Skip to content

Commit 33a6f97

Browse files
nebm fs: corrected variables; added test; refining params
1 parent b0eb23e commit 33a6f97

File tree

5 files changed

+178
-33
lines changed

5 files changed

+178
-33
lines changed

Diff for: fidimag/common/chain_method_integrators.py

+45-22
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ def __init__(self, ChainObj,
160160
# band, forces, distances, rhs_fun, action_fun,
161161
# n_images, n_dofs_image,
162162
maxSteps=1000,
163-
maxCreep=5, actionTol=1e-10, forcesTol=1e-6,
164-
etaScale=1.0, dEta=2, minEta=0.001,
163+
maxCreep=6, actionTol=1e-10, forcesTol=1e-6,
164+
etaScale=1e-6, dEta=4., minEta=1e-6,
165165
# perturbSeed=42, perturbFactor=0.1,
166-
nTrail=10, resetMax=20
166+
nTrail=13, resetMax=20
167167
):
168168
# super(FSIntegrator, self).__init__(band, rhs_fun)
169169

@@ -188,17 +188,16 @@ def __init__(self, ChainObj,
188188
self.n_dofs_image = self.ChainObj.n_dofs_image
189189
# self.forces_prev = np.zeros_like(band).reshape(n_images, -1)
190190
# self.G :
191-
self.forces = self.ChainObj.forces
191+
self.forces = self.ChainObj.G
192192
self.distances = self.ChainObj.distances
193-
self.forces_old = np.zeros_like(self.ChainObj.forces)
193+
self.forces_old = np.zeros_like(self.ChainObj.G)
194194

195195
# self.band should be just a reference to the band in the ChainObj
196196
self.band = self.ChainObj.band
197-
self.band_old = np.zeros_like(self.band)
197+
self.band_old = np.copy(self.band)
198198

199199
def run_for(self, n_steps):
200200

201-
t = 0.0
202201
nStart = 0
203202
exitFlag = False
204203
totalRestart = True
@@ -214,19 +213,25 @@ def run_for(self, n_steps):
214213

215214
INNER_DOFS = slice(self.n_dofs_image, -self.n_dofs_image)
216215

216+
# Save data of energies on every step
217+
self.ChainObj.tablewriter.save()
218+
self.ChainObj.tablewriter_dm.save()
219+
220+
np.save(self.ChainObj.name + '_init.npy', self.ChainObj.band)
221+
217222
while not exitFlag:
218223

219224
if totalRestart:
220-
if self.step > 0:
225+
if self.i_step > 0:
221226
print('Restarting')
222227
self.band[:] = self.band_old
223228

224229
# Compute from self.band. Do not update the step at this stage:
225230
# This step updates the forces and distances in the G array of the nebm module,
226231
# using the current band state self.y
227-
# TODO: remove time from chain method rhs
228-
# make a specific function to update G??
229-
self.rhs(t, self.y)
232+
# TODO: make a specific function to update G??
233+
print('Computing forces')
234+
self.ChainObj.nebm_step(self.band, ensure_zero_extrema=True)
230235
self.action = self.ChainObj.compute_action()
231236

232237
# self.step += 1
@@ -250,13 +255,17 @@ def run_for(self, n_steps):
250255
self.trailAction
251256

252257
self.ChainObj.nebm_step(self.band, ensure_zero_extrema=True)
253-
self.action = self.ChainObj.action_fun()
258+
self.action = self.ChainObj.compute_action()
254259

255260
self.trailAction[nStart] = self.action
256261
nStart = next(trailPool)
257262

258263
self.i_step += 1
259264

265+
# Save data of energies on every step
266+
self.ChainObj.tablewriter.save()
267+
self.ChainObj.tablewriter_dm.save()
268+
260269
# Getting averages of forces from the INNER images in the band (no extrema)
261270
# (forces are given by vector G in the chain method code)
262271
# TODO: we might use all band images, not only inner ones, although G is zero at the extrema
@@ -268,10 +277,14 @@ def run_for(self, n_steps):
268277
# Average step difference between trailing action and new action
269278
deltaAction = (np.abs(self.trailAction[nStart] - self.action)) / self.nTrail
270279

280+
# print('trail Actions', self.trailAction)
281+
271282
# Print log
272283
print(f'Step {self.i_step} ⟨RMS(G)〉= {mean_rms_G_norms_per_image:.5e} ',
273284
f'deltaAction = {deltaAction:.5e} Creep n = {creepCount:>3} resetC = {resetCount:>3} ',
274-
f'eta = {eta:>5.4e}')
285+
f'eta = {eta:>5.4e} '
286+
f'action = {self.action:>5.4e} action_old = {self.action_old:>5.4e}'
287+
)
275288

276289
# 10 seems like a magic number; we set here a minimum number of evaulations
277290
if (nStart > self.nTrail * 10) and (deltaAction < self.actionTol):
@@ -291,17 +304,25 @@ def run_for(self, n_steps):
291304
eta = eta / (self.dEta * self.dEta)
292305

