File: filewriter.py

package info (click to toggle)
cclib-data 1.6.2-2
  • links: PTS, VCS
  • area: non-free
  • in suites: bookworm, bullseye, sid
  • size: 87,912 kB
  • sloc: python: 16,440; sh: 131; makefile: 79; cpp: 31
file content (150 lines) | stat: -rw-r--r-- 5,225 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# -*- coding: utf-8 -*-
#
# Copyright (c) 2018, the cclib development team
#
# This file is part of cclib (http://cclib.github.io) and is distributed under
# the terms of the BSD 3-Clause License.

"""Generic file writer and related tools"""

import logging
import sys
from abc import ABCMeta, abstractmethod
from six import add_metaclass


if sys.version_info <= (3, 3):
    from collections import Iterable
else:
    from collections.abc import Iterable

import numpy

from cclib.parser.utils import PeriodicTable
from cclib.parser.utils import find_package

_has_openbabel = find_package("openbabel")
if _has_openbabel:
    from cclib.bridge import makeopenbabel
    import openbabel as ob
    import openbabel.pybel as pb


class MissingAttributeError(Exception):
    pass


@add_metaclass(ABCMeta)
class Writer:
    """Abstract class for writer objects."""

    required_attrs = ()

    def __init__(self, ccdata, jobfilename=None, indices=None, terse=False,
                 *args, **kwargs):
        """Initialize the Writer object.

        This should be called by a subclass in its own __init__ method.

        Inputs:
          ccdata - An instance of ccData, parsed from a logfile.
          jobfilename - The filename of the parsed logfile.
          indices - One or more indices for extracting specific geometries/etc. (zero-based)
          terse - Whether to print the terse version of the output file - currently limited to cjson/json formats
        """

        self.ccdata = ccdata
        self.jobfilename = jobfilename
        self.indices = indices
        self.terse = terse
        self.ghost = kwargs.get("ghost")

        self.pt = PeriodicTable()

        self._check_required_attributes()

        # Open Babel isn't necessarily present.
        if _has_openbabel:
            # Generate the Open Babel/Pybel representation of the molecule.
            # Used for calculating SMILES/InChI, formula, MW, etc.
            self.obmol, self.pbmol = self._make_openbabel_from_ccdata()
            self.bond_connectivities = self._make_bond_connectivity_from_openbabel(self.obmol)

        self._fix_indices()

    @abstractmethod
    def generate_repr(self):
        """Generate the written representation of the logfile data."""

    def _calculate_total_dipole_moment(self):
        """Calculate the total dipole moment."""

        # ccdata.moments may exist, but only contain center-of-mass coordinates
        if len(getattr(self.ccdata, 'moments', [])) > 1:
            return numpy.linalg.norm(self.ccdata.moments[1])

    def _check_required_attributes(self):
        """Check if required attributes are present in ccdata."""
        missing = [x for x in self.required_attrs
                   if not hasattr(self.ccdata, x)]
        if missing:
            missing = ' '.join(missing)
            raise MissingAttributeError(
                'Could not parse required attributes to write file: ' + missing)

    def _make_openbabel_from_ccdata(self):
        """Create Open Babel and Pybel molecules from ccData."""
        if not hasattr(self.ccdata, 'charge'):
            logging.warning("ccdata object does not have charge, setting to 0")
            _charge = 0
        else:
            _charge = self.ccdata.charge
        if not hasattr(self.ccdata, 'mult'):
            logging.warning("ccdata object does not have spin multiplicity, setting to 1")
            _mult = 1
        else:
            _mult = self.ccdata.mult
        obmol = makeopenbabel(self.ccdata.atomcoords,
                              self.ccdata.atomnos,
                              charge=_charge,
                              mult=_mult)
        if self.jobfilename is not None:
            obmol.SetTitle(self.jobfilename)
        return (obmol, pb.Molecule(obmol))

    def _make_bond_connectivity_from_openbabel(self, obmol):
        """Based upon the Open Babel/Pybel molecule, create a list of tuples
        to represent bonding information, where the three integers are
        the index of the starting atom, the index of the ending atom,
        and the bond order.
        """
        bond_connectivities = []
        for obbond in ob.OBMolBondIter(obmol):
            bond_connectivities.append((obbond.GetBeginAtom().GetIndex(),
                                        obbond.GetEndAtom().GetIndex(),
                                        obbond.GetBondOrder()))
        return bond_connectivities

    def _fix_indices(self):
        """Clean up the index container type and remove zero-based indices to
        prevent duplicate structures and incorrect ordering when
        indices are later sorted.
        """
        if not self.indices:
            self.indices = set()
        elif not isinstance(self.indices, Iterable):
            self.indices = set([self.indices])
        # This is the most likely place to get the number of
        # geometries from.
        if hasattr(self.ccdata, 'atomcoords'):
            lencoords = len(self.ccdata.atomcoords)
            indices = set()
            for i in self.indices:
                if i < 0:
                    i += lencoords
                indices.add(i)
            self.indices = indices
        return


del find_package