Coverage for /builds/ase/ase/ase/calculators/h2morse.py: 100.00%
123 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3from itertools import count
5import numpy as np
7from ase import Atoms
8from ase.calculators.calculator import all_changes
9from ase.calculators.excitation_list import Excitation, ExcitationList
10from ase.calculators.morse import MorsePotential
11from ase.data import atomic_masses
12from ase.units import Ha, invcm
14"""The H2 molecule represented by Morse-Potentials for
15gound and first 3 excited singlet states B + C(doubly degenerate)"""
17npa = np.array
18# data from:
19# https://webbook.nist.gov/cgi/cbook.cgi?ID=C1333740&Mask=1000#Diatomic
20# X B C C
21Re = npa([0.74144, 1.2928, 1.0327, 1.0327]) # eq. bond length
22ome = npa([4401.21, 1358.09, 2443.77, 2443.77]) # vibrational frequency
23# electronic transition energy
24Etrans = npa([0, 91700.0, 100089.9, 100089.9]) * invcm
26# dissociation energy
27# GS: https://aip.scitation.org/doi/10.1063/1.3120443
28De = np.ones(4) * 36118.069 * invcm
29# B, C separated energy E(1s) - E(2p)
30De[1:] += Ha / 2 - Ha / 8
31De -= Etrans
33# Morse parameter
34m = atomic_masses[1] * 0.5 # reduced mass
35# XXX find scaling factor
36rho0 = Re * ome * invcm * np.sqrt(m / 2 / De) * 4401.21 / 284.55677429605862
39def H2Morse(state=0):
40 """Return H2 as a Morse-Potential with calculator attached."""
41 atoms = Atoms('H2', positions=np.zeros((2, 3)))
42 atoms[1].position[2] = Re[state]
43 atoms.calc = H2MorseCalculator(state=state)
44 atoms.get_potential_energy()
45 return atoms
48class H2MorseCalculator(MorsePotential):
49 """H2 ground or excited state as Morse potential"""
50 _count = count(0)
52 def __init__(self, restart=None, state=0, rng=np.random):
53 self.rng = rng
54 MorsePotential.__init__(self,
55 restart=restart,
56 epsilon=De[state],
57 r0=Re[state], rho0=rho0[state])
59 def calculate(self, atoms=None, properties=['energy'],
60 system_changes=all_changes):
61 if atoms is not None:
62 assert len(atoms) == 2
63 MorsePotential.calculate(self, atoms, properties, system_changes)
65 # determine 'wave functions' including
66 # Berry phase (arbitrary sign) and
67 # random orientation of wave functions perpendicular
68 # to the molecular axis
70 # molecular axis
71 vr = atoms[1].position - atoms[0].position
72 r = np.linalg.norm(vr)
73 hr = vr / r
74 # perpendicular axes
75 vrand = self.rng.random(3)
76 hx = np.cross(hr, vrand)
77 hx /= np.linalg.norm(hx)
78 hy = np.cross(hr, hx)
79 hy /= np.linalg.norm(hy)
80 wfs = [1, hr, hx, hy]
81 # Berry phase
82 berry = (-1)**self.rng.randint(0, 2, 4)
83 self.wfs = [wf * b for wf, b in zip(wfs, berry)]
85 def read(self, filename):
86 ms = self
87 with open(filename) as fd:
88 ms.wfs = [int(fd.readline().split()[0])]
89 for _ in range(1, 4):
90 ms.wfs.append(
91 np.array([float(x)
92 for x in fd.readline().split()[:4]]))
93 ms.filename = filename
94 return ms
96 def write(self, filename, option=None):
97 """write calculated state to a file"""
98 with open(filename, 'w') as fd:
99 fd.write(f'{self.wfs[0]}\n')
100 for wf in self.wfs[1:]:
101 fd.write('{:g} {:g} {:g}\n'.format(*wf))
103 def overlap(self, other):
104 ov = np.zeros((4, 4))
105 ov[0, 0] = self.wfs[0] * other.wfs[0]
106 wfs = np.array(self.wfs[1:])
107 owfs = np.array(other.wfs[1:])
108 ov[1:, 1:] = np.dot(wfs, owfs.T)
109 return ov
112class H2MorseExcitedStatesCalculator():
113 """First singlet excited states of H2 from Morse potentials"""
115 def __init__(self, nstates=3):
116 """
117 Parameters
118 ----------
119 nstates: int
120 Numer of states to calculate 0 < nstates < 4, default 3
121 """
122 assert nstates > 0 and nstates < 4
123 self.nstates = nstates
125 def calculate(self, atoms):
126 """Calculate excitation spectrum
128 Parameters
129 ----------
130 atoms: Ase atoms object
131 """
132 # central me value and rise, unit Bohr
133 # from DOI: 10.1021/acs.jctc.9b00584
134 mc = [0, 0.8, 0.7, 0.7]
135 mr = [0, 1.0, 0.5, 0.5]
137 cgs = atoms.calc
138 r = atoms.get_distance(0, 1)
139 E0 = cgs.get_potential_energy(atoms)
141 exl = H2MorseExcitedStates()
142 for i in range(1, self.nstates + 1):
143 hvec = cgs.wfs[0] * cgs.wfs[i]
144 energy = Ha * (0.5 - 1. / 8) - E0
145 calc = H2MorseCalculator(state=i)
146 calc.calculate(atoms)
147 energy += calc.get_potential_energy()
149 mur = hvec * (mc[i] + (r - Re[0]) * mr[i])
150 muv = mur
152 exl.append(H2Excitation(energy, i, mur, muv))
153 return exl
156class H2MorseExcitedStates(ExcitationList):
157 """First singlet excited states of H2"""
159 def __init__(self, nstates=3):
160 """
161 Parameters
162 ----------
163 nstates: int, 1 <= nstates <= 3
164 Number of excited states to consider, default 3
165 """
166 self.nstates = nstates
167 super().__init__()
169 def overlap(self, ov_nn, other):
170 return (ov_nn[1:len(self) + 1, 1:len(self) + 1] *
171 ov_nn[0, 0])
173 @classmethod
174 def read(cls, filename, nstates=3):
175 """Read myself from a file"""
176 exl = cls(nstates)
177 with open(filename) as fd:
178 exl.filename = filename
179 n = int(fd.readline().split()[0])
180 for _ in range(min(n, exl.nstates)):
181 exl.append(H2Excitation.fromstring(fd.readline()))
182 return exl
184 def write(self, fname):
185 with open(fname, 'w') as fd:
186 fd.write(f'{len(self)}\n')
187 for ex in self:
188 fd.write(ex.outstring())
191class H2Excitation(Excitation):
192 def __eq__(self, other):
193 """Considered to be equal when their indices are equal."""
194 return self.index == other.index
196 def __hash__(self):
197 """Hash similar to __eq__"""
198 if not hasattr(self, 'hash'):
199 self.hash = hash(self.index)
200 return self.hash
203class H2MorseExcitedStatesAndCalculator(
204 H2MorseExcitedStatesCalculator, H2MorseExcitedStates):
205 """Traditional joined object for backward compatibility only"""
207 def __init__(self, calculator, nstates=3):
208 if isinstance(calculator, str):
209 exlist = H2MorseExcitedStates.read(calculator, nstates)
210 else:
211 atoms = calculator.atoms
212 atoms.calc = calculator
213 excalc = H2MorseExcitedStatesCalculator(nstates)
214 exlist = excalc.calculate(atoms)
215 H2MorseExcitedStates.__init__(self, nstates=nstates)
216 for ex in exlist:
217 self.append(ex)