293306
# If eta is too small, reset and start again
294-
if (eta < 1e-3):
295-
print('')
307+
if (eta < self.minEta):
308+
# print('')
296309
resetCount += 1
297310
# bestAction = self.action_old
298-
self.refine_path(self.ChainObj.distances, self.band) # Resets the path to equidistant structures (smoothing kinks?)
311+
print('Refining path')
312+
self.refine_path(self.ChainObj.path_distances, self.band) # Resets the path to equidistant structures (smoothing kinks?)
299313
# PathChanged[:] = True
300314

301315
if resetCount > self.resetMax:
302-
print('Failed to converge!')
303-
# Otherwise, just
316+
print('Failed to converge! Reached max number of restarts')
317+
exitFlag = True
318+
break # creep loop
319+
320+
totalRestart = True
321+
break # creep loop
322+
323+
# Otherwise, just start again with smaller alpha
304324
else:
325+
print('Decreasing alpha')
305326
self.band[:] = self.band_old
306327
self.forces[:] = self.forces_old
307328
# If action decreases, move to next creep step
@@ -318,20 +339,22 @@ def run_for(self, n_steps):
318339
# END creep while loop
319340

320341
# After creep loop:
321-
self.eta = self.eta * self.dEta
342+
eta = eta * self.dEta
322343
resetCount = 0
323344

345+
np.save(self.ChainObj.name + '.npy', self.ChainObj.band)
346+
324347
# Taken from the string method class
325-
def refine_path(self, distances, band):
348+
def refine_path(self, path_distances, band):
326349
"""
327350
"""
328-
new_dist = np.linspace(distances[0], distances[-1], distances.shape[0])
351+
new_dist = np.linspace(path_distances[0], path_distances[-1], path_distances.shape[0])
329352
# Restructure the string by interpolating every spin component
330353
# print(self.integrator.y[self.n_dofs_image:self.n_dofs_image + 10])
331354
bandrs = band.reshape(self.n_images, self.n_dofs_image)
332355
for i in range(self.n_dofs_image):
333356

334-
cs = si.CubicSpline(distances, bandrs[:, i])
357+
cs = si.CubicSpline(path_distances, bandrs[:, i])
335358
bandrs[:, i] = cs(new_dist)
336359

337360
# def set_options(self):

Diff for: fidimag/common/hubert_minimiser.py

