wmaee.codes.pyiron.pyiron_NEB_task

==============================================================================

Module Name: pyiron_NEB_task.py

Description: This module provides a framework for setting up and executing NEB (Nudged Elastic Band) calculations using the pyiron framework with VASP. It includes classes and methods to manage initial and final states, reorder atoms, create NEB jobs, handle image interpolation, and collect and fit energy data.

WARNING: It assumes remote and manual execution of VASP (on the cluster), it 
only handles transfer of the input/output files, not the execution (as should 
be ideally the case with pyiron).

Dependencies: - pyiron_atomistics - pyiron_base - NumPy - SciPy

Usage: from NEB import NEB # Create a NEB object, set initial and final states, and run the NEB calculation neb = NEB(pr, 'NEB_Calculation') neb.set_initial_state(initial_job) neb.set_final_state(final_job) neb_job = neb.create_neb_job(job_name='neb', nImages=5) neb_job.write_and_transfer_to_remote() # run the job manually on HPC neb_job.transfer_from_remote_and_collect() # get the barrier print(job['output/neb/barrier_forward'])

==============================================================================

  1"""
  2==============================================================================
  3Module Name: pyiron_NEB_task.py
  4==============================================================================
  5Description:
  6    This module provides a framework for setting up and executing NEB (Nudged
  7    Elastic Band) calculations using the pyiron framework with VASP. It includes
  8    classes and methods to manage initial and final states, reorder atoms,
  9    create NEB jobs, handle image interpolation, and collect and fit energy data.
 10    
 11    WARNING: It assumes remote and manual execution of VASP (on the cluster), it 
 12    only handles transfer of the input/output files, not the execution (as should 
 13    be ideally the case with pyiron).
 14
 15Dependencies:
 16    - pyiron_atomistics
 17    - pyiron_base
 18    - NumPy
 19    - SciPy
 20
 21Usage:
 22    from NEB import NEB
 23    # Create a NEB object, set initial and final states, and run the NEB calculation
 24    neb = NEB(pr, 'NEB_Calculation')
 25    neb.set_initial_state(initial_job)
 26    neb.set_final_state(final_job)
 27    neb_job = neb.create_neb_job(job_name='neb', nImages=5)
 28    neb_job.write_and_transfer_to_remote()
 29    # run the job manually on HPC
 30    neb_job.transfer_from_remote_and_collect()
 31    # get the barrier
 32    print(job['output/neb/barrier_forward'])
 33
 34==============================================================================
 35"""
 36
 37__author__ = "David Holec"
 38__version__ = "0.0.1"
 39__email__ = "david.holec@unileoben.ac.ay"
 40__status__ = "Prototype"  # Options: "Development", "Production", "Prototype"
 41__date__ = "2024-08-09"
 42__license__ = "BSD 3-Clause License"
 43
 44
 45from pyiron_atomistics.vasp.vasp import Vasp as Vasp_job
 46from pyiron_atomistics.vasp.structure import read_atoms
 47from pyiron_atomistics.vasp.parser.oszicar import Oszicar
 48from pyiron_atomistics.atomistics.job.atomistic import Trajectory
 49from pyiron_atomistics.atomistics.structure.atoms import Atoms
 50from pyiron_base import state
 51from pyiron_base.jobs.job.extension.jobstatus import job_status_successful_lst
 52from glob import glob
 53import os
 54import os.path
 55from shutil import rmtree
 56import numpy as np
 57from scipy.optimize import minimize
 58from numpy.polynomial import Polynomial
 59
 60def get_species_symbols_new(self):
 61    """
 62    MONKEY PATCH to get correct order of POTCARs.
 63    
 64    Returns:
 65        numpy.ndarray: List of the symbols of the species.
 66    """
 67    sp = [self.elements[0]]
 68    for el in self.elements[1:]:
 69        if el != sp[-1]:
 70            sp.append(el)
 71    return np.array([el.Abbreviation for el in sp])
 72
 73# Patch the `get_species_symbols` method of the `Atoms` class
 74Atoms.get_species_symbols = get_species_symbols_new
 75
 76
 77def _wrap_positions(struct, wrap):
 78    """
 79    Wrap atomic positions into the unit cell.
 80
 81    Parameters:
 82        struct (Atoms): Atomic structure.
 83        wrap (float or list): Wrap value(s) for each dimension.
 84
 85    Returns:
 86        Atoms: Structure with wrapped positions.
 87    """
 88    if isinstance(wrap, float):
 89        wrap = [wrap] * 3
 90
 91    new_pos = []
 92    for pos in struct.get_scaled_positions():
 93        for i in range(3):
 94            if pos[i] > wrap[i]:
 95                pos[i] -= 1.0
 96        new_pos.append(pos)
 97
 98    struct.set_scaled_positions(new_pos)
 99    return struct
