Coverage for cclib/io/moldenwriter.py : 80%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2017, the cclib development team
4#
5# This file is part of cclib (http://cclib.github.io) and is distributed under
6# the terms of the BSD 3-Clause License.
8"""A writer for MOLDEN format files."""
10import os.path
11import math
12import decimal
13import numpy
15from cclib.parser import utils
16from cclib.io import filewriter
19def round_molden(num, p=6):
20 """Molden style number rounding in [Atoms] section."""
21 # Digit at pth position after dot.
22 p_digit = math.floor(abs(num) * 10 ** p) % 10
23 # If the 6th digit after dot is greater than 5, but is not 7,
24 # round the number upto 6th place.
25 # Else truncate at 6th digit after dot.
26 if p_digit > 5 and p_digit != 7:
27 return round(num, p)
28 if num >= 0:
29 return math.floor(num * 10 ** p) / 10 ** p
30 else:
31 return math.ceil(num * 10 ** p) / 10 ** p
34class MOLDEN(filewriter.Writer):
35 """A writer for MOLDEN files."""
37 required_attrs = ('atomcoords', 'atomnos', 'natom')
39 def _title(self, path):
40 """Return filename without extension to be used as title."""
41 title = os.path.basename(os.path.splitext(path)[0])
42 return title
44 def _coords_from_ccdata(self, index):
45 """Create [Atoms] section using geometry at the given index."""
46 elements = [self.pt.element[Z] for Z in self.ccdata.atomnos]
47 if self.ghost is not None:
48 elements = [self.ghost if e is None else e for e in elements]
49 elif None in elements:
50 raise ValueError('It seems that there is at least one ghost atom ' +
51 'in these elements. Please use the ghost flag to'+
52 ' specify a label for the ghost atoms.')
53 atomcoords = self.ccdata.atomcoords[index]
54 atomnos = self.ccdata.atomnos
55 nos = range(self.ccdata.natom)
57 # element_name number atomic_number x y z
58 atom_template = '{:2s} {:5d} {:2d} {:12.6f} {:12.6f} {:12.6f}'
59 lines = []
60 for element, no, atomno, coord in zip(elements, nos, atomnos,
61 atomcoords):
62 x, y, z = map(round_molden, coord)
63 lines.append(atom_template.format(element, no + 1, atomno,
64 x, y, z))
66 return lines
68 def _gto_from_ccdata(self):
69 """Create [GTO] section using gbasis.
71 atom_sequence_number1 0
72 shell_label number_of_primitives 1.00
73 exponent_primitive_1 contraction_coeff_1 (contraction_coeff_1)
74 ...
75 empty line
76 atom_sequence__number2 0
77 """
79 gbasis = self.ccdata.gbasis
80 label_template = '{:s} {:5d} 1.00'
81 basis_template = '{:15.9e} {:15.9e}'
82 lines = []
84 for no, basis in enumerate(gbasis):
85 lines.append('{:3d} 0'.format(no + 1))
86 for prims in basis:
87 lines.append(label_template.format(prims[0].lower(),
88 len(prims[1])))
89 for prim in prims[1]:
90 lines.append(basis_template.format(prim[0], prim[1]))
91 lines.append('')
92 lines.append('')
93 return lines
95 def _scfconv_from_ccdata(self):
96 """Create [SCFCONV] section using gbasis.
98 scf-first 1 THROUGH 12
99 -672.634394
100 ...
101 -673.590571
102 -673.590571
103 """
105 lines = ["scf-first 1 THROUGH %d" % len(self.ccdata.scfenergies)]
107 for scfenergy in self.ccdata.scfenergies:
108 lines.append('{:15.6f}'.format(scfenergy))
110 return lines
112 def _rearrange_mocoeffs(self, mocoeffs):
113 """Rearrange cartesian F functions in mocoeffs.
115 Molden's order:
116 xxx, yyy, zzz, xyy, xxy, xxz, xzz, yzz, yyz, xyz
117 cclib's order:
118 XXX, YYY, ZZZ, XXY, XXZ, YYX, YYZ, ZZX, ZZY, XYZ
119 cclib's order can be converted by:
120 moving YYX two indexes ahead, and
121 moving YYZ two indexes back.
122 """
123 aonames = self.ccdata.aonames
124 mocoeffs = mocoeffs.tolist()
126 pos_yyx = [key for key, val in enumerate(aonames) if '_YYX' in val]
127 pos_yyz = [key for key, val in enumerate(aonames) if '_YYZ' in val]
129 if pos_yyx:
130 for pos in pos_yyx:
131 mocoeffs.insert(pos-2, mocoeffs.pop(pos))
132 if pos_yyz:
133 for pos in pos_yyz:
134 mocoeffs.insert(pos+2, mocoeffs.pop(pos))
136 return mocoeffs
139 def _mo_from_ccdata(self):
140 """Create [MO] section.
142 Sym= symmetry_label_1
143 Ene= mo_energy_1
144 Spin= (Alpha|Beta)
145 Occup= mo_occupation_number_1
146 ao_number_1 mo_coefficient_1
147 ...
148 ao_number_n mo_coefficient_n
149 ...
150 """
152 moenergies = self.ccdata.moenergies
153 mocoeffs = self.ccdata.mocoeffs
154 homos = self.ccdata.homos
155 mult = self.ccdata.mult
157 has_syms = False
158 lines = []
160 # Sym attribute is optional in [MO] section.
161 if hasattr(self.ccdata, 'mosyms'):
162 has_syms = True
163 syms = self.ccdata.mosyms
164 else:
165 syms = numpy.full_like(moenergies, 'A', dtype=str)
166 unres = len(moenergies) > 1
167 openshell = len(homos) > 1
169 spin = 'Alpha'
170 for i in range(len(moenergies)):
171 for j in range(len(moenergies[i])):
172 lines.append(' Sym= %s' % syms[i][j])
173 moenergy = utils.convertor(moenergies[i][j], 'eV', 'hartree')
174 lines.append(' Ene= {:10.4f}'.format(moenergy))
175 lines.append(' Spin= %s' % spin)
176 if unres and openshell:
177 if j <= homos[i]:
178 lines.append(' Occup= {:10.6f}'.format(1.0))
179 else:
180 lines.append(' Occup= {:10.6f}'.format(0.0))
181 elif not unres and openshell:
182 occ = numpy.sum(j <= homos)
183 if j <= homos[i]:
184 lines.append(' Occup= {:10.6f}'.format(occ))
185 else:
186 lines.append(' Occup= {:10.6f}'.format(0.0))
187 else:
188 if j <= homos[i]:
189 lines.append(' Occup= {:10.6f}'.format(2.0))
190 else:
191 lines.append(' Occup= {:10.6f}'.format(0.0))
192 # Rearrange mocoeffs according to Molden's lexicographical order.
193 mocoeffs[i][j] = self._rearrange_mocoeffs(mocoeffs[i][j])
194 for k, mocoeff in enumerate(mocoeffs[i][j]):
195 lines.append('{:4d} {:10.6f}'.format(k + 1, mocoeff))
197 spin = 'Beta'
199 return lines
201 def generate_repr(self):
202 """Generate the MOLDEN representation of the logfile data."""
204 molden_lines = ['[Molden Format]']
206 # Title of file.
207 if self.jobfilename is not None:
208 molden_lines.append('[Title]')
209 molden_lines.append(self._title(self.jobfilename))
211 # Coordinates for the Electron Density/Molecular orbitals.
212 # [Atoms] (Angs|AU)
213 unit = "Angs"
214 molden_lines.append('[Atoms] %s' % unit)
215 # Last set of coordinates for geometry optimization runs.
216 index = -1
217 molden_lines.extend(self._coords_from_ccdata(index))
219 # Either both [GTO] and [MO] should be present or none of them.
220 if hasattr(self.ccdata, 'gbasis') and hasattr(self.ccdata, 'mocoeffs')\
221 and hasattr(self.ccdata, 'moenergies'):
223 molden_lines.append('[GTO]')
224 molden_lines.extend(self._gto_from_ccdata())
226 molden_lines.append('[MO]')
227 molden_lines.extend(self._mo_from_ccdata())
229 # Omitting until issue #390 is resolved.
230 # https://github.com/cclib/cclib/issues/390
231 # if hasattr(self.ccdata, 'scfenergies'):
232 # if len(self.ccdata.scfenergies) > 1:
233 # molden_lines.append('[SCFCONV]')
234 # molden_lines.extend(self._scfconv_from_ccdata())
236 # molden_lines.append('')
238 return '\n'.join(molden_lines)
241class MoldenReformatter:
242 """Reformat Molden output files."""
244 def __init__(self, filestring):
245 self.filestring = filestring
247 def scinotation(self, num):
248 """Convert Molden style number formatting to scientific notation.
249 0.9910616900D+02 --> 9.910617e+01
250 """
251 num = num.replace('D', 'e')
252 return str('%.9e' % decimal.Decimal(num))
254 def reformat(self):
255 """Reformat Molden output file to:
256 - use scientific notation,
257 - split sp molecular orbitals to s and p, and
258 - replace multiple spaces with single."""
259 filelines = iter(self.filestring.split("\n"))
260 lines = []
262 for line in filelines:
263 line = line.replace('\n', '')
264 # Replace multiple spaces with single spaces.
265 line = ' '.join(line.split())
267 # Check for [Title] section.
268 if '[title]' in line.lower():
269 # skip the title
270 line = next(filelines)
271 line = next(filelines)
273 # Exclude SCFCONV section until issue #390 is resolved.
274 # https://github.com/cclib/cclib/issues/390
275 if '[scfconv]' in line.lower():
276 break
278 # Although Molden format specifies Sym in [MO] section,
279 # the Molden program does not print it.
280 if 'sym' in line.lower():
281 continue
283 # Convert D notation to scientific notation.
284 if 'D' in line:
285 vals = line.split()
286 vals = [self.scinotation(i) for i in vals]
287 lines.append(' '.join(vals))
289 # Convert sp to s and p orbitals.
290 elif 'sp' in line:
291 n_prim = int(line.split()[1])
292 new_s = ['s ' + str(n_prim) + ' 1.00']
293 new_p = ['p ' + str(n_prim) + ' 1.00']
294 while n_prim > 0:
295 n_prim -= 1
296 line = next(filelines).split()
297 new_s.append(self.scinotation(line[0]) + ' '
298 + self.scinotation(line[1]))
299 new_p.append(self.scinotation(line[0]) + ' '
300 + self.scinotation(line[2]))
301 lines.extend(new_s)
302 lines.extend(new_p)
303 else:
304 lines.append(line)
306 return '\n'.join(lines)