def create_tablewriter(self): entities_energy = { "step": {"unit": "<1>", "get": lambda sim: sim.step, "header": "steps"}, "energy": { "unit": "<J>", "get": lambda sim: sim.energy, "header": ["image_%d" % i for i in range(self.image_num + 2)], }, } self.tablewriter = DataSaver(self, "%s_energy.ndt" % (self.name), entities=entities_energy) entities_dm = { "step": {"unit": "<1>", "get": lambda sim: sim.step, "header": "steps"}, "dms": { "unit": "<1>", "get": lambda sim: sim.distances, "header": ["image_%d_%d" % (i, i + 1) for i in range(self.image_num + 1)], }, } self.tablewriter_dm = DataSaver(self, "%s_dms.ndt" % (self.name), entities=entities_dm)
def create_tablewriter(self): entities_energy = { 'step': { 'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps' }, 'energy': { 'unit': '<J>', 'get': lambda sim: sim.energy, 'header': ['image_%d' % i for i in range(self.image_num + 2)] } } self.tablewriter = DataSaver(self, '%s_energy.ndt' % (self.name), entities=entities_energy) entities_dm = { 'step': { 'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps' }, 'dms': { 'unit': '<1>', 'get': lambda sim: sim.distances, 'header': [ 'image_%d_%d' % (i, i + 1) for i in range(self.image_num + 1) ] } } self.tablewriter_dm = DataSaver(self, '%s_dms.ndt' % (self.name), entities=entities_dm)
def create_tablewriter(self): entities_energy = { 'step': {'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps'}, 'energy': {'unit': '<J>', 'get': lambda sim: sim.energy, 'header': ['image_%d' % i for i in range(self.image_num + 2)]} } self.tablewriter = DataSaver( self, '%s_energy.ndt' % (self.name), entities=entities_energy) entities_dm = { 'step': {'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps'}, 'dms': {'unit': '<1>', 'get': lambda sim: sim.distances, 'header': ['image_%d_%d' % (i, i + 1) for i in range(self.image_num + 1)]} } self.tablewriter_dm = DataSaver( self, '%s_dms.ndt' % (self.name), entities=entities_dm)
class NEB_Sundials(object): """ Nudged elastic band method by solving the differential equation using Sundials. """ def __init__(self, sim, initial_images, climbing_image=None, interpolations=None, spring=5e5, name="unnamed"): """ *Arguments* sim: the Simulation class initial_images: a list contain the initial value, which can have any of the forms accepted by the function 'finmag.util.helpers. vector_valued_function', for example, initial_images = [(0,0,1), (0,0,-1)] or with given defined function def init_m(pos): x=pos[0] if x<10: return (0,1,1) return (-1,0,0) initial_images = [(0,0,1), (0,0,-1), init_m ] are accepted forms. climbing_image : An integer with the index (from 1 to the total number of images minus two; it doesn't have any sense to use the extreme images) of the image with the largest energy, which will be updated in the NEB algorithm using the Climbing Image NEB method (no spring force and "with the component along the elastic band inverted" [*]). See: [*] Henkelman et al., The Journal of Chemical Physics 113, 9901 (2000) interpolations : a list only contain integers and the length of this list should equal to the length of the initial_images minus 1, i.e., len(interpolations) = len(initial_images) - 1 ** THIS IS not well defined in CARTESIAN coordinates** spring: the spring constant, a float value disable_tangent: this is an experimental option, by disabling the tangent, we can get a rough feeling about the local energy minima quickly. """ self.sim = sim self.name = name self.spring = spring # We set a minus one because the *sundials_rhs* function # only uses an array without counting the extreme images, # whose length is self.image_num (see below) if climbing_image is not None: self.climbing_image = climbing_image - 1 else: self.climbing_image = climbing_image if interpolations is None: interpolations = [0 for i in range(len(initial_images) - 1)] self.initial_images = initial_images self.interpolations = interpolations if len(interpolations) != len(initial_images) - 1: raise RuntimeError( """The length of interpolations should be equal to the length of the initial_images array minus 1, i.e., len(interpolations) = len(initial_images) - 1""" ) if len(initial_images) < 2: raise RuntimeError( """At least two images must be provided to create the energy band""" ) # the total image number including two ends self.total_image_num = len(initial_images) + sum(interpolations) self.image_num = self.total_image_num - 2 self.n = sim.n self.coords = np.zeros(3 * self.n * self.total_image_num) self.last_m = np.zeros(self.coords.shape) self.Heff = np.zeros(self.coords.shape) self.Heff.shape = (self.total_image_num, -1) self.tangents = np.zeros(3 * self.n * self.image_num) self.tangents.shape = (self.image_num, -1) self.energy = np.zeros(self.total_image_num) self.springs = np.zeros(self.image_num) self.pin_ids = np.array([i for i, v in enumerate(self.sim.pins) if v > 0], dtype=np.int32) self.t = 0 self.step = 0 self.ode_count = 1 self.integrator = None self.initial_image_coordinates() self.create_tablewriter() def create_tablewriter(self): entities_energy = { "step": {"unit": "<1>", "get": lambda sim: sim.step, "header": "steps"}, "energy": { "unit": "<J>", "get": lambda sim: sim.energy, "header": ["image_%d" % i for i in range(self.image_num + 2)], }, } self.tablewriter = DataSaver(self, "%s_energy.ndt" % (self.name), entities=entities_energy) entities_dm = { "step": {"unit": "<1>", "get": lambda sim: sim.step, "header": "steps"}, "dms": { "unit": "<1>", "get": lambda sim: sim.distances, "header": ["image_%d_%d" % (i, i + 1) for i in range(self.image_num + 1)], }, } self.tablewriter_dm = DataSaver(self, "%s_dms.ndt" % (self.name), entities=entities_dm) def initial_image_coordinates(self): """ Generate the coordinates linearly according to the number of interpolations provided. Example: Imagine we have 4 images and we want 3 interpolations between every neighbouring pair, i.e interpolations = [3, 3, 3] 1. Imagine the initial states with the interpolation numbers and choose the first and second state 0 1 2 3 X -------- X --------- X -------- X 3 3 3 2. Counter image_id is set to 0 3. Set the image 0 magnetisation vector as m0 and append the values to self.coords[0]. Update the counter: image_id = 1 now 4. Set the image 1 magnetisation values as m1 and interpolate the values between m0 and m1, generating 3 arrays with the magnetisation values of every interpolation image. For every array, append the values to self.coords[i] with i = 1, 2 and 3 ; updating the counter every time, so image_id = 4 now 5. Append the value of m1 (image 1) in self.coords[4] Update counter (image_id = 5 now) 6. Move to the next pair of images, now set the 1-th image magnetisation values as m0 and append to self.coords[5] 7. Interpolate to get self.coords[i], for i = 6, 7, 8 ... 8. Repeat as before until move to the pair of images: 2 - 3 9. Finally append the magnetisation of the last image (self.initial_images[-1]). In this case, the 3rd image Then, for every magnetisation vector values array (self.coords[i]) append the value to the simulation and store the energies corresponding to every i-th image to the self.energy[i] arrays Finally, flatten the self.coords matrix (containing the magnetisation values of every image in different rows) ** Our generalised coordinates in the NEBM are the magnetisation values """ # Initiate the counter image_id = 0 self.coords.shape = (self.total_image_num, -1) # For every interpolation between images (zero if no interpolations # were specified) for i in range(len(self.interpolations)): # Store the number n = self.interpolations[i] # Save on the first image of a pair (step 1, 6, ...) self.sim.set_m(self.initial_images[i]) m0 = self.sim.spin.copy() self.coords[image_id][:] = m0[:] image_id = image_id + 1 # Set the second image in the pair as m1 and interpolate # (step 4 and 7), saving in corresponding self.coords entries self.sim.set_m(self.initial_images[i + 1]) m1 = self.sim.spin.copy() # Interpolations (arrays with magnetisation values) coords = linear_interpolation_two(m0, m1, n, self.pin_ids) for coord in coords: self.coords[image_id][:] = coord[:] image_id = image_id + 1 # Continue to the next pair of images # Append the magnetisation of the last image self.sim.set_m(self.initial_images[-1]) m2 = self.sim.spin self.coords[image_id][:] = m2[:] # Save the energies for i in range(self.total_image_num): self.sim.spin[:] = self.coords[i][:] self.sim.compute_effective_field(t=0) self.energy[i] = self.sim.compute_energy() # Flatten the array self.coords.shape = (-1,) def add_noise(self, T=0.1): noise = T * np.random.rand(self.total_image_num, 3, self.n) noise[:, :, self.pin_ids] = 0 noise[0, :, :] = 0 noise[-1, :, :] = 0 noise.shape = (-1,) self.coords += noise def save_vtks(self): """ Save vtk files in different folders, according to the simulation name and step. Files are saved as vtks/simname_simstep_vtk/image_00000x.vtk """ # Create the directory directory = "vtks/%s_%d" % (self.name, self.step) self.vtk = SaveVTK(self.sim.mesh, directory) self.coords.shape = (self.total_image_num, -1) # We use Ms from the simulation assuming that all the # images are the same for i in range(self.total_image_num): # We will try to save for the micromagnetic simulation (Ms) # or an atomistic simulation (mu_s) # TODO: maybe this can be done with an: isinstance try: self.vtk.save_vtk(self.coords[i].reshape(-1, 3), self.sim.Ms, step=i, vtkname="m") except: self.vtk.save_vtk(self.coords[i].reshape(-1, 3), self.sim._mu_s, step=i, vtkname="m") self.coords.shape = (-1,) def save_npys(self): """ Save npy files in different folders according to the simulation name and step Files are saved as: npys/simname_simstep/image_x.npy """ # Create directory as simname_simstep directory = "npys/%s_%d" % (self.name, self.step) if not os.path.exists(directory): os.makedirs(directory) # Save the images with the format: 'image_{}.npy' # where {} is the image number, starting from 0 self.coords.shape = (self.total_image_num, -1) for i in range(self.total_image_num): name = os.path.join(directory, "image_%d.npy" % i) np.save(name, self.coords[i, :]) self.coords.shape = (-1,) def create_integrator(self, rtol=1e-6, atol=1e-6, nsteps=10000): self.integrator = cvode.CvodeSolver(self.coords, self.sundials_rhs) self.integrator.set_options(rtol, atol) def compute_effective_field(self, y): y.shape = (self.total_image_num, -1) for i in range(self.image_num): self.sim.spin[:] = y[i + 1][:] # self.sim.compute_effective_field(t=0) # Compute effective field, which is the gradient of # the energy in the NEB method (derivative with respect to # the generalised coordinates) h = self.sim.field # self.Heff[i + 1, :] = h[:] # Compute the total energy self.energy[i + 1] = self.sim.compute_energy() # Compute the 'distance' or difference between neighbouring states # around y[i+1]. This is used to compute the spring force # dm1 = compute_dm(y[i], y[i + 1]) dm2 = compute_dm(y[i + 1], y[i + 2]) self.springs[i] = self.spring * (dm2 - dm1) # Use the native NEB (C code) to compute the tangents according # to the improved NEB method, developed by Henkelman and Jonsson # at: Henkelman et al., Journal of Chemical Physics 113, 22 (2000) neb_clib.compute_tangents(y, self.energy, self.tangents, self.total_image_num, 3 * self.n) # native_neb.compute_springs(y,self.springs,self.spring) y.shape = (-1,) def sundials_rhs(self, time, y, ydot): """ Right hand side of the optimization scheme used to find the minimum energy path. In our case, we use a LLG kind of equation: d Y / dt = Y x Y x D D = -( nabla E + [nabla E * t] t ) + F_spring where Y is an image: Y = (M_0, ... , M_N) and t is the tangent vector defined according to the energy of the neighbouring images (see Henkelman et al publication) If a climbing_image index is specified, the corresponding image will be iterated without the spring force and with an inversed component along the tangent """ # Update the ODE solver self.ode_count += 1 # Compute the eff field H for every image, H = -nabla E # (derived with respect to M) self.compute_effective_field(y) # Reshape y and ydot in a matrix of total_image_num rows y.shape = (self.total_image_num, -1) ydot.shape = (self.total_image_num, -1) # Compute the total force for every image (not the extremes) # Rememeber that self.image_num = self.total_image_num - 2 # The total force is: # D = - (-nabla E + [nabla E * t] t) + F_spring # This value is different is a climbing image is specified: # D_climb = -nabla E + 2 * [nabla E * t] t for i in range(self.image_num): h = self.Heff[i + 1] t = self.tangents[i] sf = self.springs[i] if not (self.climbing_image and i == self.climbing_image): h3 = h - np.dot(h, t) * t + sf * t else: h3 = h - 2 * np.dot(h, t) * t # Update H_eff[i + 1] with the new h3 h[:] = h3[:] # Update the step with the optimisation algorithm, in this # case we use: dY /dt = Y x Y x D # (check the C code in common/) neb_clib.compute_dm_dt(y, self.Heff, ydot, self.sim._pins, self.total_image_num, self.n) ydot[0, :] = 0 ydot[-1, :] = 0 y.shape = (-1,) ydot.shape = (-1,) return 0 def compute_distance(self): distance = [] ys = self.coords ys.shape = (self.total_image_num, -1) for i in range(self.total_image_num - 1): dm = compute_dm(ys[i], ys[i + 1]) distance.append(dm) ys.shape = (-1,) self.distances = np.array(distance) def run_until(self, t): if t <= self.t: return self.integrator.run_until(t) self.coords[:] = self.integrator.y[:] m = self.coords y = self.last_m m.shape = (self.total_image_num, -1) y.shape = (self.total_image_num, -1) max_dmdt = 0 for i in range(1, self.image_num + 1): dmdt = compute_dm(y[i], m[i]) / (t - self.t) if dmdt > max_dmdt: max_dmdt = dmdt m.shape = (-1,) y.shape = (-1,) self.last_m[:] = m[:] self.t = t return max_dmdt def relax(self, dt=1e-8, stopping_dmdt=1e4, max_steps=1000, save_npy_steps=100, save_vtk_steps=100): if self.integrator is None: self.create_integrator() log.debug( "Relaxation parameters: " "stopping_dmdt={} (degrees per nanosecond), " "time_step={} s, max_steps={}.".format(stopping_dmdt, dt, max_steps) ) # Save the initial state i=0 self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() for i in range(max_steps): if i % save_vtk_steps == 0: self.save_vtks() if i % save_npy_steps == 0: self.save_npys() self.step += 1 cvode_dt = self.integrator.get_current_step() increment_dt = dt if cvode_dt > dt: increment_dt = cvode_dt dmdt = self.run_until(self.t + increment_dt) self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() log.debug("step: {:.3g}, step_size: {:.3g}" " and max_dmdt: {:.3g}.".format(self.step, increment_dt, dmdt)) if dmdt < stopping_dmdt: break log.info( "Relaxation finished at time step = {:.4g}, " "t = {:.2g}, call rhs = {:.4g} " "and max_dmdt = {:.3g}".format(self.step, self.t, self.ode_count, dmdt) ) self.save_vtks() self.save_npys()
class NEB_Sundials(object): """ Nudged elastic band method by solving the differential equation using Sundials. """ def __init__(self, sim, initial_images, climbing_image=None, interpolations=None, spring=5e5, name='unnamed'): """ *Arguments* sim: the Simulation class initial_images: a list contain the initial value, which can have any of the forms accepted by the function 'finmag.util.helpers. vector_valued_function', for example, initial_images = [(0,0,1), (0,0,-1)] or with given defined function def init_m(pos): x=pos[0] if x<10: return (0,1,1) return (-1,0,0) initial_images = [(0,0,1), (0,0,-1), init_m ] are accepted forms. climbing_image : An integer with the index (from 1 to the total number of images minus two; it doesn't have any sense to use the extreme images) of the image with the largest energy, which will be updated in the NEB algorithm using the Climbing Image NEB method (no spring force and "with the component along the elastic band inverted" [*]). See: [*] Henkelman et al., The Journal of Chemical Physics 113, 9901 (2000) interpolations : a list only contain integers and the length of this list should equal to the length of the initial_images minus 1, i.e., len(interpolations) = len(initial_images) - 1 ** THIS IS not well defined in CARTESIAN coordinates** spring: the spring constant, a float value disable_tangent: this is an experimental option, by disabling the tangent, we can get a rough feeling about the local energy minima quickly. """ self.sim = sim self.name = name self.spring = spring # We set a minus one because the *sundials_rhs* function # only uses an array without counting the extreme images, # whose length is self.image_num (see below) if climbing_image is not None: self.climbing_image = climbing_image - 1 else: self.climbing_image = climbing_image if interpolations is None: interpolations = [0 for i in range(len(initial_images) - 1)] self.initial_images = initial_images self.interpolations = interpolations if len(interpolations) != len(initial_images) - 1: raise RuntimeError( """The length of interpolations should be equal to the length of the initial_images array minus 1, i.e., len(interpolations) = len(initial_images) - 1""") if len(initial_images) < 2: raise RuntimeError("""At least two images must be provided to create the energy band""") # the total image number including two ends self.total_image_num = len(initial_images) + sum(interpolations) self.image_num = self.total_image_num - 2 # Number of nodes (spins) self.n = sim.n # Total number of spherical coordinates # (2 components per spin for every image) self.coords = np.zeros(2 * self.n * self.total_image_num) self.last_m = np.zeros(self.coords.shape) # For the effective field we use the extremes (to fit the energies # array length). We could save some memory if we don't consider them # (CHECK this in the future) self.Heff = np.zeros(self.coords.shape) # self.Heff = np.zeros(2 * self.n * self.total_image_num) self.Heff.shape = (self.total_image_num, -1) # Tangent components in spherical coordinates self.tangents = np.zeros(2 * self.n * self.image_num) self.tangents.shape = (self.image_num, -1) self.energy = np.zeros(self.total_image_num) self.springs = np.zeros(self.image_num) self.pin_ids = np.array( [i for i, v in enumerate(self.sim.pins) if v > 0], dtype=np.int32) self.t = 0 self.step = 0 self.ode_count = 1 self.integrator = None self.initial_image_coordinates() self.create_tablewriter() def create_tablewriter(self): entities_energy = { 'step': { 'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps' }, 'energy': { 'unit': '<J>', 'get': lambda sim: sim.energy, 'header': ['image_%d' % i for i in range(self.image_num + 2)] } } self.tablewriter = DataSaver(self, '%s_energy.ndt' % (self.name), entities=entities_energy) entities_dm = { 'step': { 'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps' }, 'dms': { 'unit': '<1>', 'get': lambda sim: sim.distances, 'header': [ 'image_%d_%d' % (i, i + 1) for i in range(self.image_num + 1) ] } } self.tablewriter_dm = DataSaver(self, '%s_dms.ndt' % (self.name), entities=entities_dm) def initial_image_coordinates(self): """ Generate the coordinates linearly according to the number of interpolations provided. Example: Imagine we have 4 images and we want 3 interpolations between every neighbouring pair, i.e interpolations = [3, 3, 3] 1. Imagine the initial states with the interpolation numbers and choose the first and second state 0 1 2 3 X -------- X --------- X -------- X 3 3 3 2. Counter image_id is set to 0 3. Set the image 0 magnetisation vector as m0 and append the values to self.coords[0]. Update the counter: image_id = 1 now 4. Set the image 1 magnetisation values as m1 and interpolate the values between m0 and m1, generating 3 arrays with the magnetisation values of every interpolation image. For every array, append the values to self.coords[i] with i = 1, 2 and 3 ; updating the counter every time, so image_id = 4 now 5. Append the value of m1 (image 1) in self.coords[4] Update counter (image_id = 5 now) 6. Move to the next pair of images, now set the 1-th image magnetisation values as m0 and append to self.coords[5] 7. Interpolate to get self.coords[i], for i = 6, 7, 8 ... 8. Repeat as before until move to the pair of images: 2 - 3 9. Finally append the magnetisation of the last image (self.initial_images[-1]). In this case, the 3rd image Then, for every magnetisation vector values array (self.coords[i]) append the value to the simulation and store the energies corresponding to every i-th image to the self.energy[i] arrays Finally, flatten the self.coords matrix (containing the magnetisation values of every image in different rows) ** Our generalised coordinates in the NEBM are the magnetisation values *** Remember that the sim.spin object has the spins as: [mx1, my1, mz1, mx2, my2, ... ] and we transform to spherical coordinates as [theta1, phi1, theta2, phi2, ... ] Thus if we have N + 1 images and P + 1 spins, and naming the i-th image coordinates as thetai_j, phii_j, for the j-th spin, then the flattened array at the end is: [theta0_0, phi0_0, theta0_1, phi0_1, ... , theta0_P, phi0_P, theta1_0, phi1_0, ..., thetaN_0, phiN_0, ..., thetaN_P, phiN_P] """ # Initiate the counter image_id = 0 self.coords.shape = (self.total_image_num, -1) # For every interpolation between images (zero if no interpolations # were specified) for i in range(len(self.interpolations)): # Store the number n = self.interpolations[i] # Save on the first image of a pair (step 1, 6, ...) self.sim.set_m(self.initial_images[i]) m0 = self.sim.spin.copy() # Save the spin components in spherical coordinates self.coords[image_id][:] = cartesian2spherical(m0[:]) image_id = image_id + 1 # Set the second image in the pair as m1 and interpolate # (step 4 and 7), saving in corresponding self.coords entries self.sim.set_m(self.initial_images[i + 1]) m1 = self.sim.spin.copy() # Interpolations (arrays with magnetisation values) coords = linear_interpolation_two(m0, m1, n, self.pin_ids) for coord in coords: self.coords[image_id][:] = coord[:] image_id = image_id + 1 # Continue to the next pair of images # Append the magnetisation of the last image in spherical coords self.sim.set_m(self.initial_images[-1]) m2 = self.sim.spin self.coords[image_id][:] = cartesian2spherical(m2[:]) # Save the energies for i in range(self.total_image_num): self.sim.spin[:] = spherical2cartesian(self.coords[i][:]) self.sim.compute_effective_field(t=0) self.energy[i] = self.sim.compute_energy() # Flatten the array self.coords.shape = (-1, ) def add_noise(self, T=0.1): noise = T * np.random.rand(self.total_image_num, 3, self.n) noise[:, :, self.pin_ids] = 0 noise[0, :, :] = 0 noise[-1, :, :] = 0 noise.shape = (-1, ) self.coords += noise def save_vtks(self): """ Save vtk files in different folders, according to the simulation name and step. Files are saved as vtks/simname_simstep_vtk/image_00000x.vtk """ # Create the directory directory = 'vtks/%s_%d' % (self.name, self.step) self.vtk = SaveVTK(self.sim.mesh, directory) self.coords.shape = (self.total_image_num, -1) for i in range(self.total_image_num): self.vtk.save_vtk(spherical2cartesian(self.coords[i]), step=i, vtkname='m') self.coords.shape = (-1, ) def save_npys(self): """ Save npy files in different folders according to the simulation name and step Files are saved as: npys/simname_simstep/image_x.npy """ # Create directory as simname_simstep directory = 'npys/%s_%d' % (self.name, self.step) if not os.path.exists(directory): os.makedirs(directory) # Save the images with the format: 'image_{}.npy' # where {} is the image number, starting from 0 self.coords.shape = (self.total_image_num, -1) for i in range(self.total_image_num): name = os.path.join(directory, 'image_%d.npy' % i) np.save(name, spherical2cartesian(self.coords[i, :])) self.coords.shape = (-1, ) def create_integrator(self, rtol=1e-6, atol=1e-6, nsteps=10000): self.integrator = cvode.CvodeSolver(self.coords, self.sundials_rhs) self.integrator.set_options(rtol, atol) def compute_effective_field(self, y): """ Compute the effective field using Fidimag and the tangents using the NEB C code, according to the improved NEB method, developed by Henkelman and Jonsson # at: Henkelman et al., Journal of Chemical Physics 113, 22 (2000) y :: The array with all the spin components for every image, i.e. for a N images energy band and P spins system: [theta0_0, phi0_0, theta0_1, phi0_1, ... , theta0_P, phi0_P, theta1_0, phi1_0, ..., theta1_P, phi1_P, ... thetaN_0, phiN_0, ..., thetaN_P, phiN_P] """ # Every row has all the spin components of an image y.shape = (self.total_image_num, -1) for i in range(self.image_num): # Redefine the angles of the (i + 1)-th image # (see the corresponding function) check_boundary(y[i + 1]) # Set the simulation magnetisation to the (i+1)-th image # spin components self.sim.spin = spherical2cartesian(y[i + 1]) # Compute the effective field using Fidimag's methods. # (we use the time=0 since we are only using the simulation # object to get the field) # Remember that The effective field is the gradient of # the energy with respect to the generalised coordinates # i.e. the magnetisation self.sim.compute_effective_field(t=0) h = self.sim.field # Save the field components of the (i + 1)-th image # to the effective field array which is in spherical coords self.Heff[i + 1, :] = cartesian2spherical_field(h, y[i + 1]) # Compute and save the total energy for this image self.energy[i + 1] = self.sim.compute_energy() # Compute the 'distance' or difference between neighbouring states # around the y[i+1] image. This is used to compute the spring force dm1 = compute_dm(y[i], y[i + 1]) dm2 = compute_dm(y[i + 1], y[i + 2]) self.springs[i] = self.spring * (dm2 - dm1) # Compute tangents using the C library (this is also used for # cartesian coordinates, we set here the total number of spin # components for spherical coords) neb_clib.compute_tangents(y, self.energy, self.tangents, self.total_image_num, 2 * self.n) # Flatten y y.shape = (-1, ) def sundials_rhs(self, time, y, ydot): """ Right hand side of the optimization scheme used to find the minimum energy path. In our case, we use a LLG kind of equation: d Y / dt = D D = -( nabla E + [nabla E * t] t ) + F_spring where Y is an image with P spins: Y = (M_0, ... , M_P) and t is the tangent vector defined according to the energy of the neighbouring images (see Henkelman et al publication) If a climbing_image index is specified, the corresponding image will be iterated without the spring force and with an inversed component along the tangent """ # Update the ODE solver self.ode_count += 1 # Compute the eff field H for every image, H = -nabla E # (derived with respect to M) self.compute_effective_field(y) # Reshape y and ydot in a matrix of total_image_num rows y.shape = (self.total_image_num, -1) ydot.shape = (self.total_image_num, -1) # Compute the total force for every image (not the extremes) # Rememeber that self.image_num = self.total_image_num - 2 # The total force is: # D = - (-nabla E + [nabla E * t] t) + F_spring # This value is different is a climbing image is specified: # D_climb = -nabla E + 2 * [nabla E * t] t for i in range(self.image_num): # The eff field has (i + 1) since it considers the extreme images h = self.Heff[i + 1] # Tangents and springs has length: total images -2 (no extremes) t = self.tangents[i] sf = self.springs[i] if not (self.climbing_image and i == self.climbing_image): h3 = h - np.dot(h, t) * t + sf * t else: h3 = h - 2 * np.dot(h, t) * t # D vector for the i-th image (no extreme images) ydot[i + 1, :] = h3[:] # Extreme images do not have dynamics ydot[0, :] = 0 ydot[-1, :] = 0 # Flatten the arrays y.shape = (-1, ) ydot.shape = (-1, ) return 0 def compute_distance(self): distance = [] ys = self.coords ys.shape = (self.total_image_num, -1) for i in range(self.total_image_num - 1): dm = compute_dm(ys[i], ys[i + 1]) distance.append(dm) ys.shape = (-1, ) self.distances = np.array(distance) def run_until(self, t): if t <= self.t: return self.integrator.run_until(t) self.coords[:] = self.integrator.y[:] m = self.coords y = self.last_m m.shape = (self.total_image_num, -1) y.shape = (self.total_image_num, -1) max_dmdt = 0 for i in range(1, self.image_num + 1): dmdt = compute_dm(y[i], m[i]) / (t - self.t) if dmdt > max_dmdt: max_dmdt = dmdt m.shape = (-1, ) y.shape = (-1, ) self.last_m[:] = m[:] self.t = t return max_dmdt def relax(self, dt=1e-8, stopping_dmdt=1e4, max_steps=1000, save_npy_steps=100, save_vtk_steps=100): if self.integrator is None: self.create_integrator() log.debug("Relaxation parameters: " "stopping_dmdt={} (degrees per nanosecond), " "time_step={} s, max_steps={}.".format( stopping_dmdt, dt, max_steps)) # Save the initial state i=0 self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() for i in range(max_steps): if i % save_vtk_steps == 0: self.save_vtks() if i % save_npy_steps == 0: self.save_npys() self.step += 1 cvode_dt = self.integrator.get_current_step() increment_dt = dt if cvode_dt > dt: increment_dt = cvode_dt dmdt = self.run_until(self.t + increment_dt) self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() log.debug("step: {:.3g}, step_size: {:.3g}" " and max_dmdt: {:.3g}.".format(self.step, increment_dt, dmdt)) if dmdt < stopping_dmdt: break log.info("Relaxation finished at time step = {:.4g}, " "t = {:.2g}, call rhs = {:.4g} " "and max_dmdt = {:.3g}".format(self.step, self.t, self.ode_count, dmdt)) self.save_vtks() self.save_npys()
class NEB_Sundials(object): """ Nudged elastic band method by solving the differential equation using Sundials. """ def __init__(self, sim, initial_images, climbing_image=None, interpolations=None, spring=5e5, name='unnamed'): """ *Arguments* sim: the Simulation class initial_images: a list contain the initial value, which can have any of the forms accepted by the function 'finmag.util.helpers. vector_valued_function', for example, initial_images = [(0,0,1), (0,0,-1)] or with given defined function def init_m(pos): x=pos[0] if x<10: return (0,1,1) return (-1,0,0) initial_images = [(0,0,1), (0,0,-1), init_m ] are accepted forms. climbing_image : An integer with the index (from 1 to the total number of images minus two; it doesn't have any sense to use the extreme images) of the image with the largest energy, which will be updated in the NEB algorithm using the Climbing Image NEB method (no spring force and "with the component along the elastic band inverted" [*]). See: [*] Henkelman et al., The Journal of Chemical Physics 113, 9901 (2000) interpolations : a list only contain integers and the length of this list should equal to the length of the initial_images minus 1, i.e., len(interpolations) = len(initial_images) - 1 ** THIS IS not well defined in CARTESIAN coordinates** spring: the spring constant, a float value disable_tangent: this is an experimental option, by disabling the tangent, we can get a rough feeling about the local energy minima quickly. """ self.sim = sim self.name = name self.spring = spring # We set a minus one because the *sundials_rhs* function # only uses an array without counting the extreme images, # whose length is self.image_num (see below) if climbing_image is not None: self.climbing_image = climbing_image - 1 else: self.climbing_image = climbing_image if interpolations is None: interpolations = [0 for i in range(len(initial_images) - 1)] self.initial_images = initial_images self.interpolations = interpolations if len(interpolations) != len(initial_images) - 1: raise RuntimeError("""The length of interpolations should be equal to the length of the initial_images array minus 1, i.e., len(interpolations) = len(initial_images) - 1""") if len(initial_images) < 2: raise RuntimeError("""At least two images must be provided to create the energy band""") # the total image number including two ends self.total_image_num = len(initial_images) + sum(interpolations) self.image_num = self.total_image_num - 2 # Number of nodes (spins) self.n = sim.n # Total number of spherical coordinates # (2 components per spin for every image) self.coords = np.zeros(2 * self.n * self.total_image_num) self.last_m = np.zeros(self.coords.shape) # For the effective field we use the extremes (to fit the energies # array length). We could save some memory if we don't consider them # (CHECK this in the future) self.Heff = np.zeros(self.coords.shape) # self.Heff = np.zeros(2 * self.n * self.total_image_num) self.Heff.shape = (self.total_image_num, -1) # Tangent components in spherical coordinates self.tangents = np.zeros(2 * self.n * self.image_num) self.tangents.shape = (self.image_num, -1) self.energy = np.zeros(self.total_image_num) self.springs = np.zeros(self.image_num) self.pin_ids = np.array( [i for i, v in enumerate(self.sim.pins) if v > 0], dtype=np.int32) self.t = 0 self.step = 0 self.ode_count = 1 self.integrator = None self.initial_image_coordinates() self.create_tablewriter() def create_tablewriter(self): entities_energy = { 'step': {'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps'}, 'energy': {'unit': '<J>', 'get': lambda sim: sim.energy, 'header': ['image_%d' % i for i in range(self.image_num + 2)]} } self.tablewriter = DataSaver( self, '%s_energy.ndt' % (self.name), entities=entities_energy) entities_dm = { 'step': {'unit': '<1>', 'get': lambda sim: sim.step, 'header': 'steps'}, 'dms': {'unit': '<1>', 'get': lambda sim: sim.distances, 'header': ['image_%d_%d' % (i, i + 1) for i in range(self.image_num + 1)]} } self.tablewriter_dm = DataSaver( self, '%s_dms.ndt' % (self.name), entities=entities_dm) def initial_image_coordinates(self): """ Generate the coordinates linearly according to the number of interpolations provided. Example: Imagine we have 4 images and we want 3 interpolations between every neighbouring pair, i.e interpolations = [3, 3, 3] 1. Imagine the initial states with the interpolation numbers and choose the first and second state 0 1 2 3 X -------- X --------- X -------- X 3 3 3 2. Counter image_id is set to 0 3. Set the image 0 magnetisation vector as m0 and append the values to self.coords[0]. Update the counter: image_id = 1 now 4. Set the image 1 magnetisation values as m1 and interpolate the values between m0 and m1, generating 3 arrays with the magnetisation values of every interpolation image. For every array, append the values to self.coords[i] with i = 1, 2 and 3 ; updating the counter every time, so image_id = 4 now 5. Append the value of m1 (image 1) in self.coords[4] Update counter (image_id = 5 now) 6. Move to the next pair of images, now set the 1-th image magnetisation values as m0 and append to self.coords[5] 7. Interpolate to get self.coords[i], for i = 6, 7, 8 ... 8. Repeat as before until move to the pair of images: 2 - 3 9. Finally append the magnetisation of the last image (self.initial_images[-1]). In this case, the 3rd image Then, for every magnetisation vector values array (self.coords[i]) append the value to the simulation and store the energies corresponding to every i-th image to the self.energy[i] arrays Finally, flatten the self.coords matrix (containing the magnetisation values of every image in different rows) ** Our generalised coordinates in the NEBM are the magnetisation values *** Remember that the sim.spin object has the spins as: [mx1, my1, mz1, mx2, my2, ... ] and we transform to spherical coordinates as [theta1, phi1, theta2, phi2, ... ] Thus if we have N + 1 images and P + 1 spins, and naming the i-th image coordinates as thetai_j, phii_j, for the j-th spin, then the flattened array at the end is: [theta0_0, phi0_0, theta0_1, phi0_1, ... , theta0_P, phi0_P, theta1_0, phi1_0, ..., thetaN_0, phiN_0, ..., thetaN_P, phiN_P] """ # Initiate the counter image_id = 0 self.coords.shape = (self.total_image_num, -1) # For every interpolation between images (zero if no interpolations # were specified) for i in range(len(self.interpolations)): # Store the number n = self.interpolations[i] # Save on the first image of a pair (step 1, 6, ...) self.sim.set_m(self.initial_images[i]) m0 = self.sim.spin.copy() # Save the spin components in spherical coordinates self.coords[image_id][:] = cartesian2spherical(m0[:]) image_id = image_id + 1 # Set the second image in the pair as m1 and interpolate # (step 4 and 7), saving in corresponding self.coords entries self.sim.set_m(self.initial_images[i + 1]) m1 = self.sim.spin.copy() # Interpolations (arrays with magnetisation values) coords = linear_interpolation_two(m0, m1, n, self.pin_ids) for coord in coords: self.coords[image_id][:] = coord[:] image_id = image_id + 1 # Continue to the next pair of images # Append the magnetisation of the last image in spherical coords self.sim.set_m(self.initial_images[-1]) m2 = self.sim.spin self.coords[image_id][:] = cartesian2spherical(m2[:]) # Save the energies for i in range(self.total_image_num): self.sim.spin[:] = spherical2cartesian(self.coords[i][:]) self.sim.compute_effective_field(t=0) self.energy[i] = self.sim.compute_energy() # Flatten the array self.coords.shape = (-1,) def add_noise(self, T=0.1): noise = T * np.random.rand(self.total_image_num, 3, self.n) noise[:, :, self.pin_ids] = 0 noise[0, :, :] = 0 noise[-1, :, :] = 0 noise.shape = (-1,) self.coords += noise def save_vtks(self): """ Save vtk files in different folders, according to the simulation name and step. Files are saved as vtks/simname_simstep_vtk/image_00000x.vtk """ # Create the directory directory = 'vtks/%s_%d' % (self.name, self.step) self.vtk = SaveVTK(self.sim.mesh, directory) self.coords.shape = (self.total_image_num, -1) for i in range(self.total_image_num): self.vtk.save_vtk(spherical2cartesian(self.coords[i]), step=i, vtkname='m') self.coords.shape = (-1, ) def save_npys(self): """ Save npy files in different folders according to the simulation name and step Files are saved as: npys/simname_simstep/image_x.npy """ # Create directory as simname_simstep directory = 'npys/%s_%d' % (self.name, self.step) if not os.path.exists(directory): os.makedirs(directory) # Save the images with the format: 'image_{}.npy' # where {} is the image number, starting from 0 self.coords.shape = (self.total_image_num, -1) for i in range(self.total_image_num): name = os.path.join(directory, 'image_%d.npy' % i) np.save(name, spherical2cartesian(self.coords[i, :])) self.coords.shape = (-1, ) def create_integrator(self, rtol=1e-6, atol=1e-6, nsteps=10000): self.integrator = cvode.CvodeSolver(self.coords, self.sundials_rhs) self.integrator.set_options(rtol, atol) def compute_effective_field(self, y): """ Compute the effective field using Fidimag and the tangents using the NEB C code, according to the improved NEB method, developed by Henkelman and Jonsson # at: Henkelman et al., Journal of Chemical Physics 113, 22 (2000) y :: The array with all the spin components for every image, i.e. for a N images energy band and P spins system: [theta0_0, phi0_0, theta0_1, phi0_1, ... , theta0_P, phi0_P, theta1_0, phi1_0, ..., theta1_P, phi1_P, ... thetaN_0, phiN_0, ..., thetaN_P, phiN_P] """ # Every row has all the spin components of an image y.shape = (self.total_image_num, -1) for i in range(self.image_num): # Redefine the angles of the (i + 1)-th image # (see the corresponding function) check_boundary(y[i + 1]) # Set the simulation magnetisation to the (i+1)-th image # spin components self.sim.spin = spherical2cartesian(y[i + 1]) # Compute the effective field using Fidimag's methods. # (we use the time=0 since we are only using the simulation # object to get the field) # Remember that The effective field is the gradient of # the energy with respect to the generalised coordinates # i.e. the magnetisation self.sim.compute_effective_field(t=0) h = self.sim.field # Save the field components of the (i + 1)-th image # to the effective field array which is in spherical coords self.Heff[i + 1, :] = cartesian2spherical_field(h, y[i + 1]) # Compute and save the total energy for this image self.energy[i + 1] = self.sim.compute_energy() # Compute the 'distance' or difference between neighbouring states # around the y[i+1] image. This is used to compute the spring force dm1 = compute_dm(y[i], y[i + 1]) dm2 = compute_dm(y[i + 1], y[i + 2]) self.springs[i] = self.spring * (dm2 - dm1) # Compute tangents using the C library (this is also used for # cartesian coordinates, we set here the total number of spin # components for spherical coords) neb_clib.compute_tangents( y, self.energy, self.tangents, self.total_image_num, 2 * self.n) # Flatten y y.shape = (-1, ) def sundials_rhs(self, time, y, ydot): """ Right hand side of the optimization scheme used to find the minimum energy path. In our case, we use a LLG kind of equation: d Y / dt = D D = -( nabla E + [nabla E * t] t ) + F_spring where Y is an image with P spins: Y = (M_0, ... , M_P) and t is the tangent vector defined according to the energy of the neighbouring images (see Henkelman et al publication) If a climbing_image index is specified, the corresponding image will be iterated without the spring force and with an inversed component along the tangent """ # Update the ODE solver self.ode_count += 1 # Compute the eff field H for every image, H = -nabla E # (derived with respect to M) self.compute_effective_field(y) # Reshape y and ydot in a matrix of total_image_num rows y.shape = (self.total_image_num, -1) ydot.shape = (self.total_image_num, -1) # Compute the total force for every image (not the extremes) # Rememeber that self.image_num = self.total_image_num - 2 # The total force is: # D = - (-nabla E + [nabla E * t] t) + F_spring # This value is different is a climbing image is specified: # D_climb = -nabla E + 2 * [nabla E * t] t for i in range(self.image_num): # The eff field has (i + 1) since it considers the extreme images h = self.Heff[i + 1] # Tangents and springs has length: total images -2 (no extremes) t = self.tangents[i] sf = self.springs[i] if not (self.climbing_image and i == self.climbing_image): h3 = h - np.dot(h, t) * t + sf * t else: h3 = h - 2 * np.dot(h, t) * t # D vector for the i-th image (no extreme images) ydot[i + 1, :] = h3[:] # Extreme images do not have dynamics ydot[0, :] = 0 ydot[-1, :] = 0 # Flatten the arrays y.shape = (-1,) ydot.shape = (-1,) return 0 def compute_distance(self): distance = [] ys = self.coords ys.shape = (self.total_image_num, -1) for i in range(self.total_image_num - 1): dm = compute_dm(ys[i], ys[i + 1]) distance.append(dm) ys.shape = (-1, ) self.distances = np.array(distance) def run_until(self, t): if t <= self.t: return self.integrator.run_until(t) self.coords[:] = self.integrator.y[:] m = self.coords y = self.last_m m.shape = (self.total_image_num, -1) y.shape = (self.total_image_num, -1) max_dmdt = 0 for i in range(1, self.image_num + 1): dmdt = compute_dm(y[i], m[i]) / (t - self.t) if dmdt > max_dmdt: max_dmdt = dmdt m.shape = (-1,) y.shape = (-1,) self.last_m[:] = m[:] self.t = t return max_dmdt def relax(self, dt=1e-8, stopping_dmdt=1e4, max_steps=1000, save_npy_steps=100, save_vtk_steps=100): if self.integrator is None: self.create_integrator() log.debug("Relaxation parameters: " "stopping_dmdt={} (degrees per nanosecond), " "time_step={} s, max_steps={}.".format(stopping_dmdt, dt, max_steps)) # Save the initial state i=0 self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() for i in range(max_steps): if i % save_vtk_steps == 0: self.save_vtks() if i % save_npy_steps == 0: self.save_npys() self.step += 1 cvode_dt = self.integrator.get_current_step() increment_dt = dt if cvode_dt > dt: increment_dt = cvode_dt dmdt = self.run_until(self.t + increment_dt) self.compute_distance() self.tablewriter.save() self.tablewriter_dm.save() log.debug("step: {:.3g}, step_size: {:.3g}" " and max_dmdt: {:.3g}.".format(self.step, increment_dt, dmdt)) if dmdt < stopping_dmdt: break log.info("Relaxation finished at time step = {:.4g}, " "t = {:.2g}, call rhs = {:.4g} " "and max_dmdt = {:.3g}".format(self.step, self.t, self.ode_count, dmdt)) self.save_vtks() self.save_npys()