+1
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def minimise(self,
264264
exitFlag = True
265265
break # creep loop
266266

267+
# TODO: check if we need to use last spin state here
267268
if self.totalE > self.totalE_last: # If E increases, decrease eta so minimise slower
268269
# print('Decreasing eta')
269270
self.creepCount = 0

Diff for: fidimag/common/nebm_FS.py

+21-11
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def __init__(self, sim,
113113
name='unnamed',
114114
climbing_image=None,
115115
openmp=False,
116-
integrator='sundials' # or scipy
116+
# integrator='sundials' # or scipy
117117
):
118118

119119
super(NEBM_FS, self).__init__(sim,
@@ -127,7 +127,7 @@ def __init__(self, sim,
127127
)
128128

129129
# We need the gradient norm to calculate the action
130-
self.gradientENorm = np.zeros_like(self.n_images)
130+
self.gradientENorm = np.zeros(self.n_images)
131131

132132
# Initialisation ------------------------------------------------------
133133
# See the NEBMBase class for details
@@ -136,7 +136,7 @@ def __init__(self, sim,
136136

137137
self.initialise_energies()
138138

139-
self.initialise_integrator(integrator=integrator)
139+
self.initialise_integrator()
140140

141141
self.create_tablewriter()
142142

@@ -236,9 +236,9 @@ def compute_effective_field_and_energy(self, y):
236236
the relaxation function
237237
"""
238238

239-
self.gradientE = self.gradientE.reshape(self.n_images, -1)
239+
self.gradientE.shape = (self.n_images, -1)
240240

241-
y = y.reshape(self.n_images, -1)
241+
y.shape = (self.n_images, -1)
242242

243243
# Do not update the extreme images
244244
for i in range(1, len(y) - 1):
@@ -253,13 +253,17 @@ def compute_effective_field_and_energy(self, y):
253253

254254
self.energies[i] = self.sim.compute_energy()
255255

256+
# TODO: move this calc to the action function
256257
# Compute the gradient norm per every image
257-
Gnorms2 = np.sum(self.gradientE.reshape(-1, 3)**2, axis=1)
258+
Gnorms2 = np.sum(self.gradientE**2, axis=1) / self.n_images
258259
# Compute the root mean square per image
259-
self.gradientENorm[:] = np.sqrt(np.mean(Gnorms2.reshape(self.n_images, -1), axis=1))
260+
self.gradientENorm[:] = np.sqrt(Gnorms2)
260261

261-
y = y.reshape(-1)
262-
self.gradientE = self.gradientE.reshape(-1)
262+
# DEBUG:
263+
# print('gradEnorm', self.gradientENorm)
264+
265+
y.shape = (-1)
266+
self.gradientE.shape = (-1)
263267

264268
def compute_tangents(self, y):
265269
nebm_clib.compute_tangents(self.tangents, y, self.energies,
@@ -327,7 +331,10 @@ def compute_action(self):
327331

328332
# TODO: we can use a better quadrature such as Gaussian
329333
# notice that the gradient norm here is using the RMS
330-
action = spi.trapezoid(self.gradientENorm, self.distances)
334+
action = spi.trapezoid(self.gradientENorm, self.path_distances)
335+
336+
# DEBUG:
337+
# print('action from gradE', action)
331338

332339
# The spring term in the action is added as |F_k|^2 / (2 * self.k) = self.k * x^2 / 2
333340
# (CHECK) This assumes the spring force is orthogonal to the force gradient (after projection)
@@ -341,10 +348,13 @@ def compute_action(self):
341348
dist_minus_norm = self.distances[:-1]
342349
# dY_plus_norm = distances[i];
343350
# dY_minus_norm = distances[i - 1];
344-
springF2 = self.k * ((dist_plus_norm - dist_minus_norm)**2)
351+
springF2 = self.k[1:-1] * ((dist_plus_norm - dist_minus_norm)**2)
345352
# CHECK: do we need to scale?
346353
action += np.sum(springF2) / (self.n_images - 2)
347354

355+
# DEBUG:
356+
# print('action spring term', np.sum(springF2) / (self.n_images - 2))
357+
348358
return action
349359

350360
def compute_min_action(self):

Diff for: tests/tes_oommf.py renamed to tests/test_oommf.py

File renamed without changes.

Diff for: tests/test_two_particles_nebm-fs.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
3+
# FIDIMAG:
4+
from fidimag.micro import Sim
5+
from fidimag.common import CuboidMesh
6+
from fidimag.micro import UniformExchange, UniaxialAnisotropy
7+
from fidimag.common.nebm_FS import NEBM_FS
8+
import numpy as np
9+
10+
# Material Parameters
11+
# Parameters
12+
A = 1e-12
13+
Kx = 1e5
14+
# Strong anisotropy
15+
Ms = 3.8e5
16+
17+
18+
"""
19+
We will define two particles using a 4 sites mesh, letting the
20+
sites in the middle as Ms = 0
21+
22+
"""
23+
24+
25+
def two_part(pos):
26+
27+
x = pos[0]
28+
29+
if x > 6 or x < 3:
30+
return Ms
31+
else:
32+
return 0
33+
34+
# Finite differences mesh
35+
mesh = CuboidMesh(nx=3, ny=1, nz=1, dx=3, dy=3, dz=3, unit_length=1e-9)
36+
37+
# Simulation Function
38+
def relax_string(maxst, simname, init_im, interp, save_every=10000):
39+
"""
40+
"""
41+
42+
# Prepare simulation
43+
# We define the cylinder with the Magnetisation function
44+
sim = Sim(mesh)
45+
sim.Ms = two_part
46+
47+
# sim.add(UniformExchange(A=A))
48+
49+
# Uniaxial anisotropy along x-axis
50+
sim.add(UniaxialAnisotropy(Kx, axis=(1, 0, 0)))
51+
52+
# Define many initial states close to one extreme. We want to check
53+
# if the images in the last step, are placed mostly in equally positions
54+
# init_images = init_im
55+
56+
# Number of images between each state specified before (here we need only
57+
# two, one for the states between the initial and intermediate state
58+
# and another one for the images between the intermediate and final
59+
# states). Thus, the number of interpolations must always be
60+
# equal to 'the number of initial states specified', minus one.
61+
interpolations = interp
62+
63+
nebm = NEBM_FS(sim, init_im, interpolations=interpolations, name=simname)
64+
65+
# dt = integrator.stepsize means after every integrator step, the images
66+
# are rescaled. We can run more integrator steps if we decrease the
67+
# stepsize, e.g. dt=1e-3 and integrator.stepsize=1e-4
68+
nebm.integrator.maxSteps = 33
69+
nebm.integrator.run_for(maxst,
70+
# save_vtks_every=save_every,
71+
# save_npys_every=save_every,
72+
)
73+
74+
return nebm
75+
76+
77+
def mid_m(pos):
78+
if pos[0] > 4:
79+
return (0.5, 0, 0.2)
80+
else:
81+
return (-0.5, 0, 0.2)
82+
83+
84+
def test_energy_barrier_2particles_string():
85+
# Initial images: we set here a rotation interpolating
86+
init_im = [(-1, 0, 0), (1, 0, 0)]
87+
interp = [13]
88+
89+
barriers = []
90+
91+
# Define different ks for multiple simulations
92+
# krange = ['1e8']
93+
94+
string = relax_string(10,
95+
'nebmfs_2particles_k1e8_10-10int',
96+
init_im,
97+
interp,
98+
save_every=5000,
99+
)
100+
101+
_file = np.loadtxt('string_2particles_k1e8_10-10int_energy.ndt')
102+
barriers.append((np.max(_file[-1][1:]) - _file[-1][1]) / 1.602e-19)
103+
104+
print('Energy barrier is:', barriers[-1])
105+
assert np.abs(barriers[-1] - 0.016019) < 1e-5
106+
107+
print(barriers)
108+
109+
110+
if __name__ == '__main__':
111+
test_energy_barrier_2particles_string()

0 commit comments

Comments
 (0)