100
101
102class NEB():
103    """
104    Class to handle the setup and execution of NEB (Nudged Elastic Band) calculations.
105    """
106    
107    def __init__(self, pr, NEB_calc_name):
108        """
109        Initialize the NEB class.
110
111        Parameters:
112            pr (Project): Pyiron project object.
113            NEB_calc_name (str): Name of the NEB calculation.
114        """
115        self.pr = pr.create_group(NEB_calc_name)
116        self.initial = None
117        
118    def add_initial_state(self, initial_job, delete_existing=False, wrap=0.98):
119        """
120        Add the initial state for the NEB calculation.
121
122        Parameters:
123            initial_job (Vasp_job): Initial state as a VASP job.
124            delete_existing (bool, optional): Whether to delete existing files.
125            wrap (float, optional): Wrap value for the atomic positions.
126        
127        Raises:
128            TypeError: If `initial_job` is not of type `Vasp_job`.
129        """
130        if not isinstance(initial_job, Vasp_job):
131            raise TypeError("Initial job must be a VASP job (pyiron_atomistics.vasp.vasp.Vasp)")
132        
133        self.initial_job = initial_job
134        self.initial = initial_job.get_structure()
135        if wrap is not False:
136            self.initial = _wrap_positions(initial_job.get_structure(), wrap)
137        
138        # Setup symbolic links and directory structure
139        rel_path_to_initial = os.path.relpath(self.initial_job.path, self.pr.path)
140        cwd = os.getcwd()
141        os.chdir(self.pr.path)
142        if delete_existing:
143            try:
144                os.remove('initial.h5')
145                rmtree('initial_hdf5')
146            except:
147                pass
148        os.symlink(rel_path_to_initial+'.h5', 'initial.h5')
149        os.mkdir('initial_hdf5')
150        os.chdir('initial_hdf5')
151        os.symlink(os.path.join('..', rel_path_to_initial+'_hdf5', initial_job.name), 'initial', target_is_directory=True)
152        os.chdir(cwd)
153        
154    def set_initial_state(self, initial_job, wrap=0.98):
155        """
156        Set the initial state from an existing VASP job.
157
158        Parameters:
159            initial_job (Vasp_job): Initial state as a VASP job.
160            wrap (float, optional): Wrap value for the atomic positions.
161        """
162        self.initial_job = initial_job
163        struct = initial_job.get_structure()
164        self.initial = struct
165        if wrap is not False:
166            self.initial = _wrap_positions(struct, wrap)
167    
168    def set_final_state(self, final_job, wrap=0.98):
169        """
170        Set the final state for the NEB calculation.
171
172        Parameters:
173            final_job (Vasp_job): Final state as a VASP job.
174            wrap (float, optional): Wrap value for the atomic positions.
175        """
176        self.final_job = final_job
177        struct = final_job.get_structure()
178        self.final = struct
179        if wrap is not False:
180            self.final = _wrap_positions(struct, wrap)
181        
182        
183    def reorder_final(self, moving_index, max_shift=0.05, verbose=True, wrap=0.98):
184        """
185        Reorder the final structure based on the initial structure.
186
187        Parameters:
188            moving_index (int): Index of the moving atom.
189            max_shift (float, optional): Maximum allowable shift for atom mapping.
190            verbose (bool, optional): If True, prints information about the atom positions.
191            wrap (float, optional): Wrap value for the atomic positions.
192        
193        Raises:
194            Exception: If there are issues with the atom mapping.
195        """
196        
197        initial = self.initial
198        final = self.final
199        if wrap is not False:
200            initial = _wrap_positions(initial, wrap)
201            final = _wrap_positions(final, wrap)
202            
203        mapped = [False for _ in range(len(final))]
204        mapping = [None for _ in range(len(initial))]
205        
206        for iat, at in enumerate(initial):
207            if iat == moving_index and verbose:
208                print('position in the initial structure:', at.position)
209            else:
210                nn = final.get_neighborhood(at.position, cutoff_radius=max_shift)
211                if len(nn.distances) != 1:
212                    raise Exception(f"Problem with mapping: atom {iat} in the initial structure was identified with {len(nn.distances)} images")
213                elif mapped[nn.indices[0]]:
214                    raise Exception(f"Problem with mapping: atom {iat} in the initial should be mapped to {nn.indices[0]} in the final structure but this atom has already been mapped.")
215                else:
216                    mapped[nn.indices[0]] = True
217                    mapping[iat] = nn.indices[0]
218        
219        unmapped = [i for i in range(len(final)) if not mapped[i]]
220        if len(unmapped) != 1:
221            raise Exception(f"Problem with mapping: only 1 atom must remain after mapping! Unmapped atoms: {unmapped}")
222        
223        mapping[moving_index] = unmapped[0]
224        if verbose:
225            print('position in the final structure:', self.final.positions[unmapped[0]])
226        
227        new_positions = [final.positions[mapping[i]] for i in range(len(initial))]
228        new_elements = [final.elements[mapping[i]] for i in range(len(initial))]
229        new_final = self.pr.create.structure.atoms(
230            cell=final.cell, 
231            positions=new_positions, 
232            elements=new_elements
233        )
234        self.final = new_final
235        
236    def create_neb_job(self, job_name='neb', nImages=3):
237        """
238        Create a NEB job.
239
240        Parameters:
241            job_name (str, optional): Name of the NEB job.
242            nImages (int, optional): Number of images for the NEB calculation.
243        
244        Returns:
245            NEBjob: Created NEB job object.
246        """
247        try:
248            job = self.pr[job_name]
249        except:
250            job = self.pr.create_job(job_type=self.pr.job_type.Vasp, job_name=job_name)
251            job.__class__ = NEBjob
252            job.structure = self.initial         
253            job.set_kpoints(mesh=[3, 3, 3], scheme='GC')
254            job.calc_minimize(
255                electronic_steps=60,
256                retain_charge_density=False,
257            )
258            job.input.incar['ISIF'] = 2
259            job.input.incar['ENCUT'] = 520  # from MP
260            job.input.incar['IMAGES'] = nImages - 2
261            job.input.incar['IBRION'] = 1
262            job.input.incar['NFREE'] = 2
263            job.save()
264            
265            job['input'].create_group('neb')            
266            job['input/neb'].put('nImages', nImages)
267            job['input/neb'].put('initial_image', self.initial)
268            job['input/neb'].put('final_image', self.final)
269            job['input/neb'].put('initial_energy', self.initial_job['output/generic/energy_pot'][-1])
270            job['input/neb'].put('final_energy', self.final_job['output/generic/energy_pot'][-1])
271            job._create_images()
272            job.structure = job['input/neb/initial_image'].to_object()
273            job.write_input()
274        else:
275            state.logger.warning(f'Job `{job_name}` exists. Loading from database instead of creating it!')
276        return job
277
278
279class NEBjob(Vasp_job):
280    """
281    Class for handling specific NEB (Nudged Elastic Band) job operations.
282    """
283    
284
285    
286    def _create_images(self, wrap=0.98):
287        """
288        Create interpolated images between the initial and final states.
289
290        Parameters:
291            wrap (float, optional): Wrap value for the atomic positions.
292        """
293        cells = []
294        positions = []
295        initial = _wrap_positions(self['input/neb/initial_image'].to_object(), wrap)
296        final = _wrap_positions(self['input/neb/final_image'].to_object(), wrap)
297        nImages = self['input/neb/nImages']
298        
299        for x in np.linspace(0, 1, nImages):
300            cells.append(initial.cell * (1 - x) + final.cell * x)
301            positions.append(initial.positions * (1 - x) + final.positions * x)
302        
303        self['input/neb'].put('initial_cells', cells)
304        self['input/neb'].put('initial_positions', positions)
305        
306    def get_initial_images(self):
307        """
308        Retrieve the initial interpolated images.
309
310        Returns:
311            Trajectory: Trajectory object containing the initial images.
312        """
313        initial = self['input/neb/initial_image'].to_object()
314        positions = self['input/neb/initial_positions']
315        cells = self['input/neb/initial_cells']
316        return Trajectory(positions, initial, cells=cells)
317    
318    def get_final_images(self):
319        """
320        Retrieve the final interpolated images after relaxation.
321
322        Returns:
323            Trajectory: Trajectory object containing the final images.
324        """
325        initial = self['input/neb/initial_image'].to_object()
326        positions = self['output/neb/final_positions']
327        cells = self['output/neb/final_cells']
328        return Trajectory(positions, initial, cells=cells)
329    
330    def _write_images(self):
331        """
332        Write the interpolated images to the appropriate directories.
333        """
334        path = self.path + '_hdf5/' + self.job_name + '/'
335        nImages = self['input/neb/nImages']
336        initial_structs = self.get_initial_images()
337        
338        for i in range(nImages):
339            os.chdir(path)
340            case = f'{i:02d}'
341            if not os.path.exists(case):
342                os.mkdir(case)
343            os.chdir(case)
344            struct = initial_structs[i]
345            struct.write('POSCAR', format='vasp')
346    
347    def write_and_transfer_to_remote(self):
348        """
349        Write the input files and transfer them to the remote cluster.
350
351        Warnings:
352            If the job is already marked as finished, a warning is issued and no action is taken.
353        """
354        if self.status not in job_status_successful_lst:
355            self._write_images()
356            self.write_input()
357            
358            filename = state.queue_adapter.convert_path_to_remote(
359                path=self.project_hdf5.file_name
360            )
361            working_directory = state.queue_adapter.convert_path_to_remote(
362                path=self.working_directory
363            )
364            for filename in glob(self.project_hdf5.path + '_hdf5/**', recursive=True):
365                if os.path.isfile(filename):                
366                    state.queue_adapter.transfer_file_to_remote(
367                        file=filename, transfer_back=False
368                    )
369        else:
370            state.logger.warning(f'Job `{self.job_name}` is marked as finished, uploading doesn\'t make sense')
371            
372    def collect_data(self, wrap=0.98):
373        """
374        Collect the data from the NEB calculation.
375
376        Parameters:
377            wrap (float or bool, optional): Wrap value for atomic positions. Set to `False` to disable wrapping.
378        """
379        path = self.path + '_hdf5/' + self.job_name + '/'
380        nImages = self['input/neb/nImages']
381        initial = self['input/neb/initial_image'].to_object()
382        final = self['input/neb/final_image'].to_object()
383        
384        if wrap is not False:
385            initial = _wrap_positions(initial, wrap)
386            final = _wrap_positions(final, wrap)
387        
388        cells = [initial.cell]
389        positions = [initial.positions]
390        energy_pot = []
391        
392        for i in range(1, nImages - 1):
393            case = f'{i:02d}'
394            out = Oszicar()
395            out.from_file(filename=path + case + '/OSZICAR')
396            energy_pot.append(out.parse_dict['energy_pot'])
397            struct = read_atoms(filename=path + case + '/CONTCAR')
398            if wrap is not False:
399                struct = _wrap_positions(struct, wrap)
400            cells.append(struct.cell)
401            positions.append(struct.positions)
402        
403        relaxation_steps = len(out.parse_dict['energy_pot'])
404        energy_pot = np.array(
405            [[self['input/neb/initial_energy']] * relaxation_steps] + 
406            energy_pot +
407            [[self['input/neb/final_energy']] * relaxation_steps]
408        ).T
409        
410        cells.append(final.cell)
411        positions.append(final.positions)
412        
413        self['output'].create_group('neb')
414        self['output/neb'].put('energy_pot', energy_pot)
415        self['output/neb'].put('final_cells', cells)
416        self['output/neb'].put('final_positions', positions)
417        ene = energy_pot[-1]
418        self['output/neb'].put('barrier_forward', max(ene) - ene[0])
419        self['output/neb'].put('barrier_backward', max(ene) - ene[-1])
420        self._polynomial_fit_with_derivative_constraints(n=nImages + 1)
421        
422    def transfer_from_remote_and_collect(self):
423        """
424        Transfer files from the remote cluster and collect data locally.
425        """
426        self.transfer_from_remote()
427        self.status.collect = True
428        self.collect_data()
429        self.status.finished = True
430    
431    def _polynomial_fit_with_derivative_constraints(self, n=4):
432        """
433        Fit a polynomial to the NEB energy profile with derivative constraints.
434
435        The polynomial is constrained such that its derivatives at x=0 and x=1 are zero.
436
437        Parameters:
438            n (int, optional): The order of the polynomial.
439
440        Returns:
441            Polynomial: The fitted polynomial.
442        """
443        y = self['output/neb/energy_pot'][-1]
444        x = np.linspace(0, 1, len(y))
445        
446        def poly_val_and_deriv(coeffs, x):
447            """
448            Evaluate the polynomial and its derivative.
449
450            Parameters:
451                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
452                x (float or numpy.ndarray): Point(s) at which to evaluate.
453
454            Returns:
455                tuple: Tuple containing the polynomial value and its derivative.
456            """
457            p = Polynomial(coeffs)
458            dp = p.deriv()
459            return p(x), dp(x)
460
461        def objective(coeffs, x, y):
462            """
463            Objective function to minimize.
464
465            Parameters:
466                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
467                x (float or numpy.ndarray): x-values of the data points.
468                y (float or numpy.ndarray): y-values of the data points.
469
470            Returns:
471                float: Sum of squared residuals.
472            """
473            p_vals, _ = poly_val_and_deriv(coeffs, x)
474            return np.sum((y - p_vals) ** 2)
475
476        def constraint_deriv_0(coeffs):
477            """
478            Constraint for the derivative at x=0 to be zero.
479
480            Parameters:
481                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
482
483            Returns:
484                float: Derivative at x=0.
485            """
486            _, dp0 = poly_val_and_deriv(coeffs, 0)
487            return dp0
488
489        def constraint_deriv_1(coeffs):
490            """
491            Constraint for the derivative at x=1 to be zero.
492
493            Parameters:
494                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
495
496            Returns:
497                float: Derivative at x=1.
498            """
499            _, dp1 = poly_val_and_deriv(coeffs, 1)
500            return dp1
501
502        def constraint_val_0(coeffs):
503            """
504            Constraint for the polynomial value at x=0 to match the first data point.
505
506            Parameters:
507                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
508
509            Returns:
510                float: Difference between polynomial value at x=0 and y[0].
511            """
512            p0, _ = poly_val_and_deriv(coeffs, 0)
513            return p0 - y[0]
514
515        def constraint_val_1(coeffs):
516            """
517            Constraint for the polynomial value at x=1 to match the last data point.
518
519            Parameters:
520                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
521
522            Returns:
523                float: Difference between polynomial value at x=1 and y[-1].
524            """
525            p1, _ = poly_val_and_deriv(coeffs, 1)
526            return p1 - y[-1]
527
528        initial_guess = np.ones(n + 1)
529
530        def constraint_max_val(coeffs):
531            """
532            Constraint for the polynomial maximum to match the maximum y value.
533
534            Parameters:
535                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
536
537            Returns:
538                float: Difference between maximum polynomial value and maximum y value.
539            """
540            p = Polynomial(coeffs)
541            x_vals = np.linspace(0.2, 0.8, 10)
542            p_vals = p(x_vals)
543            return max(p_vals) - max(y)
544
545        constraints = [
546            {'type': 'eq', 'fun': constraint_deriv_0},
547            {'type': 'eq', 'fun': constraint_deriv_1},
548            {'type': 'eq', 'fun': constraint_val_0},
549            {'type': 'eq', 'fun': constraint_val_1},
550            {'type': 'eq', 'fun': constraint_max_val}
551        ]
552
553        result = minimize(objective, initial_guess, args=(x, y), constraints=constraints)
554        optimized_coeffs = result.x
555        fitted_polynomial = Polynomial(optimized_coeffs)
556        self['output/neb'].put('fit', fitted_polynomial)
557        
558    def get_energy_fit(self):
559        """
560        Get the fitted polynomial for the NEB energy profile.
561
562        Returns:
563            Polynomial: The fitted polynomial.
564        """
565        return self['output/neb/fit']
def get_species_symbols_new(self):
61def get_species_symbols_new(self):
62    """
63    MONKEY PATCH to get correct order of POTCARs.
64    
65    Returns:
66        numpy.ndarray: List of the symbols of the species.
67    """
68    sp = [self.elements[0]]
69    for el in self.elements[1:]:
70        if el != sp[-1]:
71            sp.append(el)
72    return np.array([el.Abbreviation for el in sp])

