Remark: The equilibration and sampling times used in this tutorial would be not sufficient for scientific purposes, but they are long enough to get at least a qualitative insight of the behaviour of ferrofluids. They have been shortened so we achieve reasonable computation times for the purpose of a tutorial.
In this part we want to calculate estimators for the initial susceptibility, i.e. the susceptibility at zero external magnetic field. One could carry out several simulations with different external magnetic field strengths and get the initial susceptibility by fitting a line to the results. We want to go a more elegant way by using fluctuation formulas known from statistical mechanics. In three dimensions the initial susceptibility $\chi_{init}$ can be calculated with zero field simulations through
\begin{equation} \chi_\mathrm{init} = \frac{V \cdot \mu_0}{3 \cdot k_\mathrm{B} T} \left( \langle \boldsymbol{M}^2 \rangle - \langle \boldsymbol{M} \rangle^2 \right) = \frac{\mu_0}{3 \cdot k_\mathrm{B} T \cdot V} \left( \langle \boldsymbol{\mu}^2 \rangle - \langle \boldsymbol{\mu} \rangle^2 \right) \end{equation}where $\boldsymbol{M}$ is the magnetization vector and $\boldsymbol{\mu}$ is the total magnetic dipole moment of the system. In direction $i$ it reads
\begin{equation} M_i = \frac{1}{V} \Bigg\langle \sum_{j=1}^N \tilde{\mu}_j^i \Bigg\rangle \end{equation}where $\tilde{\mu}_j^i$ is the $j$ th dipole moment in direction $i$.
We want to derive the fluctuation formula. We start with the definition of the magnetic susceptibility. In general this reads
\begin{equation} \chi \equiv \frac{\partial}{\partial H} \langle M_{\boldsymbol{H}} \rangle \end{equation}with $\langle M_{\boldsymbol{H}} \rangle$ the ensemble averaged magnetization in direction of a homogeneous external magnetic field $\boldsymbol{H}$.
In thermal equilibrium the ensemble average of the magnetization reads
\begin{equation} \langle M_{\boldsymbol{H}} \rangle = \frac{1}{V Z_\mathrm{c}} \left \lbrack \sum_{\alpha} \mu_{\boldsymbol{H},\alpha} e^{ -\beta E_{\alpha}(H=0) + \beta\mu_0\mu_{\boldsymbol{H},\alpha}H }\right \rbrack \end{equation}with $Z_\mathrm{c}$ the canonical partition function, $E_{\alpha}(H=0)$ the energy without an external magnetic field $\boldsymbol{H}$, $\beta$ the inverse thermal energy $\frac{1}{k_\mathrm{B}T}$, $\mu_{\boldsymbol{H},\alpha}$ the total magnetic dipole moment of the system in direction of the external magnetic field $\boldsymbol{H}$ in microstate $\alpha$ and $V$ the system volume.
Now we insert the magnetization $\langle M_{\boldsymbol{H}} \rangle$ in the definition of the magnetic susceptibility $\chi$ and let the derivative operate on the ensemble average. We get the fluctuation formula
\begin{equation} \chi = \frac{\beta\mu_0}{V} \left \lbrack \frac{1}{Z_\mathrm{c}}\sum_{\alpha} \mu_{\alpha}^2~ e^{ -\beta E_{\alpha}(H=0) + \beta\mu_0\mu_{\boldsymbol{H},\alpha}H } - \frac{1}{Z_\mathrm{c}}\sum_{\alpha} \mu_{\alpha}~ e^{ -\beta E_{\alpha}(H=0) + \beta\mu_0\mu_{\boldsymbol{H},\alpha}H }~~ \frac{1}{Z_\mathrm{c}}\sum_{\alpha'}\mu_{\alpha'}~ e^{ -\beta E_{\alpha'}(H=0) + \beta\mu_0\mu_{\boldsymbol{H},\alpha}H }\right \rbrack = \frac{\beta\mu_0}{V} \left \lbrack \langle \mu_{\boldsymbol{H}}^2 \rangle - \langle \mu_{\boldsymbol{H}} \rangle^2 \right \rbrack = \frac{\beta\mu_0}{V} \left(\Delta \mu_{\boldsymbol{H}}\right)^2 \end{equation}At zero external magnetic field ($H = 0$) there is no distinct direction for the system, so we can take the fluctuations $\Delta \mu$ in all directions and divide it by the dimension. Thus we can use more data points of our simulation for the average and get a more precise estimator for the susceptibility. Thus finally the fluctuation formula for the initial susceptibility in three dimensions reads
\begin{equation} \chi_\mathrm{init} = \frac{\beta\mu_0}{3V} \left \lbrack \langle \boldsymbol{\mu}^2 \rangle - \langle \boldsymbol{\mu} \rangle^2 \right \rbrack = \frac{V\beta\mu_0}{3} \left \lbrack \langle \boldsymbol{M}^2 \rangle - \langle \boldsymbol{M} \rangle^2 \right \rbrack \end{equation}where $\boldsymbol{\mu}$ and $\boldsymbol{M}$ are defined above.
In this part we want to consider a three dimensional ferrofluid system and compare our result for the initial susceptibility $\chi_\mathrm{init}$ with them of Ref. [1].
First we import all necessary packages and check for the required ESPResSo features
import espressomd
import espressomd.magnetostatics
import espressomd.zn
espressomd.assert_features(['DIPOLES', 'DP3M', 'LENNARD_JONES'])
%matplotlib inline
import matplotlib.pyplot as plt
plt.rcParams.update({'font.size': 18})
import numpy as np
import tqdm
Now we set up all necessary simulation parameters
lj_sigma = 1
lj_epsilon = 1
lj_cut = 2**(1. / 6.) * lj_sigma
# magnetic field constant
mu_0 = 1.
# Particles
N = 1000
# Volume fraction
# phi = rho * 4. / 3. * np.pi * ( lj_sigma / 2 )**3.
phi = 0.0262
# Dipolar interaction parameter lambda = mu_0 m^2 /(4 pi sigma^3 kT)
dip_lambda = 3.
# Temperature
kT = 1.0
# Friction coefficient
gamma = 1.0
# Time step
dt = 0.02
# box size 3d
box_size = (N * np.pi * 4. / 3. * (lj_sigma / 2.)**3. / phi)**(1. / 3.)
Next we set up the system. As in part I, the orientation of the dipole moments is set directly on the particles, whereas the magnitude of the moments is taken into account when determining the prefactor of the dipolar P3M (for more details see part I).
Hint: It should be noted that we seed both the Langevin thermostat and the random number generator of numpy. Latter means that the initial configuration of our system is the same every time this script is executed. As the time evolution of the system depends not solely on the Langevin thermostat but also on the numeric accuracy and DP3M (the tuned parameters are slightly different every time) it is only partly predefined. You can change the seeds to simulate with a different initial configuration and a guaranteed different time evolution.
system = espressomd.System(box_l=(box_size, box_size, box_size))
system.time_step = dt
# Lennard-Jones interaction
system.non_bonded_inter[0, 0].lennard_jones.set_params(epsilon=lj_epsilon, sigma=lj_sigma, cutoff=lj_cut, shift="auto")
# Random dipole moments
np.random.seed(seed=1)
dip_phi = 2 * np.pi * np.random.random((N, 1))
dip_cos_theta = 2 * np.random.random((N, 1)) - 1
dip_sin_theta = np.sin(np.arccos(dip_cos_theta))
dip = np.hstack((
dip_sin_theta * np.sin(dip_phi),
dip_sin_theta * np.cos(dip_phi),
dip_cos_theta))
# Random positions in system volume
pos = box_size * np.random.random((N, 3))
# Add particles
particles = system.part.add(pos=pos, rotation=N * [(True, True, True)], dip=dip)
# Remove overlap between particles by means of the steepest descent method
mass = 1.0
f_max = 0.01 * lj_sigma * mass / system.time_step**2
system.integrator.set_steepest_descent(
f_max=f_max,
gamma=0.1,
max_displacement=0.05)
system.integrator.run(5000)
assert np.all(np.abs(system.part.all().f) < f_max), "Overlap removal did not converge!"
# Switch to velocity Verlet integrator
system.integrator.set_vv()
system.thermostat.set_langevin(kT=kT, gamma=gamma, seed=1)
# tune verlet list skin
system.cell_system.skin = 0.8
# Setup dipolar P3M
accuracy = 5E-4
system.magnetostatics.solver = espressomd.magnetostatics.DipolarP3M(accuracy=accuracy, prefactor=dip_lambda * lj_sigma**3 * kT)
DipolarP3M tune parameters: Accuracy goal = 5.00000e-04 prefactor = 3.00000e+00 System: box_l = 2.71372e+01 # magnetic part = 1000 Sum[mu_i^2] = 1.00000e+03 mesh cao r_cut_iL alpha_L err rs_err ks_err time [ms] 8 3 4.24571e-01 5.16252e+00 4.967e-04 3.535e-04 3.489e-04 12.54 8 2 4.70520e-01 4.45931e+00 5.478e-04 3.535e-04 4.185e-04 accuracy not achieved 8 4 3.98839e-01 5.63003e+00 4.988e-04 3.535e-04 3.519e-04 11.55 8 5 3.85974e-01 5.88831e+00 4.938e-04 3.535e-04 3.448e-04 10.08 8 6 3.79541e-01 6.02431e+00 4.946e-04 3.535e-04 3.459e-04 10.05 8 7 3.76784e-01 6.08409e+00 4.966e-04 3.535e-04 3.487e-04 10.45 10 3 3.79541e-01 6.02431e+00 5.265e-04 3.535e-04 3.901e-04 accuracy not achieved 10 4 3.58043e-01 6.51615e+00 4.990e-04 3.535e-04 3.522e-04 8.79 10 5 3.44700e-01 6.85394e+00 4.954e-04 3.535e-04 3.470e-04 7.71 10 6 3.36546e-01 7.07431e+00 4.963e-04 3.535e-04 3.482e-04 7.80 10 7 3.32098e-01 7.19931e+00 4.975e-04 3.535e-04 3.500e-04 8.42 12 3 3.44700e-01 6.85394e+00 5.600e-04 3.535e-04 4.343e-04 accuracy not achieved 12 4 3.26523e-01 7.36112e+00 4.949e-04 3.535e-04 3.463e-04 7.21 12 5 3.12384e-01 7.79881e+00 4.989e-04 3.535e-04 3.520e-04 6.72 12 6 3.04306e-01 8.06821e+00 4.980e-04 3.535e-04 3.508e-04 6.65 12 7 2.99593e-01 8.23248e+00 4.944e-04 3.535e-04 3.456e-04 6.77 14 3 3.04306e-01 8.06821e+00 7.639e-04 3.535e-04 6.771e-04 accuracy not achieved 14 4 3.00739e-01 8.19198e+00 4.958e-04 3.536e-04 3.476e-04 5.76 14 5 2.87069e-01 8.69676e+00 4.949e-04 3.536e-04 3.462e-04 5.31 14 6 2.78749e-01 9.02977e+00 4.981e-04 3.535e-04 3.509e-04 5.48 14 7 2.73994e-01 9.22962e+00 4.934e-04 3.536e-04 3.441e-04 5.88 16 3 2.87069e-01 8.69676e+00 7.359e-04 3.536e-04 6.454e-04 accuracy not achieved 16 4 2.79781e-01 8.98733e+00 4.957e-04 3.535e-04 3.474e-04 5.09 16 5 2.65764e-01 9.59362e+00 4.961e-04 3.535e-04 3.480e-04 4.89 16 6 2.57914e-01 9.96370e+00 4.957e-04 3.535e-04 3.475e-04 4.94 16 7 2.52868e-01 1.02145e+01 4.955e-04 3.535e-04 3.472e-04 5.36 18 3 2.65764e-01 9.59362e+00 8.406e-04 3.535e-04 7.626e-04 accuracy not achieved 18 4 2.62130e-01 9.76197e+00 4.973e-04 3.535e-04 3.498e-04 4.76 18 5 2.48115e-01 1.04605e+01 4.951e-04 3.535e-04 3.466e-04 4.63 18 6 2.40329e-01 1.08860e+01 4.946e-04 3.535e-04 3.459e-04 4.72 18 7 2.35138e-01 1.11862e+01 4.983e-04 3.535e-04 3.511e-04 5.22 20 3 2.48115e-01 1.04605e+01 9.470e-04 3.535e-04 8.785e-04 accuracy not achieved 20 4 2.47146e-01 1.05120e+01 4.991e-04 3.535e-04 3.523e-04 4.44 20 5 2.33577e-01 1.12794e+01 4.885e-04 3.535e-04 3.371e-04 4.35 20 6 2.25824e-01 1.17618e+01 4.868e-04 3.536e-04 3.347e-04 4.53 20 7 2.20978e-01 1.20817e+01 4.866e-04 3.535e-04 3.343e-04 5.04 22 3 2.33577e-01 1.12794e+01 1.042e-03 3.535e-04 9.803e-04 accuracy not achieved 22 4 2.33577e-01 1.12794e+01 5.083e-04 3.535e-04 3.652e-04 accuracy not achieved 22 5 2.19891e-01 1.21554e+01 4.974e-04 3.535e-04 3.499e-04 4.53 22 6 2.12592e-01 1.26717e+01 4.898e-04 3.535e-04 3.390e-04 4.87 22 7 2.08030e-01 1.30138e+01 4.865e-04 3.536e-04 3.341e-04 5.34 24 3 2.19891e-01 1.21554e+01 1.181e-03 3.535e-04 1.127e-03 accuracy not achieved 24 4 2.19891e-01 1.21554e+01 5.423e-04 3.535e-04 4.112e-04 accuracy not achieved 24 5 2.08725e-01 1.29607e+01 4.956e-04 3.535e-04 3.473e-04 4.55 24 6 2.00994e-01 1.35738e+01 4.937e-04 3.535e-04 3.446e-04 4.86 24 7 1.96699e-01 1.39366e+01 4.868e-04 3.535e-04 3.346e-04 5.35 26 3 2.08725e-01 1.29607e+01 1.292e-03 3.535e-04 1.243e-03 accuracy not achieved 26 4 2.08725e-01 1.29607e+01 5.658e-04 3.535e-04 4.417e-04 accuracy not achieved 26 5 1.98941e-01 1.37452e+01 4.932e-04 3.535e-04 3.439e-04 5.28 26 6 1.90787e-01 1.44643e+01 4.975e-04 3.535e-04 3.500e-04 5.57 26 7 1.86711e-01 1.48488e+01 4.870e-04 3.535e-04 3.350e-04 6.00 28 3 1.98941e-01 1.37
Now we equilibrate for a while
equil_rounds = 10
equil_steps = 100
for i in tqdm.trange(equil_rounds):
system.integrator.run(equil_steps)
100%|██████████| 10/10 [00:04<00:00, 2.05it/s]
As we need the magnetization of our system, we use espressomd.observables.MagneticDipoleMoment to calculate the total dipole moment of the system which is the magnetization times the volume of the system.
import espressomd.observables
dipm_tot_calc = espressomd.observables.MagneticDipoleMoment(ids=particles.id)
Now we set the desired number of loops for the sampling
# Sampling
loops = 200
and sample the first and second moment of the magnetization or total dipole moment, by averaging over all total dipole moments occurring during the simulation
# initialize array that will hold the sampled dipole moments
dipms = np.full((loops, 3), np.nan)
# sample dipole moment
for i in tqdm.trange(loops):
system.integrator.run(10)
dipms[i, :] = dipm_tot_calc.calculate()
# calculate average first and second moment of total dipole moment
dipm_tot = np.mean(dipms, axis=0)
dipm_tot_2 = np.mean(dipms**2, axis=0)
100%|██████████| 200/200 [00:10<00:00, 19.37it/s]
For the estimator of the initial susceptibility $\chi_\mathrm{init}$ we need the magnitude of one single dipole moment
# dipole moment
dipm = np.sqrt(dip_lambda * 4 * np.pi * lj_sigma**3. * kT / mu_0)
print(f'dipm = {dipm:.4f}')
dipm = 6.1400
Now we can calculate $\chi_\mathrm{init}$ from our simulation data
# susceptibility in 3d system
chi = mu_0 / (system.volume() * 3. * kT) * (np.sum(dipm_tot_2 * dipm**2.) - np.sum(np.square(dipm_tot * dipm)))
and print the result
print(f'chi = {chi:.4f}')
chi = 0.6524
Compared with the value $\chi = 0.822 \pm 0.017$ of Ref. [1] (see table 1) it should be very similar.
Now we want to compare the result with the theoretical expectations. At first with the simple Langevin susceptibility
chi_L = 8. * dip_lambda * phi
print(f'chi_L = {chi_L:.4f}')
chi_L = 0.6288
and at second with the more advanced one (see Ref. [1] eq. (6)) which has a cubic accuracy in $\chi_\mathrm{L}$ and reads
\begin{equation} \chi = \chi_\mathrm{L} \left( 1 + \frac{\chi_\mathrm{L}}{3} + \frac{\chi_\mathrm{L}^2}{144} \right) \end{equation}chi_I = chi_L * (1 + chi_L / 3. + chi_L**2. / 144.)
print(f'chi_I = {chi_I:.4f}')
chi_I = 0.7623
Both of them should be smaller than our result, but the second one should be closer to our one. The deviation of the theoretical results to our simulation result can be explained by the fact that in the Langevin model there are no interactions between the particles incorporated at all and the more advanced (mean-field-type) one of Ref. [1] do not take occurring cluster formations into account but assumes a homogeneous distribution of the particles. For higher values of the volume fraction $\phi$ and the dipolar interaction parameter $\lambda$ the deviations will increase as the cluster formation will become more pronounced.
At the end of this tutorial we now want to sample the magnetization curve of a three dimensional system and compare the results with analytical solutions. Again we will compare with the Langevin function but also with the approximation of Ref. [2] (see also Ref. [1] for the right coefficients) which takes the dipole-dipole interaction into account. For this approximation, which is a modified mean-field theory based on the pair correlation function, the Langevin parameter $\alpha$ is replaced by
\begin{equation} \alpha' = \alpha + \chi_\mathrm{L}~L(\alpha) + \frac{\chi_\mathrm{L}^{2}}{16} L(\alpha) \frac{\mathrm{d} L(\alpha)}{\mathrm{d}\alpha} \end{equation}where $\chi_\mathrm{L}$ is the Langevin susceptibility
\begin{equation} \chi_\mathrm{L} = \frac{N}{V}\frac{\mu_0 \mu^2}{3k_\mathrm{B}T} = 8 \cdot \lambda \cdot \phi \end{equation}Analogous to part II we start at zero external magnetic field and increase the external field successively. At every value of the external field we sample the total dipole moment which is proportional to the magnetization as we have a fixed volume.
First we create a list of values of the Langevin parameter $\alpha$. As we already sampled the magnetization at zero external field in the last section, we take this value and continue with the sampling of an external field unequal zero
alphas = [0,0.5,1,2,4,8]
Now for each value in this list we sample the total dipole moment / magnetization of the system for a while. Keep in mind that we only the current orientation of the dipole moments, i.e. the unit vector of the dipole moments, is saved in the particle list but not their magnitude. Thus we have to use $H\cdot \mu$ as the external magnetic field, where $\mu$ is the magnitude of a single magnetic dipole moment. We will apply the field in x-direction using the class constraints of ESPResSo.
As in part II we use the same system for every value of the Langevin parameter $\alpha$. Thus we use that the system is already pre-equilibrated from the previous run so we save some equilibration time. For scientific purposes one would use a new system for every value for the Langevin parameter to ensure that the systems are independent and no correlation effects are measured. Also one would perform more than just one simulation for each value of $\alpha$ to increase the precision of the results.
Additionally, we will visualize the sampling of the magnetization curve using ZnDraw. Note, that the magnetic field is increasing during the video.
# remove all constraints
system.constraints.clear()
# array for magnetizations in field direction
magnetizations = np.full_like(alphas, np.nan)
# use result for alpha=0 from previous chapter
magnetizations[0] = np.average(dipm_tot)
# number of loops for sampling
loops_m = 100
for ndx, alpha in enumerate(pbar := tqdm.tqdm(alphas)):
pbar.set_description(f"Sampling for α={alpha:.2f}")
if alpha == 0:
continue
# set magnetic field constraint
H_dipm = alpha * kT / mu_0
H_field = [H_dipm, 0, 0]
H_constraint = espressomd.constraints.HomogeneousMagneticField(H=H_field)
system.constraints.add(H_constraint)
# equilibration
for i in range(equil_rounds):
system.integrator.run(equil_steps)
# sampling
magn_temp = np.full(loops_m, np.nan)
for i in range(loops_m):
system.integrator.run(20)
magn_temp[i] = dipm_tot_calc.calculate()[0]
# save average magnetization
magnetizations[ndx] = np.mean(magn_temp)
# remove constraint
if not alpha == alphas[-1]:
system.constraints.clear()
Sampling for α=8.00: 100%|██████████| 6/6 [01:12<00:00, 12.10s/it]
Now we define the Langevin function and the modified mean-field-approximation of the Langevin parameter of Ref. [2]
# Langevin function
def L(y):
return np.cosh(y) / np.sinh(y) - 1 / y
# second order mean-field-model from Ref. [2]
def alpha_mean_field(alpha, dip_lambda, phi):
chi = 8. * dip_lambda * phi
return alpha + chi * L(alpha) + chi**2. / 16. * L(alpha) * (1. / alpha**2. - 1. / np.sinh(alpha)**2.)
We also want to plot the linear approximation at $\alpha = 0$ to see for which values of $\alpha$ this approximation holds. We use the initial susceptibility calculated in the first chapter of this part as the gradient. As we want the gradient of $M^*$ with respect to $\alpha$ which fulfills the relation
\begin{equation} \frac{\partial M^*}{\partial \alpha} = \frac{1}{M_\mathrm{sat}}\frac{\partial M}{\partial \left( \frac{\mu_0\mu}{k_\mathrm{B}T} H\right)} = \frac{k_\mathrm{B}T~V}{\mu_0\mu^2N}\frac{\partial M}{\partial H} = \frac{k_\mathrm{B}T~V}{\mu_0\mu^2N}~\chi \end{equation}we have to scale our calculated initial susceptibility $\chi_{init}$ by a factor to get it in our dimensionless units.
Now we plot the resulting curves together with our simulation results and the linear approximation
y = np.arange(0.01, 10, 0.1)
initial_susceptibility = system.volume() * kT * chi / (N * mu_0 * dipm**2)
plt.figure(figsize=(10, 10))
plt.ylim(0, 1.)
plt.xlabel(r'$\alpha$', fontsize=20)
plt.ylabel(r'$M^*$', fontsize=20)
plt.plot(y, L(y), label='Langevin function')
plt.plot(y, L(alpha_mean_field(y, dip_lambda, phi)),
label='modified mean-field-theory')
plt.plot(alphas, magnetizations / N, 'o', label='simulation results')
plt.plot(y, initial_susceptibility * y,
label=r'linear approximation at $\alpha = 0$')
plt.legend(fontsize=20)
plt.show()
We can see that the magnetization curve where we used the Langevin parameter of the modified mean-field-theory is closer to our simulation results. Also we can clearly see that the linear approximation holds only for very small values of $\alpha$.
At this point it should be mentioned, that the modified mean-field-model assumes a spatially homogeneous system which is not the case at higher volume fractions $\phi$ and dipolar interaction parameters $\lambda$ as the particles form clusters. We can already see this with our simulation results as they visibly deviate from the modified mean-field-model.
At sufficiently high volume fractions $\phi$ and dipolar interaction parameters $\lambda$ these clusters can be so rigid, that simulations with normal methods are impossible as the relaxation times exceeds normal simulation times by far, resulting in strongly correlated configurations and thus measurements.
Finally, we use ZnDraw to visualize the evolving system for high $\alpha$. We just need to continue running our system as it is already warmed up for high magnetic field.
# initializing zndraw visualizer
color = {0: "#7fc454"}
radii = {0: lj_sigma/2}
vis = espressomd.zn.Visualizer(system, colors=color, radii=radii)
for _ in range(400):
system.integrator.run(5)
vis.update()
[1] Zuowei Wang, Christian Holm, and Hanns Walter Müller. Molecular dynamics
study on the equilibrium magnetization properties and structure of ferrofluids. In:
Phys. Rev. E 66: 021405, 2002. DOI:10.1103/PhysRevE.66.021405
[2] Alexey O. Ivanov and Olga B. Kuznetsova. Magnetic properties of dense ferrofluids:
An influence of interparticle correlations. Phys. Rev. E 64: 041405, 2001. DOI:10.1103/PhysRevE.64.041405