Coverage Report

Created: 2026-09-14 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openbabel/src/formats/groformat.cpp
Line
Count
Source
1
/**********************************************************************
2
Copyright (C) 2010 by Reinis Danne
3
Some portions Copyright (C) 2004 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
15
#include <openbabel/babelconfig.h>
16
#include <openbabel/obmolecformat.h>
17
#include <openbabel/mol.h>
18
#include <openbabel/atom.h>
19
#include <openbabel/elements.h>
20
#include <openbabel/generic.h>
21
#include <openbabel/obiter.h>
22
23
24
#include <sstream>
25
#include <iomanip>
26
27
using namespace std;
28
namespace OpenBabel
29
{
30
31
class GROFormat : public OBMoleculeFormat
32
{
33
public:
34
  //Register this format type ID in the constructor
35
  GROFormat()
36
12
  {
37
    /* GRO is the file extension and is case insensitive. A MIME type can be
38
       added as an optional third parameter.
39
       Multiple file extensions can be registered by adding extra statements.*/
40
12
    OBConversion::RegisterFormat("gro",this);
41
42
    /* If there are any format specific options they should be registered here
43
       so that the commandline interface works properly.
44
       The first parameter is the option name. If it is a single letter it can
45
       be concatinated with other single letter options. For output options it
46
       can be multicharcter and is then written as --optionname on the command
47
       line. The third parameter is the number of parameters the option takes.
48
       Currently this is either 1 or 0 and if it is 0 can be omitted for output
49
       options. The parameter is always text and needs to be parsed to extract
50
       a number.
51
52
       Options can apply when writing - 4th parameter is
53
       OBConversion::OUTOPTIONS or can be omitted as shown. A single letter
54
       output option is preceded by -x on the command line.
55
       Or options can apply to the input format - the 4th parameter is
56
       OBConversion::INOPTIONS. They are then preceded by -a on the command
57
       line.
58
59
       Each option letter may be reused in other formats, but within the same
60
       group, INOPTIONS or OUTOPTIONS, must take the same number of parameters
61
       (0 or 1). There will be an error message when OpenBabel  runs if there
62
       are conflicts between formats. A list of formats currently used (which
63
       may not be comprehensive) is in docs/options.html.
64
    */
65
12
    OBConversion::RegisterOptionParam("f", this, 1);
66
12
    OBConversion::RegisterOptionParam("n", this);
67
12
    OBConversion::RegisterOptionParam("s", this, 0, OBConversion::INOPTIONS);
68
69
12
  }
70
71
  /* The first line of the description should be a brief identifier, <40 chars,
72
     because it is used in dropdown lists, etc. in some user interfaces.
73
     The rest is optional.
74
75
     Describe any format specific options here. This text is parsed to provide
76
     checkboxes, etc for the GUI (for details click the control menu),
77
     so please try to keep to a similar form.
78
79
     Write options are the most common, and the "Write" is optional.
80
     The option f takes a text parameter, so that it is essential that the
81
     option is registered in the constructor of the class.
82
     Finish the options with a blank line as shown, if there are more than one
83
     group of options, or if there are further comments after them.
84
  */
85
  const char* Description() override  // required
86
0
  {
87
0
    return
88
0
    "GRO format\n"
89
0
    "This is GRO file format as used in Gromacs.\n"
90
0
    "Right now there is only limited support for element perception. It works "
91
0
    "for \nelements with one letter symbols if the atomtype starts with the "
92
0
    "same letter.\n\n"
93
94
0
    "Read Options e.g. -as\n"
95
0
    " s  Consider single bonds only\n"
96
0
    " b  Disable bonding entierly\n"
97
0
    ;
98
0
  }
99
100
  //Optional URL where the file format is specified
101
  const char* SpecificationURL() override
102
0
  {
103
0
    return "https://manual.gromacs.org/documentation/current/reference-manual/file-formats.html#gro";
104
0
  }