MONKEY PATCH to get correct order of POTCARs.

Returns: numpy.ndarray: List of the symbols of the species.

class NEB:
103class NEB():
104    """
105    Class to handle the setup and execution of NEB (Nudged Elastic Band) calculations.
106    """
107    
108    def __init__(self, pr, NEB_calc_name):
109        """
110        Initialize the NEB class.
111
112        Parameters:
113            pr (Project): Pyiron project object.
114            NEB_calc_name (str): Name of the NEB calculation.
115        """
116        self.pr = pr.create_group(NEB_calc_name)
117        self.initial = None
118        
119    def add_initial_state(self, initial_job, delete_existing=False, wrap=0.98):
120        """
121        Add the initial state for the NEB calculation.
122
123        Parameters:
124            initial_job (Vasp_job): Initial state as a VASP job.
125            delete_existing (bool, optional): Whether to delete existing files.
126            wrap (float, optional): Wrap value for the atomic positions.
127        
128        Raises:
129            TypeError: If `initial_job` is not of type `Vasp_job`.
130        """
131        if not isinstance(initial_job, Vasp_job):
132            raise TypeError("Initial job must be a VASP job (pyiron_atomistics.vasp.vasp.Vasp)")
133        
134        self.initial_job = initial_job
135        self.initial = initial_job.get_structure()
136        if wrap is not False:
137            self.initial = _wrap_positions(initial_job.get_structure(), wrap)
138        
139        # Setup symbolic links and directory structure
140        rel_path_to_initial = os.path.relpath(self.initial_job.path, self.pr.path)
141        cwd = os.getcwd()
142        os.chdir(self.pr.path)
143        if delete_existing:
144            try:
145                os.remove('initial.h5')
146                rmtree('initial_hdf5')
147            except:
148                pass
149        os.symlink(rel_path_to_initial+'.h5', 'initial.h5')
150        os.mkdir('initial_hdf5')
151        os.chdir('initial_hdf5')
152        os.symlink(os.path.join('..', rel_path_to_initial+'_hdf5', initial_job.name), 'initial', target_is_directory=True)
153        os.chdir(cwd)
154        
155    def set_initial_state(self, initial_job, wrap=0.98):
156        """
157        Set the initial state from an existing VASP job.
158
159        Parameters:
160            initial_job (Vasp_job): Initial state as a VASP job.
161            wrap (float, optional): Wrap value for the atomic positions.
162        """
163        self.initial_job = initial_job
164        struct = initial_job.get_structure()
165        self.initial = struct
166        if wrap is not False:
167            self.initial = _wrap_positions(struct, wrap)
168    
169    def set_final_state(self, final_job, wrap=0.98):
170        """
171        Set the final state for the NEB calculation.
172
173        Parameters:
174            final_job (Vasp_job): Final state as a VASP job.
175            wrap (float, optional): Wrap value for the atomic positions.
176        """
177        self.final_job = final_job
178        struct = final_job.get_structure()
179        self.final = struct
180        if wrap is not False:
181            self.final = _wrap_positions(struct, wrap)
182        
183        
184    def reorder_final(self, moving_index, max_shift=0.05, verbose=True, wrap=0.98):
185        """
186        Reorder the final structure based on the initial structure.
187
188        Parameters:
189            moving_index (int): Index of the moving atom.
190            max_shift (float, optional): Maximum allowable shift for atom mapping.
191            verbose (bool, optional): If True, prints information about the atom positions.
192            wrap (float, optional): Wrap value for the atomic positions.
193        
194        Raises:
195            Exception: If there are issues with the atom mapping.
196        """
197        
198        initial = self.initial
199        final = self.final
200        if wrap is not False:
201            initial = _wrap_positions(initial, wrap)
202            final = _wrap_positions(final, wrap)
203            
204        mapped = [False for _ in range(len(final))]
205        mapping = [None for _ in range(len(initial))]
206        
207        for iat, at in enumerate(initial):
208            if iat == moving_index and verbose:
209                print('position in the initial structure:', at.position)
210            else:
211                nn = final.get_neighborhood(at.position, cutoff_radius=max_shift)
212                if len(nn.distances) != 1:
213                    raise Exception(f"Problem with mapping: atom {iat} in the initial structure was identified with {len(nn.distances)} images")
214                elif mapped[nn.indices[0]]:
215                    raise Exception(f"Problem with mapping: atom {iat} in the initial should be mapped to {nn.indices[0]} in the final structure but this atom has already been mapped.")
216                else:
217                    mapped[nn.indices[0]] = True
218                    mapping[iat] = nn.indices[0]
219        
220        unmapped = [i for i in range(len(final)) if not mapped[i]]
221        if len(unmapped) != 1:
222            raise Exception(f"Problem with mapping: only 1 atom must remain after mapping! Unmapped atoms: {unmapped}")
223        
224        mapping[moving_index] = unmapped[0]
225        if verbose:
226            print('position in the final structure:', self.final.positions[unmapped[0]])
227        
228        new_positions = [final.positions[mapping[i]] for i in range(len(initial))]
229        new_elements = [final.elements[mapping[i]] for i in range(len(initial))]
230        new_final = self.pr.create.structure.atoms(
231            cell=final.cell, 
232            positions=new_positions, 
233            elements=new_elements
234        )
235        self.final = new_final
236        
237    def create_neb_job(self, job_name='neb', nImages=3):
238        """
239        Create a NEB job.
240
241        Parameters:
242            job_name (str, optional): Name of the NEB job.
243            nImages (int, optional): Number of images for the NEB calculation.
244        
245        Returns:
246            NEBjob: Created NEB job object.
247        """
248        try:
249            job = self.pr[job_name]
250        except:
251            job = self.pr.create_job(job_type=self.pr.job_type.Vasp, job_name=job_name)
252            job.__class__ = NEBjob
253            job.structure = self.initial         
254            job.set_kpoints(mesh=[3, 3, 3], scheme='GC')
255            job.calc_minimize(
256                electronic_steps=60,
257                retain_charge_density=False,
258            )
259            job.input.incar['ISIF'] = 2
260            job.input.incar['ENCUT'] = 520  # from MP
261            job.input.incar['IMAGES'] = nImages - 2
262            job.input.incar['IBRION'] = 1
263            job.input.incar['NFREE'] = 2
264            job.save()
265            
266            job['input'].create_group('neb')            
267            job['input/neb'].put('nImages', nImages)
268            job['input/neb'].put('initial_image', self.initial)
269            job['input/neb'].put('final_image', self.final)
270            job['input/neb'].put('initial_energy', self.initial_job['output/generic/energy_pot'][-1])
271            job['input/neb'].put('final_energy', self.final_job['output/generic/energy_pot'][-1])
272            job._create_images()
273            job.structure = job['input/neb/initial_image'].to_object()
274            job.write_input()
275        else:
276            state.logger.warning(f'Job `{job_name}` exists. Loading from database instead of creating it!')
277        return job

