Coverage Report

Created: 2026-10-28 07:19

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