Skip to content

Commit fb48be9

Browse files
committed
First draft
1 parent 5d3f29e commit fb48be9

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed

examples/pet-mad-dos/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
PET-MAD DOS example
2+
=======================
3+
4+
An example of how to use PET-MAD-DOS to compute the density of states (DOS)
5+
of systems.

examples/pet-mad-dos/dos_model.pt

26.8 MB
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
channels:
2+
- conda-forge
3+
dependencies:
4+
- python=3.12
5+
- pip
6+
- pip:
7+
- torch
8+
- numpy
9+
- ase
10+
- metatrain
11+
- matplotlib
12+
- pet-mad>=1.2.0,<2.0
13+
- scipy
14+
- metatensor-torch
15+
- metatomic-torch
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""
2+
PET-MAD-DOS: A universal model for the Density of States
3+
=========================================================
4+
5+
:Authors: Pol Febrer `@pfebrer <https://github.com/pfebrer>`_,
6+
How Wei Bin `@HowWeiBin <https://github.com/HowWeiBin>`_,
7+
8+
This example demonstrates how to use a universal model for the Density of States (DOS)
9+
to compute the electronic heat capacity of a material using only universal ML models.
10+
11+
In the example, we will:
12+
13+
- Use the universal PET-MAD force field to run MD for our material of interest.
14+
- Use PET-MAD-DOS to compute the DOS for snapshots of the MD trajectory.
15+
- Compute the electronic heat capacity from the predicted DOS.
16+
"""
17+
18+
# %%
19+
#
20+
# Load PET-MAD
21+
# ------------
22+
#
23+
24+
import torch
25+
import numpy as np
26+
27+
from pet_mad.calculator import PETMADCalculator
28+
29+
petmad = PETMADCalculator(version="latest", device="cpu")
30+
31+
# %%
32+
#
33+
# Create a system and run some MD steps
34+
# -------------------------------------
35+
#
36+
37+
from ase.build import bulk
38+
import ase.md
39+
40+
atoms = bulk("Au", "diamond", a=5.43, cubic=True)
41+
atoms.calc = petmad
42+
43+
integrator = ase.md.VelocityVerlet(atoms, timestep=0.5 * ase.units.fs)
44+
45+
n_steps = 100
46+
47+
traj_atoms = []
48+
49+
for step in range(n_steps):
50+
# run a single simulation step
51+
integrator.run(1)
52+
53+
traj_atoms.append(atoms.copy())
54+
55+
# %%
56+
#
57+
# Load the DOS model
58+
# ------------------
59+
#
60+
61+
from metatrain.utils.neighbor_lists import (
62+
get_requested_neighbor_lists,
63+
get_system_with_neighbor_lists,
64+
)
65+
66+
from metatensor.torch.atomistic import load_atomistic_model, systems_to_torch
67+
68+
model = load_atomistic_model("dos_model.pt")
69+
70+
# %%
71+
#
72+
# Prepare trajectory for running the DOS model
73+
# --------------------------------------------
74+
#
75+
76+
# Convert from ase atoms to System objects and add the neighbor lists.
77+
eval_systems = systems_to_torch(traj_atoms)
78+
79+
eval_systems = [
80+
get_system_with_neighbor_lists(system, get_requested_neighbor_lists(model)).to(dtype=torch.float32)
81+
for system in eval_systems
82+
]
83+
84+
# %%
85+
#
86+
# Run the DOS model
87+
# -----------------
88+
#
89+
90+
from metatomic.torch import ModelEvaluationOptions
91+
92+
# Define the evaluation options for the model (we only need the DOS output)
93+
DOS_output = model.capabilities().outputs["mtt::dos"]
94+
DOS_output.per_atom = False
95+
options = ModelEvaluationOptions(
96+
outputs={"mtt::dos": DOS_output},
97+
)
98+
99+
# Run the model to compute the DOS on the snapshots of the trajectory.
100+
# We only evaluate every 4th system to speed up the computation
101+
out = model(eval_systems[::4], options=options, check_consistency=False)
102+
103+
# %%
104+
#
105+
# Plot ensemble DOS
106+
# -----------------
107+
#
108+
109+
import matplotlib.pyplot as plt
110+
111+
# Get the DOS values from the output of the model (this will be shape [n_systems, n_E])
112+
all_DOS = out["mtt::dos"].block(0).values
113+
# Get the energy axis (the bounds are always the same for PET-MAD-DOS)
114+
E = torch.linspace(-148.1456 - 1.5 - 10, 79.1528 + 1.5, all_DOS.shape[1])
115+
116+
# Compute the ensemble DOS by averaging over all systems
117+
# and ensure that there are no negative values
118+
ensemble_DOS = torch.mean(all_DOS, dim = 0)
119+
ensemble_DOS[ensemble_DOS < 0] = 0
120+
121+
# Plot the ensemble DOS
122+
plt.plot(E, ensemble_DOS)
123+
plt.xlabel('Energy (eV)')
124+
plt.ylabel('Density of States')
125+
plt.title('Ensemble Density of States from PET-MAD')
126+
plt.show()
127+
128+
# %%
129+
#
130+
# Compute electronic heat capacity
131+
# --------------------------------
132+
#
133+
# First some helper functions to:
134+
#
135+
# - Compute the Fermi-Dirac distribution
136+
# - Compute the Fermi level for a given density of states and number of electrons
137+
# - Compute the electronic heat capacity given a DOS.
138+
#
139+
140+
from collections.abc import Sequence
141+
142+
from ase.units import kB
143+
from scipy.interpolate import interp1d
144+
145+
def fd_distribution(E: Sequence, mu: float, T: float) -> np.array:
146+
r"""Compute the Fermi-Dirac distribution.
147+
148+
.. math::
149+
f(E) = \frac{1}{1 + e^{(E - \mu) / (k_B T)}}
150+
151+
Parameters
152+
----------
153+
E:
154+
Values of the energy axis for which to compute the Fermi-Dirac distribution (eV)
155+
mu:
156+
Fermi level / chemical potential (eV)
157+
T:
158+
Temperature (K)
159+
"""
160+
161+
y = (E-mu)/ (kB * T)
162+
# np.exp(y) can lead to overflow if y is too large, so we use a trick to avoid it
163+
# We compute exp(-|y|) and then treat positive and negative values separately
164+
ey = np.exp(-np.abs(y))
165+
166+
negs = (y<0)
167+
pos = (y>=0)
168+
y[negs] = 1 / (1+ey[negs])
169+
y[pos] = ey[pos] / (1+ey[pos])
170+
171+
return y
172+
173+
def get_fermi(dos: Sequence, E: Sequence, n_elec: float, T: float = 0.) -> float:
174+
"""Compute the Fermi level for a given density of states and number of electrons.
175+
176+
Parameters
177+
----------
178+
dos:
179+
Density of states.
180+
E:
181+
Energy axis corresponding to the DOS (eV)
182+
n_elec:
183+
Total number of electrons in the system.
184+
T:
185+
Temperature (K).
186+
"""
187+
# First compute the Fermi level at T=0 by finding the energy where the
188+
# cumulative DOS equals the number of electrons
189+
cumulative_dos = torch.cumulative_trapezoid(dos, E)
190+
Efdos = interp1d(cumulative_dos, E[1:])
191+
Ef_0 = Efdos(n_elec)
192+
193+
if T == 0:
194+
return Ef_0
195+
196+
# For finite temperatures, test a range of Fermi levels around Ef_0
197+
# and find the one that gives the correct number of electrons.
198+
integrated_doses = []
199+
trial_fermis = np.linspace(Ef_0 - 0.5, Ef_0 + 0.5, 100)
200+
for trial_fermi in trial_fermis:
201+
fd = fd_distribution(E, trial_fermi, T)
202+
integrated_dos = torch.trapezoid(dos * fd, E)
203+
integrated_doses.append(integrated_dos)
204+
205+
n_elecs_Ef = interp1d(integrated_doses, trial_fermis)
206+
Ef = n_elecs_Ef(n_elec)
207+
return Ef
208+
209+
def compute_heat_capacity(dos: Sequence, E: Sequence, T: float, n_elec: float, dT: float = 1.):
210+
"""Compute the electronic heat capacity.
211+
212+
It uses the finite difference method to compute the heat capacity
213+
from the change in internal energy with temperature.
214+
215+
Parameters
216+
----------
217+
dos:
218+
Density of states.
219+
E:
220+
Energy axis corresponding to the DOS (eV)
221+
n_elec:
222+
Total number of electrons in the system.
223+
T:
224+
Temperature (K).
225+
dT:
226+
Temperature step for finite difference (K).
227+
"""
228+
# The calculations are more numerically stable if we shift the energy so that
229+
# near the fermi level energies are close to zero. We compute the Fermi level
230+
# at T to shift the energy axis.
231+
Ef_T = get_fermi(dos, E, n_elec=n_elec, T=T)
232+
E = E - Ef_T
233+
234+
# Compute the internal energy at T-dT and T+dT
235+
U = []
236+
for T_side in (T - dT, T + dT):
237+
238+
Ef_side = get_fermi(dos, E, n_elec=n_elec, T=T_side)
239+
240+
U_side = torch.trapezoid(E * dos * fd_distribution(E, Ef_side, T_side), E)
241+
U.append(U_side)
242+
243+
# Compute the heat capacity as the gradient of the internal energy
244+
# with respect to temperature
245+
heat_capacity = (U[1] - U[0]) / (2 * dT) / kB
246+
247+
return heat_capacity
248+
249+
# %%
250+
#
251+
# Compute the heat capacity at different temperatures.
252+
#
253+
# TODO: Here we need to compute the ensemble DOS at each temperature by doing MD
254+
# at each temperature.
255+
#
256+
257+
# Total number of electrons in the system
258+
total_elec = 19 * len(traj_atoms[0])
259+
260+
# Compute the heat capacity at different temperatures
261+
heat_capacities = []
262+
Ts = np.linspace(200, 1000, 10)
263+
for T in Ts:
264+
heat_capacity = compute_heat_capacity(ensemble_DOS, E, T, n_elec=total_elec, dT=1)
265+
266+
heat_capacities.append(heat_capacity.item() / len(traj_atoms[0]))
267+
268+
# Plot them
269+
plt.plot(Ts, heat_capacities)
270+
plt.xlabel('Temperature (K)')
271+
plt.ylabel('Heat Capacity [eV/(K*atom)] (divided by kB)')
272+
plt.title('Electronic Heat Capacity from PET-MAD')
273+
plt.show()
274+
275+

0 commit comments

Comments
 (0)