Class to handle the setup and execution of NEB (Nudged Elastic Band) calculations.

NEB(pr, NEB_calc_name)
108    def __init__(self, pr, NEB_calc_name):
109        """
110        Initialize the NEB class.
111
112        Parameters:
113            pr (Project): Pyiron project object.
114            NEB_calc_name (str): Name of the NEB calculation.
115        """
116        self.pr = pr.create_group(NEB_calc_name)
117        self.initial = None

Initialize the NEB class.

Parameters: pr (Project): Pyiron project object. NEB_calc_name (str): Name of the NEB calculation.

pr
initial
def add_initial_state(self, initial_job, delete_existing=False, wrap=0.98):
119    def add_initial_state(self, initial_job, delete_existing=False, wrap=0.98):
120        """
121        Add the initial state for the NEB calculation.
122
123        Parameters:
124            initial_job (Vasp_job): Initial state as a VASP job.
125            delete_existing (bool, optional): Whether to delete existing files.
126            wrap (float, optional): Wrap value for the atomic positions.
127        
128        Raises:
129            TypeError: If `initial_job` is not of type `Vasp_job`.
130        """
131        if not isinstance(initial_job, Vasp_job):
132            raise TypeError("Initial job must be a VASP job (pyiron_atomistics.vasp.vasp.Vasp)")
133        
134        self.initial_job = initial_job
135        self.initial = initial_job.get_structure()
136        if wrap is not False:
137            self.initial = _wrap_positions(initial_job.get_structure(), wrap)
138        
139        # Setup symbolic links and directory structure
140        rel_path_to_initial = os.path.relpath(self.initial_job.path, self.pr.path)
141        cwd = os.getcwd()
142        os.chdir(self.pr.path)
143        if delete_existing:
144            try:
145                os.remove('initial.h5')
146                rmtree('initial_hdf5')
147            except:
148                pass
149        os.symlink(rel_path_to_initial+'.h5', 'initial.h5')
150        os.mkdir('initial_hdf5')
151        os.chdir('initial_hdf5')
152        os.symlink(os.path.join('..', rel_path_to_initial+'_hdf5', initial_job.name), 'initial', target_is_directory=True)
153        os.chdir(cwd)

