Coverage Report

Created: 2026-04-09 06:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openbabel/src/alias.cpp
Line
Count
Source
1
/**********************************************************************
2
alias.cpp - implementation of an OBGenericData class to hold alias information on atoms
3
Copyright (C) 2008 by Chris Morley
4
5
This program is free software; you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation version 2 of the License.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
***********************************************************************/
14
#include <openbabel/babelconfig.h>
15
#include <cstdlib>
16
#include <memory>
17
#include <sstream>
18
#include <string>
19
#include <openbabel/alias.h>
20
#include <openbabel/obconversion.h>
21
#include <openbabel/op.h>
22
#include <openbabel/mol.h>
23
#include <openbabel/atom.h>
24
#include <openbabel/bond.h>
25
#include <openbabel/builder.h>
26
#include <openbabel/obiter.h>
27
#include <openbabel/parsmart.h>
28
#include <openbabel/mcdlutil.h>
29
#include <openbabel/elements.h>
30
#include <openbabel/generic.h>
31
32
0
#define MARK_UNUSED(x) (void)(x)
33
34
using namespace std;
35
namespace OpenBabel
36
{
37
  std::string AliasData::GetAlias(bool rightAligned)const
38
0
  {
39
0
    if(rightAligned)
40
0
    {
41
0
      if(!_right_form.empty())
42
0
        return _right_form;
43
0
      if(table().find(_alias)!=table().end())
44
0
        return table().find(_alias)->second.right_form;
45
0
    }
46
0
    return _alias;
47
0
  }
48
49
  bool AliasData::Expand(OBMol& mol, const unsigned int atomindex)
50
236k
  {
51
    /*
52
    Interprets the alias text and adds atom(s) as appropriate to mol.
53
    Tries the following in turn until one is successful:
54
    1) If starts with number treat as isotope+element e.g. 2H
55
    2) Looks up alias in superatom.txt e.g. COOH Pr
56
    3) If of the form Rn stored as a * atom with Atom Class data
57
    Returns false if none are successful.
58
    */
59
60
    //parse as isotopic atom
61
236k
    if(isdigit(_alias[0]))
62
142k
    {
63
142k
      std::stringstream ss(_alias);
64
142k
      int iso;
65
142k
      std::string el;
66
142k
      ss >> iso >>el;
67
142k
      unsigned int elemno = OBElements::GetAtomicNum(el.c_str());
68
142k
      if(elemno > 0)
69
40.9k
      {
70
40.9k
        OBAtom* pAtom = mol.GetAtom(atomindex);
71
40.9k
        if(!pAtom)
72
0
          return false;
73
40.9k
        pAtom->SetIsotope(iso);
74
40.9k
        pAtom->SetAtomicNum(elemno);
75
40.9k
        return true;
76
40.9k
      }
77
142k
    }
78
79
196k
    if(FromNameLookup(mol, atomindex))
80
0
      return true;
81
82
    // Rn is stored as an atom with 0 atomic number and atomclass = n
83
    // R', R'' etc. are treated as R1, R2
84
    // Note that if the name contains anything after the number it is ignored.
85
196k
    if(_alias[0]=='R' && (_alias[1]=='\'' || isdigit(_alias[1])))
86
3.27k
    {
87
3.27k
      unsigned int n = 1;
88
3.27k
      if(_alias[1]=='\'')
89
110k
        while(n<_alias.size()-1 && _alias[n]==_alias[n+1]) n++;
90
379
      else
91
379
        n = atoi(_alias.c_str()+1);
92
93
3.27k
      OBPairInteger *atomclass = new OBPairInteger();
94
3.27k
      atomclass->SetAttribute("Atom Class");
95
3.27k
      atomclass->SetValue(n);
96
3.27k
      mol.GetAtom(atomindex)->SetData(atomclass);
97
98
3.27k
      if(atomindex <= mol.NumAtoms()) //needed for Rn aliases in mdlformat
99
3.27k
        mol.GetAtom(atomindex)->SetAtomicNum(0);
100
101
3.27k
      _right_form = _alias;
102
3.27k
      return true;
103
3.27k
    }
104
105
192k
    obErrorLog.ThrowError(__FUNCTION__, "Alias " + _alias +
106
192k
      " was not chemically interpreted\n", obWarning, onceOnly);
107
192k
    return false;
108
196k
  }
109
110
bool AliasData::FromNameLookup(OBMol& mol, const unsigned int atomindex)
111
196k
{
112
  /*Converts an alias name (like COOH) to real chemistry:
113
    looks up in a table loaded from superatom.txt;
114
    converts the SMILES of the fragment and adds it to the molecule.
115
    If the molecule already has atom coordinates, generates coordinates
116
    for the new atoms, using builder for 3D and MCDL for 2D.
117
  */
118
119
196k
  OBAtom* XxAtom = mol.GetAtom(atomindex);
120
/*  if(XxAtom->GetExplicitDegree()>1)
121
  {
122
    obErrorLog.ThrowError(__FUNCTION__, _alias + " is multivalent, which is currently not supported.", obWarning);
123
    return false;
124
  }
125
*/
126
196k
  SuperAtomTable::iterator pos = table().find(_alias);
127
196k
  if(pos==table().end())
128
196k
    return false;
129
130
0
  int dimension=0;
131
0
  if(mol.Has3D())
132
0
    dimension=3;
133
0
  else if(mol.Has2D())
134
0
    dimension=2;
135
0
  mol.SetDimension(dimension);
136
137
  //Convert SMILES of alias
138
0
  OBConversion conv;
139
0
  OBMol obFrag;
140
0
  obFrag.SetIsPatternStructure();
141
0
  if(conv.SetInFormat("smi"))
142
0
  {
143
0
    conv.ReadString(&obFrag, '*' + pos->second.smiles);//Add dummy atom to SMILES
144
0
    _right_form = pos->second.right_form;
145
0
    _color      = pos->second.color;
146
0
  }
147
0
  obFrag.SetDimension(dimension);//will be same as parent
148
149
  //Find index of *first* atom to which XxAtom is attached (could be NULL)
150
0
  OBBondIterator bi;
151
0
  OBAtom* firstAttachAtom = XxAtom->BeginNbrAtom(bi);
152
0
  unsigned mainAttachIdx = firstAttachAtom ? firstAttachAtom->GetIdx() : 0;
153
0
  unsigned int firstAttachFlags = 0;
154
0
  unsigned int firstAttachOrder = 1;
155
0
  if (firstAttachAtom) {
156
0
    firstAttachFlags = mol.GetBond(XxAtom, firstAttachAtom)->GetFlags();
157
0
    firstAttachOrder = mol.GetBond(XxAtom, firstAttachAtom)->GetBondOrder();
158
0
  }
159
  //++Make list of other attachments* of XxAtom
160
  // (Added later so that the existing bonding of the XXAtom are retained)
161
0
  vector<pair<OBAtom*, unsigned> > otherAttachments;
162
0
  OBAtom* pAttach;
163
0
  while(firstAttachAtom && (pAttach = XxAtom->NextNbrAtom(bi)) ) // extra parentheses to minimize warnings
164
0
    otherAttachments.push_back(make_pair(pAttach, (*bi)->GetBondOrder()));
165
166
  //Copy coords of XxAtom to the first real atom in the fragment
167
  //so that the connecting bond is well defined for 2D case
168
0
  obFrag.GetAtom(2)->SetVector( XxAtom->GetVector());
169
170
  //delete original Xx atom
171
0
  mol.DeleteAtom(XxAtom, false);//delay deletion of the OBAtom object because this is attached to it
172
  //Correct indices for the deletion
173
0
  if(atomindex<mainAttachIdx)
174
0
    --mainAttachIdx;
175
176
  //Find the eventual index of first atom in fragment
177
0
  unsigned newFragIdx = mol.NumAtoms()+1;
178
179
  //Give the fragment appropriate coordinates
180
0
  if(dimension==3)
181
0
  {
182
0
    OBBuilder builder;
183
184
0
    builder.Build(obFrag);
185
0
    obFrag.DeleteAtom(obFrag.GetAtom(1));//remove dummy atom
186
0
    mol += obFrag; //Combine with main molecule
187
0
    if(mainAttachIdx) {
188
0
      builder.Connect(mol, mainAttachIdx, newFragIdx,XxAtom->GetVector(),firstAttachOrder);
189
0
    }
190
0
  }
191
0
  else // 0D, 2D
192
0
  {
193
0
    obFrag.DeleteAtom(obFrag.GetAtom(1));//remove dummy atom
194
0
    mol += obFrag; //Combine with main molecule and connect
195
0
    if(mainAttachIdx)
196
0
      mol.AddBond(mainAttachIdx, newFragIdx, 1, firstAttachFlags);
197
0
  }
198
199
0
  if(dimension==2)//Use MCDL
200
0
    groupRedraw(&mol, mol.NumBonds()-1, newFragIdx, true);
201
202
  //++Add bonds from list to newFragIdx
203
0
  while(!otherAttachments.empty())
204
0
  {
205
0
    mol.AddBond(otherAttachments.back().first->GetIdx(), newFragIdx, otherAttachments.back().second);
206
0
    otherAttachments.pop_back();
207
0
  }
208
209
  //Store the ids of the atoms which replace the alias (the last atoms in the combined molecule).
210
  //The ids do not change when other atoms are deleted.
211
0
  for(unsigned i=obFrag.NumAtoms();i;--i)
212
0
    _expandedatoms.push_back(mol.GetAtom(mol.NumAtoms()-i +1)->GetId());
213
214
  //Make a copy of this AliasData object (currently attached to XxAtom)
215
  //and attach it to the first atom of the fragment.
216
0
  mol.GetAtom(newFragIdx)->CloneData(this);
217
218
0
  delete(XxAtom);
219
0
  return true;
220
196k
}
221
222
bool AliasData::LoadFile(SuperAtomTable& table)
223
392k
{
224
  //In table: key=alias left-form; value=pair<alias right-form, SMILES>
225
392k
  ifstream ifs;
226
392k
  if (OpenDatafile(ifs, "superatom.txt").length() == 0)
227
392k
  {
228
392k
    obErrorLog.ThrowError(__FUNCTION__, "Cannot open superatom.txt", obError);
229
392k
    return false;
230
392k
  }
231
0
  string ln;
232
0
  while(getline(ifs, ln))
233
0
  {
234
0
    if (ln[0]=='#' || ln.empty())
235
0
      continue;
236
0
    std::vector<string> vec;
237
0
    if(tokenize(vec, ln) && vec.size()>=3)
238
0
    {
239
      //table[ vec[0] ] = make_pair(vec[1], vec[2]);
240
0
      AliasItem item;
241
0
      item.right_form = vec[1];
242
0
      item.smiles     = vec[2];
243
0
      item.color      = vec.size()>=4 ? vec[3] : "";
244
0
      table[ vec[0] ] = item;
245
0
    }
246
0
  }
247
0
  return true;
248
392k
}
249
250
bool AliasData::LoadFile(SmartsTable& smtable)
251
0
{
252
  //Re-parse the datafile. Seems simpler than trying to extract from the map.
253
0
  ifstream ifs;
254
0
  if (OpenDatafile(ifs, "superatom.txt").length() == 0)
255
0
  {
256
0
    obErrorLog.ThrowError(__FUNCTION__, "Cannot open superatom.txt", obError);
257
0
    return false;
258
0
  }
259
0
  string ln;
260
0
  while(getline(ifs, ln))
261
0
  {
262
0
    if ((ln[0]=='#' && ln[1]!='#') || ln.empty())
263
0
      continue;
264
0
    if(ln[0]=='#') //stop reading at line starting with ##
265
0
      break;
266
0
    std::vector<string> vec;
267
0
    if(tokenize(vec, ln) && vec.size()>=3)
268
0
    {
269
      //Convert SMILES with implicit H to SMARTS with explicit H.
270
      //Converting into and out of OBMol is a bit heavy, but saves
271
      //worrying about edge cases in a string parse.
272
0
      stringstream ss('*'+vec[2]),// '*' added to SMILES because the superatom has to be attached
273
0
                   ssmarts;
274
0
      OBConversion conv(&ss, &ssmarts);
275
0
      conv.AddOption("h",OBConversion::GENOPTIONS);//add explicit Hs...
276
0
      conv.AddOption("h");//...and output them to ensure the superatom itself is not substituted
277
0
      if(conv.SetInAndOutFormats("smi","smi"))
278
0
        conv.Convert();
279
0
      if(!ssmarts.str().empty())
280
0
      {
281
        //OBSmartsPattern objects are not copyable without complications,
282
        //so reference semantics used.
283
284
0
        std::shared_ptr<OBSmartsPattern> psp(new OBSmartsPattern);
285
0
        psp->Init(ssmarts.str());
286
0
        smtable.push_back(make_pair(vec[0], psp));
287
0
      }
288
0
    }
289
0
  }
290
0
  return true;
291
0
}
292
293
0
void AliasData::AddExpandedAtom(int id) { _expandedatoms.push_back(id); };
294
295
void AliasData::DeleteExpandedAtoms(OBMol& mol)
296
0
{
297
  //The atom that carries the AliasData object remains as an Xx atom with no charge;
298
  //the others are deleted. All the attached hydrogens are also deleted.
299
0
  for(unsigned i=0;i<_expandedatoms.size();++i)
300
0
  {
301
0
    OBAtom* at = mol.GetAtomById(_expandedatoms[i]);
302
0
    if(!at)
303
0
      continue;
304
0
    mol.DeleteHydrogens(at);
305
0
    if(at->HasData(AliasDataType))
306
0
    {
307
0
      at->SetAtomicNum(0);
308
0
      at->SetFormalCharge(0);
309
0
      at->SetSpinMultiplicity(0);
310
0
    }
311
0
    else
312
0
      mol.DeleteAtom(at);
313
0
  }
314
0
  _expandedatoms.clear();
315
0
}
316
317
void AliasData::RevertToAliasForm(OBMol& mol)
318
0
{
319
  //Deleting atoms invalidates the iterator, so start again
320
  //and continue until all no unexpanded aliases are found in molecule.
321
0
  bool acted = false;
322
0
  do
323
0
  {
324
0
    FOR_ATOMS_OF_MOL(a, mol)
325
0
    {
326
0
      acted=false;
327
0
      AliasData* ad = nullptr;
328
0
      if((ad = (static_cast<AliasData*>(a->GetData(AliasDataType)))) && ad->IsExpanded())
329
0
      {
330
0
        ad->DeleteExpandedAtoms(mol);
331
0
        acted = true;
332
0
        break;
333
0
      }
334
0
    }
335
0
  }while(acted);
336
0
}
337
338
bool AliasData::AddAliases(OBMol* pmol)
339
0
{
340
0
  static SmartsTable smtable;
341
0
  if(smtable.empty())
342
0
    LoadFile(smtable);
343
0
  set<int> AllExAtoms;
344
0
  SmartsTable::iterator iter;
345
0
  for(iter=smtable.begin();iter!=smtable.end();++iter)
346
0
  {
347
0
    if((*iter).second->Match(*pmol))
348
0
    {
349
0
      vector<std::vector<int> > mlist = (*iter).second->GetUMapList();
350
0
      for(unsigned imatch=0;imatch<mlist.size();++imatch) //each match
351
0
      {
352
0
        AliasData* ad  = new AliasData;
353
0
        ad->SetAlias((*iter).first);
354
        //iatom==0 is the * that was added to the front of the SMILES, so start at 1
355
0
        for(unsigned iatom=1; iatom<mlist[imatch].size();++iatom)//each atom in match
356
0
        {
357
0
          int idx = mlist[imatch][iatom];
358
359
0
          if(AllExAtoms.count(idx))
360
0
          {
361
            //atom already appears in an alias so abandon this (smaller) alias
362
0
            delete ad;
363
0
            ad = nullptr;
364
0
            break;
365
0
          }
366
0
          else
367
0
          {
368
0
            OBAtom* a = pmol->GetAtom(idx);
369
            // atom might not be present in original molecule, because SMARTS are all with hydrogens added
370
            // original molecule might lack them
371
0
            if (a != nullptr) {
372
0
              AllExAtoms.insert(idx);
373
0
              int id  = a->GetId();
374
0
              ad->AddExpandedAtom(id);
375
0
            }
376
0
          }
377
0
        }
378
0
        if(ad)
379
0
          pmol->GetAtom(mlist[imatch][1])->SetData(ad);//attach alias to first expanded atom
380
0
      }
381
0
    }
382
0
  }
383
0
  return true;
384
0
}
385
386
//OpGenAlias is a wrapper for AddAliases. Use like:
387
//babel infile.xxx outfile.yyy --genalias
388
class OpGenAlias : public OBOp
389
{
390
public:
391
6
  OpGenAlias(const char* ID) : OBOp(ID, false){};
392
0
  const char* Description() override { return "Generate aliases as an alternative representation."; }
393
394
0
  bool WorksWith(OBBase* pOb) const override { return dynamic_cast<OBMol*>(pOb) != nullptr; }
395
  bool Do(OBBase* pOb, const char* OptionText, OpMap* pmap, OBConversion*) override;
396
};
397
398
/////////////////////////////////////////////////////////////////
399
OpGenAlias theOpGenAlias("genalias"); //Global instance
400
401
/////////////////////////////////////////////////////////////////
402
bool OpGenAlias::Do(OBBase* pOb, const char* OptionText, OpMap* pmap, OBConversion*)
403
0
{
404
  // Mark variables as unused to avoid warnings
405
0
  MARK_UNUSED(OptionText);
406
0
  MARK_UNUSED(pmap);
407
408
0
  OBMol* pmol = dynamic_cast<OBMol*>(pOb);
409
0
  if(!pmol)
410
0
    return false;
411
0
  return AliasData::AddAliases(pmol);
412
0
}
413
414
}//namespace
415
416
//! \file alias.cpp
417
//! \brief OBGenericData class to for atom alias data (e.g., in 2D drawing programs for "COOH")