Coverage for examples/thermodynamics.py: 97%
65 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1""" Investigation of thermodynamic properties
3This example show how thermodynamic data can be extracted
4using the `extract_scalars` function from the `gamdpy` package.
6The script runs a NVT simulation of a Lennard-Jones crystal.
7The potential energy, virial, and kinetic energy are extracted
8from the simulation output and the mean values are printed.
9Derived quantities such as kinetic temperature and pressure are computed.
10The energy trajectory is plotted and the error estimate of potential energy
11is estimated using the blocking method.
13"""
15import numpy as np
16import matplotlib.pyplot as plt
17from scipy import stats
19import gamdpy as gp
22###############################
23# Setup and run simulation #
24###############################
26# Setup configuration: FCC Lattice
27configuration = gp.Configuration(D=3)
28configuration.make_lattice(gp.unit_cells.FCC, cells=[8, 8, 8], rho=0.973)
29configuration['m'] = 1.0
30configuration.randomize_velocities(temperature=0.7)
32# Setup pair potential: Single component 12-6 Lennard-Jones
33pair_func = gp.apply_shifted_potential_cutoff(gp.LJ_12_6_sigma_epsilon)
34sig, eps, cut = 1.0, 1.0, 2.5
35pair_pot = gp.PairPotential(pair_func, params=[sig, eps, cut], max_num_nbs=1000)
37# Setup integrator: NVT
38integrator = gp.integrators.NVT(temperature=0.7, tau=0.2, dt=0.005)
40# Setup runtime actions, i.e. actions performed during simulation of timeblocks
41runtime_actions = [gp.TrajectorySaver(),
42 gp.ScalarSaver(4),
43 gp.MomentumReset(100)]
45# Setup Simulation.
46sim = gp.Simulation(configuration, pair_pot, integrator, runtime_actions,
47 num_timeblocks=64, steps_per_timeblock=2048,
48 storage='memory')
50# Run simulation
51sim.run()
53# Basic information of NVT simulation
54N = sim.configuration.N # Number of particles
55T = sim.integrator.temperature # Temperature
56V = configuration.get_volume()
57rho = N / V # Density
60########################
61# Extracting scalars #
62########################
64# Extract potential energy (U), virial (W), and kinetic energy (K)
65# We use first_block=1 to skip the initial "equilibration" block.
66U, W, K = gp.extract_scalars(sim.output, ['U', 'W', 'K'], first_block=1)
68# Print mean values
69print(f"Mean potential energy per particle: {np.mean(U) / N}")
70print(f"Mean virial per particle: {np.mean(W) / N}")
71print(f"Mean kinetic energy per particle: {np.mean(K) / N}")
74########################
75# Derived quantities #
76########################
78# Time
79dt = sim.integrator.dt
80time = np.arange(len(U)) * dt * sim.output['scalar_saver'].attrs["steps_between_output"]
81print(f"Total time of analysed trajectory: {time[-1]}")
83# Compute kinetic temperature
84D = sim.configuration.D # dimension of space
85dof = D * N - D # degrees of freedom
86T_kin = 2 * K / dof
87print(f"Mean kinetic temperature: {np.mean(T_kin)}")
89# Compute instantaneous pressure
90P = rho * T_kin + W / V
91print(f"Mean pressure: {np.mean(P)}")
94################################
95# Plotting energy trajectory #
96################################
98# Plot potential energy per particle
99plt.figure()
100plt.plot(time, U / N, label='Potential energy')
101plt.axhline(np.mean(U) / N, color='k', linestyle='--', label='Mean')
102plt.xlabel('Time')
103plt.ylabel('Potential energy')
104plt.legend()
105if __name__ == "__main__":
106 plt.show()
109########################################
110# Statistical analysis using blocking #
111########################################
113def error_estimate_by_blocking(data, blocks, confidence=0.95):
114 """ Estimate error on mean using block averaging. """
115 block_length = len(data) // blocks
116 data = data[:blocks * block_length].reshape(blocks, block_length)
117 block_means = np.mean(data, axis=1) # Mean of each block
118 sem = np.std(block_means) / np.sqrt(blocks) # Standard error of the mean
119 t_value = stats.t.ppf((1 + confidence) / 2, blocks - 1) # t-value for confidence interval
120 return t_value * sem # Error estimate
123# Find optimal block size
124test_num_blocks = list(set(np.logspace(0, np.log10(len(U) / 4), num=32, dtype=int)))
125test_num_blocks.sort()
126test_num_blocks = test_num_blocks[2:]
127errors = [error_estimate_by_blocking(U/N, blocks) for blocks in test_num_blocks]
129# Good choice for number of blocks (see figure below)
130# If num_blocks is too small, there is large uncertainty on the error estimate.
131# If num_blocks is too large, blocks are not statistical independent.
132num_blocks = 64
133error = error_estimate_by_blocking(U/N, num_blocks)
135plt.figure()
136plt.title('Error estimate of potential energy per particle')
137plt.plot(test_num_blocks, errors, 'o')
138plt.plot(num_blocks, error, 'ro', label='Estimated error')
139plt.xscale('log')
140plt.xlim(1, None)
141plt.ylim(0, None)
142plt.xlabel('Number of blocks')
143plt.ylabel('Estimated error (95% confidence interval)')
144if __name__ == "__main__":
145 plt.show()
147print(f'Potential energy per particle {np.mean(U) / N:.4f} ± {error:.4f} (95% confidence interval)')