Add the initial state for the NEB calculation.

Parameters: initial_job (Vasp_job): Initial state as a VASP job. delete_existing (bool, optional): Whether to delete existing files. wrap (float, optional): Wrap value for the atomic positions.

Raises: TypeError: If initial_job is not of type Vasp_job.

def set_initial_state(self, initial_job, wrap=0.98):
155    def set_initial_state(self, initial_job, wrap=0.98):
156        """
157        Set the initial state from an existing VASP job.
158
159        Parameters:
160            initial_job (Vasp_job): Initial state as a VASP job.
161            wrap (float, optional): Wrap value for the atomic positions.
162        """
163        self.initial_job = initial_job
164        struct = initial_job.get_structure()
165        self.initial = struct
166        if wrap is not False:
167            self.initial = _wrap_positions(struct, wrap)

Set the initial state from an existing VASP job.

Parameters: initial_job (Vasp_job): Initial state as a VASP job. wrap (float, optional): Wrap value for the atomic positions.

def set_final_state(self, final_job, wrap=0.98):
169    def set_final_state(self, final_job, wrap=0.98):
170        """
171        Set the final state for the NEB calculation.
172
173        Parameters:
174            final_job (Vasp_job): Final state as a VASP job.
175            wrap (float, optional): Wrap value for the atomic positions.
176        """
177        self.final_job = final_job
178        struct = final_job.get_structure()
179        self.final = struct
180        if wrap is not False:
181            self.final = _wrap_positions(struct, wrap)

Set the final state for the NEB calculation.

