File: h2morse.py

package info (click to toggle)
python-ase 3.21.1-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 13,936 kB
  • sloc: python: 122,428; xml: 946; makefile: 111; javascript: 47
file content (213 lines) | stat: -rw-r--r-- 6,882 bytes parent folder | download
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from itertools import count
import numpy as np

from ase import Atoms
from ase.units import invcm, Ha
from ase.data import atomic_masses
from ase.calculators.calculator import all_changes
from ase.calculators.morse import MorsePotential
from ase.calculators.excitation_list import Excitation, ExcitationList

"""The H2 molecule represented by Morse-Potentials for
gound and first 3 excited singlet states B + C(doubly degenerate)"""

npa = np.array
# data from:
# https://webbook.nist.gov/cgi/cbook.cgi?ID=C1333740&Mask=1000#Diatomic
#         X        B       C       C
Re = npa([0.74144, 1.2928, 1.0327, 1.0327])  # eq. bond length
ome = npa([4401.21, 1358.09, 2443.77, 2443.77])  # vibrational frequency
# electronic transition energy
Etrans = npa([0, 91700.0, 100089.9, 100089.9]) * invcm

# dissociation energy
# GS: https://aip.scitation.org/doi/10.1063/1.3120443
De = np.ones(4) * 36118.069 * invcm
# B, C separated energy E(1s) - E(2p)
De[1:] += Ha / 2 - Ha / 8
De -= Etrans

# Morse parameter
m = atomic_masses[1] * 0.5  # reduced mass
# XXX find scaling factor
rho0 = Re * ome * invcm * np.sqrt(m / 2 / De) * 4401.21 / 284.55677429605862


def H2Morse(state=0):
    """Return H2 as a Morse-Potential with calculator attached."""
    atoms = Atoms('H2', positions=np.zeros((2, 3)))
    atoms[1].position[2] = Re[state]
    atoms.calc = H2MorseCalculator(state)
    atoms.get_potential_energy()
    return atoms


class H2MorseCalculator(MorsePotential):
    """H2 ground or excited state as Morse potential"""
    _count = count(0)

    def __init__(self, state):
        MorsePotential.__init__(self,
                                epsilon=De[state],
                                r0=Re[state], rho0=rho0[state])

    def calculate(self, atoms=None, properties=['energy'],
                  system_changes=all_changes):
        if atoms is not None:
            assert len(atoms) == 2
        MorsePotential.calculate(self, atoms, properties, system_changes)

        # determine 'wave functions' including
        # Berry phase (arbitrary sign) and
        # random orientation of wave functions perpendicular
        # to the molecular axis
        
        # molecular axis
        vr = atoms[1].position - atoms[0].position
        r = np.linalg.norm(vr)
        hr = vr / r
        # defined seed for tests
        seed = next(self._count)
        np.random.seed(seed)
        # perpendicular axes
        vrand = np.random.rand(3)
        hx = np.cross(hr, vrand)
        hx /= np.linalg.norm(hx)
        hy = np.cross(hr, hx)
        hy /= np.linalg.norm(hy)
        wfs = [1, hr, hx, hy]
        # Berry phase
        berry = (-1)**np.random.randint(0, 2, 4)
        self.wfs = [wf * b for wf, b in zip(wfs, berry)]

    @classmethod
    def read(cls, filename):
        ms = cls(3)
        with open(filename) as f:
            ms.wfs = [int(f.readline().split()[0])]
            for i in range(1, 4):
                ms.wfs.append(
                    np.array([float(x)
                              for x in f.readline().split()[:4]]))
        ms.filename = filename
        return ms
        
    def write(self, filename, option=None):
        """write calculated state to a file"""
        with open(filename, 'w') as f:
            f.write('{}\n'.format(self.wfs[0]))
            for wf in self.wfs[1:]:
                f.write('{0:g} {1:g} {2:g}\n'.format(*wf))

    def overlap(self, other):
        ov = np.zeros((4, 4))
        ov[0, 0] = self.wfs[0] * other.wfs[0]
        wfs = np.array(self.wfs[1:])
        owfs = np.array(other.wfs[1:])
        ov[1:, 1:] = np.dot(wfs, owfs.T)
        return ov


class H2MorseExcitedStatesCalculator():
    """First singlet excited states of H2 from Morse potentials"""
    def __init__(self, nstates=3):
        """
        Parameters
        ----------
        nstates: int
          Numer of states to calculate 0 < nstates < 4, default 3
        """
        assert nstates > 0 and nstates < 4
        self.nstates = nstates

    def calculate(self, atoms):
        """Calculate excitation spectrum

        Parameters
        ----------
        atoms: Ase atoms object
        """
        # central me value and rise, unit Bohr
        # from DOI: 10.1021/acs.jctc.9b00584
        mc = [0, 0.8, 0.7, 0.7]
        mr = [0, 1.0, 0.5, 0.5]

        cgs = atoms.calc
        r = atoms.get_distance(0, 1)
        E0 = cgs.get_potential_energy(atoms)
        
        exl = H2MorseExcitedStates()
        for i in range(1, self.nstates + 1):
            hvec = cgs.wfs[0] * cgs.wfs[i]
            energy = Ha * (0.5 - 1. / 8) - E0
            calc = H2MorseCalculator(i)
            calc.calculate(atoms)
            energy += calc.get_potential_energy()

            mur = hvec * (mc[i] + (r - Re[0]) * mr[i])
            muv = mur

            exl.append(H2Excitation(energy, i, mur, muv))
        return exl


class H2MorseExcitedStates(ExcitationList):
    """First singlet excited states of H2"""
    def __init__(self, nstates=3):
        """
        Parameters
        ----------
        nstates: int, 1 <= nstates <= 3
          Number of excited states to consider, default 3
        """
        self.nstates = nstates
        super().__init__()

    def overlap(self, ov_nn, other):
        return (ov_nn[1:len(self) + 1, 1:len(self) + 1] *
                ov_nn[0, 0])

    @classmethod
    def read(cls, filename, nstates=3):
        """Read myself from a file"""
        exl = cls(nstates)
        with open(filename, 'r') as f:
            exl.filename = filename
            n = int(f.readline().split()[0])
            for i in range(min(n, exl.nstates)):
                exl.append(H2Excitation.fromstring(f.readline()))
        return exl

    def write(self, fname):
        with open(fname, 'w') as f:
            f.write('{0}\n'.format(len(self)))
            for ex in self:
                f.write(ex.outstring())


class H2Excitation(Excitation):
    def __eq__(self, other):
        """Considered to be equal when their indices are equal."""
        return self.index == other.index

    def __hash__(self):
        """Hash similar to __eq__"""
        if not hasattr(self, 'hash'):
            self.hash = hash(self.index)
        return self.hash


class H2MorseExcitedStatesAndCalculator(
        H2MorseExcitedStatesCalculator, H2MorseExcitedStates):
    """Traditional joined object for backward compatibility only"""
    def __init__(self, calculator, nstates=3):
        if isinstance(calculator, str):
            exlist = H2MorseExcitedStates.read(calculator, nstates)
        else:
            atoms = calculator.atoms
            atoms.calc = calculator
            excalc = H2MorseExcitedStatesCalculator(nstates)
            exlist = excalc.calculate(atoms)
        H2MorseExcitedStates.__init__(self, nstates=nstates)
        for ex in exlist:
            self.append(ex)