Coverage Report

Created: 2026-02-26 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openbabel/src/stereo/cistrans.cpp
Line
Count
Source
1
#include <openbabel/stereo/cistrans.h>
2
#include <openbabel/mol.h>
3
#include <openbabel/atom.h>
4
#include <openbabel/oberror.h>
5
6
using namespace std;
7
8
namespace OpenBabel {
9
10
  //
11
  // OBCisTransStereo::Config struct
12
  //
13
14
  bool OBCisTransStereo::Config::operator==(const Config &other) const
15
0
  {
16
0
    if ((begin != other.begin) && (begin != other.end))
17
0
      return false;
18
0
    if ((end != other.begin) && (end != other.end))
19
0
      return false;
20
0
    if ((refs.size() != 4) || (other.refs.size() != 4))
21
0
      return false;
22
23
0
    Config u1, u2;
24
0
    if (!OBStereo::ContainsSameRefs(refs, other.refs)) {
25
      // find a ref that occurs in both
26
0
      for (OBStereo::ConstRefIter i = refs.begin(); i != refs.end(); ++i)
27
0
        if (OBStereo::ContainsRef(other.refs, *i)) {
28
0
          u1 = OBTetraPlanarStereo::ToConfig(*this, *i, OBStereo::ShapeU); // refs[0] = u1.refs[0]
29
0
          u2 = OBTetraPlanarStereo::ToConfig(other, *i, OBStereo::ShapeU); // refs[0] = u2.refs[0]
30
0
        }
31
32
      // check if they actualy share an id...
33
0
      if (u1.refs.empty())
34
0
        return false;
35
0
    } else {
36
      // normalize the other Config struct
37
0
      u1 = OBTetraPlanarStereo::ToConfig(*this, refs.at(0), OBStereo::ShapeU); // refs[0] = u1.refs[0]
38
0
      u2 = OBTetraPlanarStereo::ToConfig(other, refs.at(0), OBStereo::ShapeU); // refs[0] = u2.refs[0]
39
      // both now start with the same ref
40
      //
41
      // 2 possiblilities:
42
      //
43
      //   1 2 3 4      1 2 3 4
44
      //   |   |        |   |      <- in any case, refs[0] & refs[2] remain unchanged
45
      //   1 2 3 4      1 4 3 2
46
      //
47
0
      return (u1.refs[2] == u2.refs[2]);
48
0
    }
49
50
    // possibilities:
51
    //
52
    //   1 2 3 4
53
    //   |   |      <- refs[0] & refs[2] remain unchanged
54
    //   1 H 3 H
55
    //
56
    //   1 2 3 4
57
    //   |     |    <- refs[0] & refs[3] remain unchanged
58
    //   1 H H 4
59
    //
60
    //   1 2 3 4
61
    //   | |        <- refs[0] & refs[1] remain unchanged
62
    //   1 2 H H
63
0
    if ((u1.refs[2] == OBStereo::ImplicitRef) || (u2.refs[2] == OBStereo::ImplicitRef)) {
64
      // 1 2 H 4
65
0
      if ((u1.refs[3] == OBStereo::ImplicitRef) || (u2.refs[3] == OBStereo::ImplicitRef)) {
66
0
        return (u1.refs[1] == u2.refs[1]); // 1 2 H H
67
0
      } else {
68
0
        return (u1.refs[3] == u2.refs[3]); // 1 H H 4
69
0
      }
70
0
    } else
71
0
      return (u1.refs[2] == u2.refs[2]); // 1 2 3 4  &  1 H 3 4  &  1 2 3 H
72
73
0
    return false;
74
0
  }
75
76
  //
77
  // OBCisTransStereo class
78
  //
79
80
0
  OBCisTransStereo::OBCisTransStereo(OBMol *mol) : OBTetraPlanarStereo(mol)
81
0
  {
82
0
  }
83
84
  OBCisTransStereo::~OBCisTransStereo()
85
0
  {
86
0
  }
87
88
  bool OBCisTransStereo::IsValid() const
89
0
  {
90
0
    if ((m_cfg.begin == OBStereo::NoRef) || (m_cfg.end == OBStereo::NoRef))
91
0
      return false;
92
0
    if (m_cfg.refs.size() != 4)
93
0
      return false;
94
0
    return true;
95
0
  }
96
97
  void OBCisTransStereo::SetConfig(const Config &config)
98
0
  {
99
0
    if (config.begin == OBStereo::NoRef) {
100
0
      obErrorLog.ThrowError(__FUNCTION__,
101
0
          "OBCisTransStereo::SetConfig : double bond begin id is invalid.", obError);
102
0
      m_cfg = Config();
103
0
      return;
104
0
    }
105
0
    if (config.end == OBStereo::NoRef) {
106
0
      obErrorLog.ThrowError(__FUNCTION__,
107
0
          "OBCisTransStereo::SetConfig : double bond end id is invalid.", obError);
108
0
      m_cfg = Config();
109
0
      return;
110
0
    }
111
0
    if (config.refs.size() != 4) {
112
0
      std::stringstream ss;
113
0
      ss << "OBCisTransStereo::SetConfig : found " << config.refs.size();
114
0
      ss << " reference ids, should be 4.";
115
0
      obErrorLog.ThrowError(__FUNCTION__, ss.str(), obError);
116
0
      m_cfg = Config();
117
0
      return;
118
0
    }
119
120
    // store using U shape
121
0
    m_cfg = OBTetraPlanarStereo::ToConfig(config, config.refs.at(0), OBStereo::ShapeU);
122
0
  }
123
124
  OBCisTransStereo::Config OBCisTransStereo::GetConfig(OBStereo::Shape shape) const
125
0
  {
126
0
    if (!IsValid())
127
0
      return Config();
128
129
0
    return OBTetraPlanarStereo::ToConfig(m_cfg, m_cfg.refs.at(0), shape);
130
0
  }
131
132
  OBCisTransStereo::Config OBCisTransStereo::GetConfig(unsigned long start,
133
      OBStereo::Shape shape) const
134
0
  {
135
0
    if (!IsValid())
136
0
      return Config();
137
138
0
    return OBTetraPlanarStereo::ToConfig(m_cfg, start, shape);
139
0
  }
140
141
  bool OBCisTransStereo::IsTrans(unsigned long id1, unsigned long id2) const
142
0
  {
143
0
    return (GetTransRef(id1) == id2);
144
0
  }
145
146
  bool OBCisTransStereo::IsCis(unsigned long id1, unsigned long id2) const
147
0
  {
148
0
    return (GetCisRef(id1) == id2);
149
0
  }
150
151
  bool OBCisTransStereo::operator==(const OBCisTransStereo &other) const
152
0
  {
153
0
    if (!IsValid() || !other.IsValid())
154
0
      return false;
155
156
0
    Config u = OBTetraPlanarStereo::ToConfig(other.GetConfig(),
157
0
        m_cfg.refs.at(0), OBStereo::ShapeU);
158
0
    unsigned long a1 = u.refs.at(0);
159
0
    unsigned long b1 = u.refs.at(2);
160
161
0
    if ((a1 == OBStereo::ImplicitRef) && (b1 == OBStereo::ImplicitRef)) {
162
0
      a1 = u.refs.at(1);
163
0
      b1 = u.refs.at(3);
164
0
    }
165
166
0
    if (b1 != OBStereo::ImplicitRef)
167
0
      if (a1 == GetTransRef(b1))
168
0
        return true;
169
0
    if (a1 != OBStereo::ImplicitRef)
170
0
      if (b1 == GetTransRef(a1))
171
0
        return true;
172
173
0
    return false;
174
0
  }
175
176
  unsigned long OBCisTransStereo::GetCisOrTransRef(unsigned long id, bool getcisref) const
177
0
  {
178
0
    if (!IsValid())
179
0
      return OBStereo::NoRef;
180
181
0
    if (id == OBStereo::ImplicitRef)
182
0
      return OBStereo::NoRef;
183
184
    // find id
185
0
    for (int i = 0; i < 4; ++i) {
186
0
      if (m_cfg.refs.at(i) == id) {
187
        // Use its index to find the index of the cis (or trans) atom
188
0
        int j;
189
0
        if (getcisref) // GetCisRef
190
0
          j = 3 - i; // Convert 0 to 3, and 3 to 0
191
0
        else // GetTransRef
192
0
          j = (i > 1) ? i - 2 : i + 2;
193
194
0
        unsigned long refId = m_cfg.refs.at(j);
195
0
        return refId;
196
0
      }
197
0
    }
198
199
    // id not found
200
0
    return OBStereo::NoRef;
201
0
  }
202
203
  unsigned long OBCisTransStereo::GetTransRef(unsigned long id) const
204
0
  {
205
0
    return GetCisOrTransRef(id, false);
206
0
  }
207
208
  unsigned long OBCisTransStereo::GetCisRef(unsigned long id) const
209
0
  {
210
0
    return GetCisOrTransRef(id, true);
211
0
  }
212
213
  bool OBCisTransStereo::IsOnSameAtom(unsigned long id1, unsigned long id2) const
214
0
  {
215
0
    const OBMol *mol = GetMolecule();
216
0
    if (!mol) {
217
0
      obErrorLog.ThrowError(__FUNCTION__, "OBCisTransStereo::IsOnSameAtom : No valid molecule set", obError);
218
0
      return false;
219
0
    }
220
221
0
    OBAtom *begin = mol->GetAtomById(m_cfg.begin);
222
0
    if (!begin) {
223
0
      obErrorLog.ThrowError(__FUNCTION__, "OBCisTransStereo::IsOnSameAtom : Begin reference id is not valid.", obError);
224
0
      return false;
225
0
    }
226
0
    OBAtom *end = mol->GetAtomById(m_cfg.end);
227
0
    if (!end) {
228
0
      obErrorLog.ThrowError(__FUNCTION__, "OBCisTransStereo::IsOnSameAtom : End reference id is not valid.", obError);
229
0
      return false;
230
0
    }
231
232
0
    OBAtom *a = mol->GetAtomById(id1);
233
0
    OBAtom *b = mol->GetAtomById(id2);
234
235
0
    if (a && b) {
236
      // both on begin atom?
237
0
      if (a->IsConnected(begin) && b->IsConnected(begin))
238
0
          return true;
239
      // both on end atom?
240
0
      if (a->IsConnected(end) && b->IsConnected(end))
241
0
          return true;
242
0
      return false;
243
0
    } else {
244
0
      if (a) {
245
        // b atom not found, could be a deleted hydrogen...
246
0
        if (a->IsConnected(begin)) {
247
          // a is connected to begin. if this is the atom missing a hydrogen, return false
248
0
          if (begin->GetExplicitDegree() == 2)
249
0
            return true;
250
          // check if the end atom really is missing an atom
251
0
          if (end->GetExplicitDegree() != 2) {
252
0
            obErrorLog.ThrowError(__FUNCTION__,
253
0
                "OBCisTransStereo::IsOnSameAtom : id2 is not valid and is not a missing hydrogen.", obError);
254
0
            return false;
255
0
          }
256
          // inform user we are treating id2 as deleted hydrogen
257
0
          obErrorLog.ThrowError(__FUNCTION__,
258
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id2 doesn't exist anymore, must be a (deleted) hydrogen.", obInfo);
259
0
        } else if (a->IsConnected(end)) {
260
          // a is connected to end. again, if this is the atom missing a hydrogen, return false
261
0
          if (end->GetExplicitDegree() == 2)
262
0
            return true;
263
          // check if the begin atom really is missing an atom
264
0
          if (begin->GetExplicitDegree() != 2) {
265
0
            obErrorLog.ThrowError(__FUNCTION__,
266
0
                "OBCisTransStereo::IsOnSameAtom : id2 is not valid and is not a missing hydrogen.", obError);
267
0
            return true;
268
0
          }
269
          // inform user we are treating id2 as deleted hydrogen
270
0
          obErrorLog.ThrowError(__FUNCTION__,
271
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id2 doesn't exist, must be a (deleted) hydrogen.", obInfo);
272
273
0
        } else {
274
0
          obErrorLog.ThrowError(__FUNCTION__,
275
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id1 isn't connected to the begin or end atom.", obError);
276
0
          return true;
277
0
        }
278
0
      } else if (b) {
279
        // a atom not found, could be a deleted hydrogen...
280
0
        if (b->IsConnected(begin)) {
281
          // b is connected to begin. if this is the atom missing a hydrogen, return false
282
0
          if (begin->GetExplicitDegree() == 2)
283
0
            return true;
284
          // check if the end atom really is missing an atom
285
0
          if (end->GetExplicitDegree() != 2) {
286
0
            obErrorLog.ThrowError(__FUNCTION__,
287
0
                "OBCisTransStereo::IsOnSameAtom : id1 is not valid and is not a missing hydrogen.", obError);
288
0
            return true;
289
0
          }
290
          // inform user we are treating id1 as deleted hydrogen
291
0
          obErrorLog.ThrowError(__FUNCTION__,
292
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id1 doesn't exist, must be a (deleted) hydrogen.", obInfo);
293
0
        } else if (b->IsConnected(end)) {
294
          // a is connected to end. again, if this is the atom missing a hydrogen, return false
295
0
          if (end->GetExplicitDegree() == 2)
296
0
            return true;
297
          // check if the begin atom really is missing an atom
298
0
          if (begin->GetExplicitDegree() != 2) {
299
0
            obErrorLog.ThrowError(__FUNCTION__,
300
0
                "OBCisTransStereo::IsOnSameAtom : id1 is not valid and is not a missing hydrogen.", obError);
301
0
            return true;
302
0
          }
303
          // inform user we are treating id2 as deleted hydrogen
304
0
          obErrorLog.ThrowError(__FUNCTION__,
305
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id1 doesn't exist, must be a (deleted) hydrogen.", obInfo);
306
0
        } else {
307
0
          obErrorLog.ThrowError(__FUNCTION__,
308
0
              "OBCisTransStereo::IsOnSameAtom : Atom with id1 isn't connected to the begin or end atom.", obError);
309
0
          return true;
310
0
        }
311
0
      } else {
312
0
        OBAtom *c = nullptr, *d = nullptr;
313
        // no a & b, check the remaining ids which will reveal same info
314
0
        for (int i = 0; i < 4; ++i) {
315
0
          if ((m_cfg.refs.at(i) == id1) || (m_cfg.refs.at(i) == id2))
316
0
            continue;
317
0
          if (!c) {
318
0
            c = mol->GetAtomById(m_cfg.refs.at(i));
319
0
          } else {
320
0
            d = mol->GetAtomById(m_cfg.refs.at(i));
321
0
          }
322
0
        }
323
0
        if (!c || !d) {
324
0
          obErrorLog.ThrowError(__FUNCTION__, "OBCisTransStereo::IsOnSameAtom : invalid stereochemistry!", obError);
325
0
          return true;
326
0
        }
327
0
        if ((begin->GetExplicitDegree() != 2) || (end->GetExplicitDegree() != 2)) {
328
0
          obErrorLog.ThrowError(__FUNCTION__, "OBCisTransStereo::IsOnSameAtom : invalid stereochemistry!", obError);
329
0
          return true;
330
0
        }
331
0
        obErrorLog.ThrowError(__FUNCTION__,
332
0
            "OBCisTransStereo::IsOnSameAtom : Atoms with id1 & id2 don't exist, must be a (deleted) hydrogens.", obInfo);
333
0
        return IsOnSameAtom(c->GetId(), d->GetId());
334
0
      }
335
0
    }
336
337
0
    return false;
338
0
  }
339
340
  OBGenericData* OBCisTransStereo::Clone(OBBase *mol) const
341
0
  {
342
0
    OBCisTransStereo *data = new OBCisTransStereo(static_cast<OBMol*>(mol));
343
0
    data->SetConfig(m_cfg);
344
0
    return data;
345
0
  }
346
347
} // namespace OpenBabel
348
349
namespace std {
350
351
  ostream& operator<<(ostream &out, const OpenBabel::OBCisTransStereo &ct)
352
0
  {
353
0
    OpenBabel::OBCisTransStereo::Config cfg = ct.GetConfig();
354
0
    out << "OBCisTransStereo(begin = " << cfg.begin;
355
0
    out << ", end = " << cfg.end;
356
357
0
    out << ", refs = ";
358
0
    for (OpenBabel::OBStereo::Refs::iterator i = cfg.refs.begin(); i != cfg.refs.end(); ++i)
359
0
      if (*i != OpenBabel::OBStereo::ImplicitRef)
360
0
        out << *i << " ";
361
0
      else
362
0
        out << "H ";
363
364
0
    switch (cfg.shape) {
365
0
      case OpenBabel::OBStereo::ShapeU:
366
0
        out << ", shape = U)";
367
0
        break;
368
0
      case OpenBabel::OBStereo::ShapeZ:
369
0
        out << ", shape = Z)";
370
0
        break;
371
0
      case OpenBabel::OBStereo::Shape4:
372
0
        out << ", shape = 4)";
373
0
        break;
374
0
    }
375
376
0
    return out;
377
0
  }
378
379
  ostream& operator<<(ostream &out, const OpenBabel::OBCisTransStereo::Config &cfg)
380
0
  {
381
0
    out << "OBCisTransStereo::Config(begin = " << cfg.begin;
382
0
    out << ", end = " << cfg.end;
383
384
0
    out << ", refs = ";
385
0
    for (OpenBabel::OBStereo::Refs::const_iterator i = cfg.refs.begin(); i != cfg.refs.end(); ++i)
386
0
      if (*i != OpenBabel::OBStereo::ImplicitRef)
387
0
        out << *i << " ";
388
0
      else
389
0
        out << "H ";
390
391
0
    switch (cfg.shape) {
392
0
      case OpenBabel::OBStereo::ShapeU:
393
0
        out << ", shape = U)";
394
0
        break;
395
0
      case OpenBabel::OBStereo::ShapeZ:
396
0
        out << ", shape = Z)";
397
0
        break;
398
0
      case OpenBabel::OBStereo::Shape4:
399
0
        out << ", shape = 4)";
400
0
        break;
401
0
    }
402
403
0
    return out;
404
0
  }
405
406
} // namespace std
407