Parameters: final_job (Vasp_job): Final state as a VASP job. wrap (float, optional): Wrap value for the atomic positions.

def reorder_final(self, moving_index, max_shift=0.05, verbose=True, wrap=0.98):
184    def reorder_final(self, moving_index, max_shift=0.05, verbose=True, wrap=0.98):
185        """
186        Reorder the final structure based on the initial structure.
187
188        Parameters:
189            moving_index (int): Index of the moving atom.
190            max_shift (float, optional): Maximum allowable shift for atom mapping.
191            verbose (bool, optional): If True, prints information about the atom positions.
192            wrap (float, optional): Wrap value for the atomic positions.
193        
194        Raises:
195            Exception: If there are issues with the atom mapping.
196        """
197        
198        initial = self.initial
199        final = self.final
200        if wrap is not False:
201            initial = _wrap_positions(initial, wrap)
202            final = _wrap_positions(final, wrap)
203            
204        mapped = [False for _ in range(len(final))]
205        mapping = [None for _ in range(len(initial))]
206        
207        for iat, at in enumerate(initial):
208            if iat == moving_index and verbose:
209                print('position in the initial structure:', at.position)
210            else:
211                nn = final.get_neighborhood(at.position, cutoff_radius=max_shift)
212                if len(nn.distances) != 1:
213                    raise Exception(f"Problem with mapping: atom {iat} in the initial structure was identified with {len(nn.distances)} images")
214                elif mapped[nn.indices[0]]:
215                    raise Exception(f"Problem with mapping: atom {iat} in the initial should be mapped to {nn.indices[0]} in the final structure but this atom has already been mapped.")
216                else:
217                    mapped[nn.indices[0]] = True
218                    mapping[iat] = nn.indices[0]
219        
220        unmapped = [i for i in range(len(final)) if not mapped[i]]
221        if len(unmapped) != 1:
222            raise Exception(f"Problem with mapping: only 1 atom must remain after mapping! Unmapped atoms: {unmapped}")
223        
224        mapping[moving_index] = unmapped[0]
225        if verbose:
226            print('position in the final structure:', self.final.positions[unmapped[0]])
227        
228        new_positions = [final.positions[mapping[i]] for i in range(len(initial))]
229        new_elements = [final.elements[mapping[i]] for i in range(len(initial))]
230        new_final = self.pr.create.structure.atoms(
231            cell=final.cell, 
232            positions=new_positions, 
233            elements=new_elements
234        )
235        self.final = new_final

Reorder the final structure based on the initial structure.

Parameters: moving_index (int): Index of the moving atom. max_shift (float, optional): Maximum allowable shift for atom mapping. verbose (bool, optional): If True, prints information about the atom positions. wrap (float, optional): Wrap value for the atomic positions.

Raises: Exception: If there are issues with the atom mapping.

def create_neb_job(self, job_name='neb', nImages=3):
237    def create_neb_job(self, job_name='neb', nImages=3):
238        """
239        Create a NEB job.
240
241        Parameters:
242            job_name (str, optional): Name of the NEB job.
243            nImages (int, optional): Number of images for the NEB calculation.
244        
245        Returns:
246            NEBjob: Created NEB job object.
247        """
248        try:
249            job = self.pr[job_name]
250        except:
251            job = self.pr.create_job(job_type=self.pr.job_type.Vasp, job_name=job_name)
252            job.__class__ = NEBjob
253            job.structure = self.initial         
254            job.set_kpoints(mesh=[3, 3, 3], scheme='GC')
255            job.calc_minimize(
256                electronic_steps=60,
257                retain_charge_density=False,
258            )
259            job.input.incar['ISIF'] = 2
260            job.input.incar['ENCUT'] = 520  # from MP
261            job.input.incar['IMAGES'] = nImages - 2
262            job.input.incar['IBRION'] = 1
263            job.input.incar['NFREE'] = 2
264            job.save()
265            
266            job['input'].create_group('neb')            
267            job['input/neb'].put('nImages', nImages)
268            job['input/neb'].put('initial_image', self.initial)
269            job['input/neb'].put('final_image', self.final)
270            job['input/neb'].put('initial_energy', self.initial_job['output/generic/energy_pot'][-1])
271            job['input/neb'].put('final_energy', self.final_job['output/generic/energy_pot'][-1])
272            job._create_images()
273            job.structure = job['input/neb/initial_image'].to_object()
274            job.write_input()
275        else:
276            state.logger.warning(f'Job `{job_name}` exists. Loading from database instead of creating it!')
277        return job

Create a NEB job.

Parameters: job_name (str, optional): Name of the NEB job. nImages (int, optional): Number of images for the NEB calculation.

Returns: NEBjob: Created NEB job object.