105
106
  /* This optional function is for formats which can contain more than one
107
     molecule. It is used to quickly position the input stream after the nth
108
     molecule without requiring to convert and discard all the n molecules.
109
     See obconversion.cpp for details.*/
110
  int SkipObjects(int n, OBConversion* pConv) override
111
0
  {
112
0
    string line = "";
113
0
    int natoms = 0;
114
0
    int nlines = 0;
115
116
0
    if (n == 0)
117
0
      n++;
118
0
    istream& ifs = *pConv->GetInStream();
119
0
    getline(ifs, line);
120
0
    ifs >> natoms;
121
0
    if (natoms <= 0 || natoms >= 100000000)
122
0
      return -1;
123
0
    nlines = (natoms+3)*n;
124
0
    while (ifs && --nlines) {
125
0
      getline(ifs, line);
126
0
    }
127
128
0
    return ifs.good() ? 1 : -1;
129
0
  }
130
131
  ////////////////////////////////////////////////////
132
  /// Declarations for the "API" interface functions. Definitions are below
133
  bool ReadMolecule(OBBase* pOb, OBConversion* pConv) override;
134
  bool WriteMolecule(OBBase* pOb, OBConversion* pConv) override;
135
};
136
  ////////////////////////////////////////////////////
137
//Make an instance of the format class
138
GROFormat theGROFormat;
139
140
/////////////////////////////////////////////////////////////////
141
142
bool GROFormat::ReadMolecule(OBBase* pOb, OBConversion* pConv)
143
0
{
144
0
  OBMol* pmol = pOb->CastAndClear<OBMol>();
145
0
  if (pmol == nullptr)
146
0
      return false;
147
148
0
  istream& ifs = *pConv->GetInStream();
149
150
0
  pmol->BeginModify();
151
152
  /** Parse the input stream and use the OpenBabel API to populate the OBMol **/
153
154
0
  char buffer[BUFF_SIZE];
155
0
  string line = "";
156
0
  stringstream errorMsg;
157
158
0
  int natoms = 0;
159
0
  string title = "";
160
0
  long int resid = 0; // 5
161
0
  string resname = ""; //5
162
0
  string atomtype = ""; //5
163
  //long int atomid = 0; //5
164
0
  double x = 0.0; // 8.3
165
0
  double y = 0.0; // 8.3
166
0
  double z = 0.0; // 8.3
167
0
  double vx = 0.0; // 8.4
168
0
  double vy = 0.0; // 8.4
169
0
  double vz = 0.0; // 8.4
170
0
  string tempstr = "";
171
0
  long int residx = 0;
172
0
  OBAtom* atom;
173
0
  OBResidue* res = nullptr;
174
0
  OBVectorData* velocity;
175
176
0
  if (!ifs || ifs.peek() == EOF) {
177
0
    return false; // Trying to read past end of the file
178
0
  }
179
180
0
  if (!ifs.getline(buffer, BUFF_SIZE)) {
181
0
    errorMsg << "Problems reading a GRO file: "
182
0
             << "Cannot read the first line!";
183
0
    obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
184
0
    return false;
185
0
  }
186
187
  // Get the title
188
0
  title.assign(buffer);
189
0
  if (title.size() < 1) {
190
0
    title = pConv->GetTitle();
191
0
    pmol->SetTitle(title);
192
0
  } else {
193
0
    pmol->SetTitle(title);
194
0
  }
195
196
0
  if (!ifs.getline(buffer, BUFF_SIZE)) {
197
0
    errorMsg << "Problems reading a GRO file: "
198
0
             << "Cannot read the second line!";
199
0
    obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
200
0
    return false;
201
0
  }
202
203
  // Get the number of atoms
204
0
  stringstream(buffer) >> natoms;
205
0
  if (natoms < 1 || natoms >= 100000000) {
206
0
    errorMsg << "Problems reading a GRO file: "
207
0
             << "There are no atoms in the file or the second line is"
208
0
             << " incorrectly written.";
209
0
    obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
210
0
    return false;
211
0
  }
212
0
  pmol->ReserveAtoms(natoms);
213
214
  // Read all atom records
215
0
  for (int i=1; i<=natoms; i++) {
216
0
    if (!ifs.getline(buffer,BUFF_SIZE)) {
217
0
      errorMsg << "Problems reading a GRO file: "
218
0
               << "Could not read line #" << i+2 << ", file error." << endl
219
0
               << " According to the second line, there should be " << natoms
220
0
               << " atoms, and therefore " << natoms+3 << " lines in the file.";
221
0
      obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
222
0
      return false;
223
0
    }
224
225
0
    line = buffer;
226
227
    // Get atom
228
0
    atom  = pmol->NewAtom();
229
230
0
    tempstr.assign(line,0,5);
231
0
    stringstream(tempstr) >> resid;
232
233
0
    resname.assign(line,5,5);
234
0
    Trim(resname);
235
236
0
    atomtype.assign(line,10,5);
237
0
    Trim(atomtype);
238
239
    // Not used, OB assigns its own indizes
240
    //tempstr.assign(line,15,5);
241
    //stringstream(tempstr) >> atomid;
242
243
0
    tempstr.assign(line,20,8);
244
0
    stringstream(tempstr) >> x;
245
246
0
    tempstr.assign(line,28,8);
247
0
    stringstream(tempstr) >> y;
248
249
0
    tempstr.assign(line,36,8);
250
0
    stringstream(tempstr) >> z;
251
252
0
    if (line.size() > 44) {
253
0
      tempstr.assign(line,44,8);
254
0
      stringstream(tempstr) >> vx;
255
256
0
      tempstr.assign(line,52,8);
257
0
      stringstream(tempstr) >> vy;
258
259
0
      tempstr.assign(line,60,8);
260
0
      stringstream(tempstr) >> vz;
261
262
0
      velocity = new OBVectorData();
263
0
      velocity->SetData(vx, vy, vz);
264
0
      velocity->SetAttribute("Velocity");
265
0
      velocity->SetOrigin(fileformatInput);
266
0
      atom->SetData(velocity);
267
0
    }
268
269
    // OB translates this and, e.g., OW1 turns into O3
270
    // Type conversion should be done explicitly if that needs to be
271
    // controlled.
272
0
    atom->SetType(atomtype);
273
274
    // Set coordinates of the atom, multiply by 10 to convert from nm to
275
    // angstrom
276
0
    atom->SetVector(x*10, y*10, z*10);
277
278
0
    if (resid == residx) {
279
      // Add atom to an existing residue
280
0
      if (res == nullptr) {
281
0
        res = pmol->NewResidue();
282
0
        res->SetName(resname);
283
0
        res->SetNum(resid);
284
0
      }
285
0
      res->AddAtom(atom);
286
0
    } else {
287
      // Create new residue and use that
288
0
      res = pmol->NewResidue();
289
0
      res->SetName(resname);
290
0
      res->SetNum(resid);
291
0
      res->AddAtom(atom);
292
0
      residx = resid;
293
0
    }
294
295
    // Atom type has to be set in residues as AtomID
296
0
    res->SetAtomID(atom, atomtype);
297
298
    // For determining the element this doesn't work:
299
    // atom->SetAtomicNum(OBElements::GetAtomicNum(atom->GetType()));
300
    //
301
    // So the element simbol should be found while reading in the file. It
302
    // could be possible to provide an option for external atomtype<->element
303
    // simbol map. Now such functionality is not implemented.
304
305
    // TODO: Make element perception optional and improve it
306
0
    string element = "";
307
0
    if (atomtype[0] == 'C') {
308
0
      if (atomtype.find_first_of("aloru",1) == 1) {
309
0
        element.assign(atomtype,0,2);
310
0
      } else {
311
0
        element.assign(atomtype,0,1);
312
0
      }
313
0
    } else if (atomtype[0] == 'N') {
314
0
      if (atomtype.find_first_of("abei",1) == 1) {
315
0
        element.assign(atomtype,0,2);
316
0
      } else {
317
0
        element.assign(atomtype,0,1);
318
0
      }
319
0
    } else {
320
0
      element.assign(atomtype,0,1);
321
0
    }
322
0
    atom->SetAtomicNum(OBElements::GetAtomicNum(element.data()));
323
0
  }
324
325
  // Get periodic box
326
0
  if (!ifs.getline(buffer,BUFF_SIZE)) {
327
0
    errorMsg << "Problems reading a GRO file: "
328
0
             << "Could not read box vectors!" << endl;
329
0
    obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
330
0
    return false;
331
0
  }
332
333
0
  double v1x=0.0, v2y=0.0, v3z=0.0;
334
0
  double v1y=0.0, v1z=0.0, v2x=0.0;
335
0
  double v2z=0.0, v3x=0.0, v3y=0.0;
336
337
0
  stringstream ss(buffer);
338
0
  if (!ss) {
339
0
    errorMsg << "Problems reading a GRO file: "
340
0
             << "Could not read box vectors!" << endl;
341
0
    obErrorLog.ThrowError(__FUNCTION__, errorMsg.str(), obWarning);
342
0
    return false;
343
0
  }
344
345
0
  ss >> v1x >> v2y >> v3z;
346
0
  if (ss) {
347
0
    ss >> v1y >> v1z >> v2x;
348
0
    ss >> v2z >> v3x >> v3y;
349
0
  }
350
351
0
  if (!(v1x == 0.0 && v2y == 0.0 && v3z == 0.0 &&
352
0
        v1y == 0.0 && v1z == 0.0 && v2x == 0.0 &&
353
0
        v2z == 0.0 && v3x == 0.0 && v3y == 0.0)) {
354
  // Set box vectors and convert nm to angstroms
355
0
  const vector3 v1(v1x*10,v1y*10,v1z*10);
356
0
  const vector3 v2(v2x*10,v2y*10,v2z*10);
357
0
  const vector3 v3(v3x*10,v3y*10,v3z*10);
358
359
0
  OBUnitCell* cell = new OBUnitCell();
360
0
  cell->SetData(v1, v2, v3);
361
0
  pmol->SetData(cell);
362
0
  }
363
364
0
  pmol->EndModify();
365
366
  // Input options -as and -ab
367
0
  if (!pConv->IsOption("b", OBConversion::INOPTIONS)) {
368
0
    pmol->ConnectTheDots();
369
0
    if (!pConv->IsOption("s", OBConversion::INOPTIONS)) {
370
0
      pmol->PerceiveBondOrders();
371
0
    }
372
0
  }
373
0
  pmol->SetChainsPerceived();
374
375
  /* For multi-molecule formats, leave the input stream at the start of the
376
     next molecule, ready for this routine to be called again.
377
378
   * Return true if ok. Returning false means discard the OBMol and stop
379
     converting, unless the -e option is set. With a multi-molecule inputstream
380
     this will skip the current molecule and continue with the next, if
381
     SkipObjects() has been defined. If it has not, and continuation after
382
     errors is still required, it is necessary to leave the input stream at the
383
     beginning of next object when returning false;*/
384
0
  return true;
385
0
}
386
387
////////////////////////////////////////////////////////////////
388
389
bool GROFormat::WriteMolecule(OBBase* pOb, OBConversion* pConv)
390
2.03k
{
391
2.03k
  OBMol* pmol = dynamic_cast<OBMol*>(pOb);
392
2.03k
  if (pmol == nullptr)
393
0
      return false; // Stop converting
394
395
2.03k
  ostream& ofs = *pConv->GetOutStream();
396
397
  /** Write the representation of the OBMol molecule to the output stream **/
398
399
2.03k
  OBVectorData* vector;
400
2.03k
  vector3 v;
401
2.03k
  OBResidue* res = nullptr;
402
2.03k
  string tempstr = "";
403
2.03k
  long int atIdx = 0;
404
2.03k
  long int resIdx = 0;
405
406
2.03k
  ofs << pmol->GetTitle() << endl;
407
2.03k
  ofs << pmol->NumAtoms() << endl;
408
2.03k
  ofs.setf(ios::fixed);
409
410
2.03k
  FOR_ATOMS_OF_MOL(atom, pmol) {
411
    // CVE-2022-42885: atoms read from formats without residue
412
    // information return a null OBResidue. Synthesize defaults
413
    // rather than dereferencing the null pointer.
414
0
    res = atom->GetResidue();
415
0
    resIdx = (res != nullptr) ? res->GetNum() : 1;
416
    // Check if residue index excedes the field width and should be wrapped
417
0
    if (resIdx > 99999) {
418
0
      ofs << setw(5) << resIdx - 100000*int(resIdx/100000);
419
0
    } else {
420
0
      ofs << setw(5) << resIdx;
421
0
    }
422
0
    ofs << setw(5) << left
423
0
        << ((res != nullptr) ? res->GetName() : string("UNK"));
424
    // Remove whitespace from AtomID left by other formats
425
0
    tempstr = (res != nullptr) ? res->GetAtomID(&(*atom))
426
0
                               : string(atom->GetType());
427
0
    ofs << setw(5) << right << Trim(tempstr);
428
0
    atIdx = atom->GetIdx();
429
    // Check if atom index excedes the field width and should be wrapped
430
0
    if (atIdx > 99999) {
431
0
      ofs << setw(5) << atIdx - 100000*int(atIdx/100000);
432
0
    } else {
433
0
      ofs << setw(5) << atIdx;
434
0
    }
435
0
    ofs.precision(3);
436
    // OpenBabel uses angstrom, so converting to nm by dividing by 10
437
0
    ofs << setw(8) << atom->x()/10
438
0
        << setw(8) << atom->y()/10
439
0
        << setw(8) << atom->z()/10;
440
0
    if (atom->GetData("Velocity")) {
441
0
      vector = (OBVectorData*) atom->GetData("Velocity");
442
0
      v = vector->GetData();
443
0
      ofs.precision(4);
444
0
      ofs << setw(8) << v.x()
445
0
          << setw(8) << v.y()
446
0
          << setw(8) << v.z();
447
0
    }
448
0
    ofs << endl;
449
0
  }
450
451
  // On the last line of the file goes periodic box specification
452
2.03k
  if (pmol->HasData(OBGenericDataType::UnitCell)) {
453
0
    OBUnitCell* cell = (OBUnitCell*) pmol->GetData(OBGenericDataType::UnitCell);
454
0
    matrix3x3 m = cell->GetCellMatrix();
455
0
    vector3 v1 = m.GetRow(0);
456
0
    vector3 v2 = m.GetRow(1);
457
0
    vector3 v3 = m.GetRow(2);
458
459
    // Gromacs itself uses precision of 5, so it should be fine
460
0
    ofs.precision(5);
461
0
    ofs << "   " << v1.x()/10
462
0
        << "   " << v2.y()/10
463
0
        << "   " << v3.z()/10;
464
465
    // If there is any non-zero value among others, then write them all
466
0
    const double TRESHOLD = 1.0e-8;
467
0
    if (fabs(v1.y()) > TRESHOLD || fabs(v1.z()) > TRESHOLD ||
468
0
        fabs(v2.x()) > TRESHOLD || fabs(v2.z()) > TRESHOLD ||
469
0
        fabs(v3.x()) > TRESHOLD || fabs(v3.y()) > TRESHOLD) {
470
0
      ofs << "   " << v1.y()/10
471
0
          << "   " << v1.z()/10
472
0
          << "   " << v2.x()/10
473
0
          << "   " << v2.z()/10
474
0
          << "   " << v3.x()/10
475
0
          << "   " << v3.y()/10;
476
0
    }
477
2.03k
  } else {
478
    // Set to zero if there is no box data in the molecule
479
2.03k
    ofs << "   0.00000   0.00000   0.00000";
480
2.03k
  }
481
2.03k
  ofs << endl;
482
483
2.03k
  return true;
484
2.03k
}
485
486
} //namespace OpenBabel
487