class NEBjob(pyiron_atomistics.vasp.vasp.Vasp):
280class NEBjob(Vasp_job):
281    """
282    Class for handling specific NEB (Nudged Elastic Band) job operations.
283    """
284    
285
286    
287    def _create_images(self, wrap=0.98):
288        """
289        Create interpolated images between the initial and final states.
290
291        Parameters:
292            wrap (float, optional): Wrap value for the atomic positions.
293        """
294        cells = []
295        positions = []
296        initial = _wrap_positions(self['input/neb/initial_image'].to_object(), wrap)
297        final = _wrap_positions(self['input/neb/final_image'].to_object(), wrap)
298        nImages = self['input/neb/nImages']
299        
300        for x in np.linspace(0, 1, nImages):
301            cells.append(initial.cell * (1 - x) + final.cell * x)
302            positions.append(initial.positions * (1 - x) + final.positions * x)
303        
304        self['input/neb'].put('initial_cells', cells)
305        self['input/neb'].put('initial_positions', positions)
306        
307    def get_initial_images(self):
308        """
309        Retrieve the initial interpolated images.
310
311        Returns:
312            Trajectory: Trajectory object containing the initial images.
313        """
314        initial = self['input/neb/initial_image'].to_object()
315        positions = self['input/neb/initial_positions']
316        cells = self['input/neb/initial_cells']
317        return Trajectory(positions, initial, cells=cells)
318    
319    def get_final_images(self):
320        """
321        Retrieve the final interpolated images after relaxation.
322
323        Returns:
324            Trajectory: Trajectory object containing the final images.
325        """
326        initial = self['input/neb/initial_image'].to_object()
327        positions = self['output/neb/final_positions']
328        cells = self['output/neb/final_cells']
329        return Trajectory(positions, initial, cells=cells)
330    
331    def _write_images(self):
332        """
333        Write the interpolated images to the appropriate directories.
334        """
335        path = self.path + '_hdf5/' + self.job_name + '/'
336        nImages = self['input/neb/nImages']
337        initial_structs = self.get_initial_images()
338        
339        for i in range(nImages):
340            os.chdir(path)
341            case = f'{i:02d}'
342            if not os.path.exists(case):
343                os.mkdir(case)
344            os.chdir(case)
345            struct = initial_structs[i]
346            struct.write('POSCAR', format='vasp')
347    
348    def write_and_transfer_to_remote(self):
349        """
350        Write the input files and transfer them to the remote cluster.
351
352        Warnings:
353            If the job is already marked as finished, a warning is issued and no action is taken.
354        """
355        if self.status not in job_status_successful_lst:
356            self._write_images()
357            self.write_input()
358            
359            filename = state.queue_adapter.convert_path_to_remote(
360                path=self.project_hdf5.file_name
361            )
362            working_directory = state.queue_adapter.convert_path_to_remote(
363                path=self.working_directory
364            )
365            for filename in glob(self.project_hdf5.path + '_hdf5/**', recursive=True):
366                if os.path.isfile(filename):                
367                    state.queue_adapter.transfer_file_to_remote(
368                        file=filename, transfer_back=False
369                    )
370        else:
371            state.logger.warning(f'Job `{self.job_name}` is marked as finished, uploading doesn\'t make sense')
372            
373    def collect_data(self, wrap=0.98):
374        """
375        Collect the data from the NEB calculation.
376
377        Parameters:
378            wrap (float or bool, optional): Wrap value for atomic positions. Set to `False` to disable wrapping.
379        """
380        path = self.path + '_hdf5/' + self.job_name + '/'
381        nImages = self['input/neb/nImages']
382        initial = self['input/neb/initial_image'].to_object()
383        final = self['input/neb/final_image'].to_object()
384        
385        if wrap is not False:
386            initial = _wrap_positions(initial, wrap)
387            final = _wrap_positions(final, wrap)
388        
389        cells = [initial.cell]
390        positions = [initial.positions]
391        energy_pot = []
392        
393        for i in range(1, nImages - 1):
394            case = f'{i:02d}'
395            out = Oszicar()
396            out.from_file(filename=path + case + '/OSZICAR')
397            energy_pot.append(out.parse_dict['energy_pot'])
398            struct = read_atoms(filename=path + case + '/CONTCAR')
399            if wrap is not False:
400                struct = _wrap_positions(struct, wrap)
401            cells.append(struct.cell)
402            positions.append(struct.positions)
403        
404        relaxation_steps = len(out.parse_dict['energy_pot'])
405        energy_pot = np.array(
406            [[self['input/neb/initial_energy']] * relaxation_steps] + 
407            energy_pot +
408            [[self['input/neb/final_energy']] * relaxation_steps]
409        ).T
410        
411        cells.append(final.cell)
412        positions.append(final.positions)
413        
414        self['output'].create_group('neb')
415        self['output/neb'].put('energy_pot', energy_pot)
416        self['output/neb'].put('final_cells', cells)
417        self['output/neb'].put('final_positions', positions)
418        ene = energy_pot[-1]
419        self['output/neb'].put('barrier_forward', max(ene) - ene[0])
420        self['output/neb'].put('barrier_backward', max(ene) - ene[-1])
421        self._polynomial_fit_with_derivative_constraints(n=nImages + 1)
422        
423    def transfer_from_remote_and_collect(self):
424        """
425        Transfer files from the remote cluster and collect data locally.
426        """
427        self.transfer_from_remote()
428        self.status.collect = True
429        self.collect_data()
430        self.status.finished = True
431    
432    def _polynomial_fit_with_derivative_constraints(self, n=4):
433        """
434        Fit a polynomial to the NEB energy profile with derivative constraints.
435
436        The polynomial is constrained such that its derivatives at x=0 and x=1 are zero.
437
438        Parameters:
439            n (int, optional): The order of the polynomial.
440
441        Returns:
442            Polynomial: The fitted polynomial.
443        """
444        y = self['output/neb/energy_pot'][-1]
445        x = np.linspace(0, 1, len(y))
446        
447        def poly_val_and_deriv(coeffs, x):
448            """
449            Evaluate the polynomial and its derivative.
450
451            Parameters:
452                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
453                x (float or numpy.ndarray): Point(s) at which to evaluate.
454
455            Returns:
456                tuple: Tuple containing the polynomial value and its derivative.
457            """
458            p = Polynomial(coeffs)
459            dp = p.deriv()
460            return p(x), dp(x)
461
462        def objective(coeffs, x, y):
463            """
464            Objective function to minimize.
465
466            Parameters:
467                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
468                x (float or numpy.ndarray): x-values of the data points.
469                y (float or numpy.ndarray): y-values of the data points.
470
471            Returns:
472                float: Sum of squared residuals.
473            """
474            p_vals, _ = poly_val_and_deriv(coeffs, x)
475            return np.sum((y - p_vals) ** 2)
476
477        def constraint_deriv_0(coeffs):
478            """
479            Constraint for the derivative at x=0 to be zero.
480
481            Parameters:
482                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
483
484            Returns:
485                float: Derivative at x=0.
486            """
487            _, dp0 = poly_val_and_deriv(coeffs, 0)
488            return dp0
489
490        def constraint_deriv_1(coeffs):
491            """
492            Constraint for the derivative at x=1 to be zero.
493
494            Parameters:
495                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
496
497            Returns:
498                float: Derivative at x=1.
499            """
500            _, dp1 = poly_val_and_deriv(coeffs, 1)
501            return dp1
502
503        def constraint_val_0(coeffs):
504            """
505            Constraint for the polynomial value at x=0 to match the first data point.
506
507            Parameters:
508                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
509
510            Returns:
511                float: Difference between polynomial value at x=0 and y[0].
512            """
513            p0, _ = poly_val_and_deriv(coeffs, 0)
514            return p0 - y[0]
515
516        def constraint_val_1(coeffs):
517            """
518            Constraint for the polynomial value at x=1 to match the last data point.
519
520            Parameters:
521                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
522
523            Returns:
524                float: Difference between polynomial value at x=1 and y[-1].
525            """
526            p1, _ = poly_val_and_deriv(coeffs, 1)
527            return p1 - y[-1]
528
529        initial_guess = np.ones(n + 1)
530
531        def constraint_max_val(coeffs):
532            """
533            Constraint for the polynomial maximum to match the maximum y value.
534
535            Parameters:
536                coeffs (list or numpy.ndarray): Coefficients of the polynomial.
537
538            Returns:
539                float: Difference between maximum polynomial value and maximum y value.
540            """
541            p = Polynomial(coeffs)
542            x_vals = np.linspace(0.2, 0.8, 10)
543            p_vals = p(x_vals)
544            return max(p_vals) - max(y)
545
546        constraints = [
547            {'type': 'eq', 'fun': constraint_deriv_0},
548            {'type': 'eq', 'fun': constraint_deriv_1},
549            {'type': 'eq', 'fun': constraint_val_0},
550            {'type': 'eq', 'fun': constraint_val_1},
551            {'type': 'eq', 'fun': constraint_max_val}
552        ]
553
554        result = minimize(objective, initial_guess, args=(x, y), constraints=constraints)
555        optimized_coeffs = result.x
556        fitted_polynomial = Polynomial(optimized_coeffs)
557        self['output/neb'].put('fit', fitted_polynomial)
558        
559    def get_energy_fit(self):
560        """
561        Get the fitted polynomial for the NEB energy profile.
562
563        Returns:
564            Polynomial: The fitted polynomial.
565        """
566        return self['output/neb/fit']

Class for handling specific NEB (Nudged Elastic Band) job operations.

def get_initial_images(self):
307    def get_initial_images(self):
308        """
309        Retrieve the initial interpolated images.
310
311        Returns:
312            Trajectory: Trajectory object containing the initial images.
313        """
314        initial = self['input/neb/initial_image'].to_object()
315        positions = self['input/neb/initial_positions']
316        cells = self['input/neb/initial_cells']
317        return Trajectory(positions, initial, cells=cells)

Retrieve the initial interpolated images.

Returns: Trajectory: Trajectory object containing the initial images.

def get_final_images(self):
319    def get_final_images(self):
320        """
321        Retrieve the final interpolated images after relaxation.
322
323        Returns:
324            Trajectory: Trajectory object containing the final images.
325        """
326        initial = self['input/neb/initial_image'].to_object()
327        positions = self['output/neb/final_positions']
328        cells = self['output/neb/final_cells']
329        return Trajectory(positions, initial, cells=cells)

Retrieve the final interpolated images after relaxation.

Returns: Trajectory: Trajectory object containing the final images.

def write_and_transfer_to_remote(self):
348    def write_and_transfer_to_remote(self):
349        """
350        Write the input files and transfer them to the remote cluster.
351
352        Warnings:
353            If the job is already marked as finished, a warning is issued and no action is taken.
354        """
355        if self.status not in job_status_successful_lst:
356            self._write_images()
357            self.write_input()
358            
359            filename = state.queue_adapter.convert_path_to_remote(
360                path=self.project_hdf5.file_name
361            )
362            working_directory = state.queue_adapter.convert_path_to_remote(
363                path=self.working_directory
364            )
365            for filename in glob(self.project_hdf5.path + '_hdf5/**', recursive=True):
366                if os.path.isfile(filename):                
367                    state.queue_adapter.transfer_file_to_remote(
368                        file=filename, transfer_back=False
369                    )
370        else:
371            state.logger.warning(f'Job `{self.job_name}` is marked as finished, uploading doesn\'t make sense')

Write the input files and transfer them to the remote cluster.

Warnings: If the job is already marked as finished, a warning is issued and no action is taken.

def collect_data(self, wrap=0.98):
373    def collect_data(self, wrap=0.98):
374        """
375        Collect the data from the NEB calculation.
376
377        Parameters:
378            wrap (float or bool, optional): Wrap value for atomic positions. Set to `False` to disable wrapping.
379        """
380        path = self.path + '_hdf5/' + self.job_name + '/'
381        nImages = self['input/neb/nImages']
382        initial = self['input/neb/initial_image'].to_object()
383        final = self['input/neb/final_image'].to_object()
384        
385        if wrap is not False:
386            initial = _wrap_positions(initial, wrap)
387            final = _wrap_positions(final, wrap)
388        
389        cells = [initial.cell]
390        positions = [initial.positions]
391        energy_pot = []
392        
393        for i in range(1, nImages - 1):
394            case = f'{i:02d}'
395            out = Oszicar()
396            out.from_file(filename=path + case + '/OSZICAR')
397            energy_pot.append(out.parse_dict['energy_pot'])
398            struct = read_atoms(filename=path + case + '/CONTCAR')
399            if wrap is not False:
400                struct = _wrap_positions(struct, wrap)
401            cells.append(struct.cell)
402            positions.append(struct.positions)
403        
404        relaxation_steps = len(out.parse_dict['energy_pot'])
405        energy_pot = np.array(
406            [[self['input/neb/initial_energy']] * relaxation_steps] + 
407            energy_pot +
408            [[self['input/neb/final_energy']] * relaxation_steps]
409        ).T
410        
411        cells.append(final.cell)
412        positions.append(final.positions)
413        
414        self['output'].create_group('neb')
415        self['output/neb'].put('energy_pot', energy_pot)
416        self['output/neb'].put('final_cells', cells)
417        self['output/neb'].put('final_positions', positions)
418        ene = energy_pot[-1]
419        self['output/neb'].put('barrier_forward', max(ene) - ene[0])
420        self['output/neb'].put('barrier_backward', max(ene) - ene[-1])
421        self._polynomial_fit_with_derivative_constraints(n=nImages + 1)

Collect the data from the NEB calculation.

Parameters: wrap (float or bool, optional): Wrap value for atomic positions. Set to False to disable wrapping.

def transfer_from_remote_and_collect(self):
423    def transfer_from_remote_and_collect(self):
424        """
425        Transfer files from the remote cluster and collect data locally.
426        """
427        self.transfer_from_remote()
428        self.status.collect = True
429        self.collect_data()
430        self.status.finished = True

Transfer files from the remote cluster and collect data locally.

def get_energy_fit(self):
559    def get_energy_fit(self):
560        """
561        Get the fitted polynomial for the NEB energy profile.
562
563        Returns:
564            Polynomial: The fitted polynomial.
565        """
566        return self['output/neb/fit']

Get the fitted polynomial for the NEB energy profile.

Returns: Polynomial: The fitted polynomial.