Coverage for calorine/nep/nep.py: 100%
300 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 13:50 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 13:50 +0000
1import contextlib
2import os
3import warnings
4from pathlib import Path
5from typing import List, Optional, Tuple, Union
7import numpy as np
8from ase import Atoms
10import _nepy
11from calorine.nep.model import _get_nep_contents
14def _get_atomic_properties(
15 structure: Atoms,
16) -> Tuple[List[float], List[str], List[float]]:
17 """Fetches cell, symbols and positions for a structure. Since NEP_CPU requires a cell, if the
18 structure has no cell a default cubic cell with a side of 100 Å will be used.
20 Parameters
21 ----------
22 structure
23 Atoms object representing the structure.
25 Returns
26 -------
27 List[float]
28 Cell vectors
29 List[str]
30 Atomic species
31 List[float]
32 Atom positions
33 List[float]
34 Atom masses
35 """
36 if structure.cell.rank == 0:
37 warnings.warn('Using default unit cell (cubic with side 100 Å).')
38 set_default_cell(structure)
40 c = structure.get_cell(complete=True).flatten()
41 cell = [c[0], c[3], c[6], c[1], c[4], c[7], c[2], c[5], c[8]]
43 symbols = structure.get_chemical_symbols()
44 positions = list(
45 structure.get_positions().T.flatten()
46 ) # [x1, ..., xN, y1, ... yN,...]
47 masses = structure.get_masses()
48 return cell, symbols, positions, masses
51def _setup_nepy(
52 model_filename: str,
53 natoms: int,
54 cell: List[float],
55 symbols: List[str],
56 positions: List[float],
57 masses: List[float],
58 debug: bool,
59) -> _nepy.NEPY:
60 """Sets up an instance of the NEPY pybind11 interface to NEP_CPU.
62 Parameters
63 ----------
64 model_filename
65 Path to model.
66 natoms:
67 Number of atoms in the structure.
68 cell:
69 Cell vectors.
70 symbols:
71 Atom species.
72 positions:
73 Atom positions.
74 masses:
75 Atom masses.
76 debug:
77 Flag to control if the output from NEP_CPU will be printed.
79 Returns
80 -------
81 NEPY
82 NEPY interface
83 """
84 # Ensure that `model_filename` exists to avoid segfault in pybind11 code
85 if not os.path.isfile(model_filename):
86 raise ValueError(f'{Path(model_filename)} does not exist')
88 # Disable output from C++ code by default
89 if debug:
90 nepy = _nepy.NEPY(model_filename, natoms, cell, symbols, positions, masses)
91 else:
92 with open(os.devnull, 'w') as f:
93 with contextlib.redirect_stdout(f):
94 with contextlib.redirect_stderr(f):
95 nepy = _nepy.NEPY(
96 model_filename, natoms, cell, symbols, positions, masses
97 )
98 return nepy
101def set_default_cell(structure: Atoms, box_length: float = 100):
102 """Adds a cubic box to an Atoms object. Atoms object is edited in-place.
104 Parameters
105 ----------
106 structure
107 Structure to add box to
108 box_length
109 Cubic box side length in Å, by default 100
110 """
111 structure.set_cell([[box_length, 0, 0], [0, box_length, 0], [0, 0, box_length]])
112 structure.center()
115def get_descriptors(
116 structure: Atoms, model_filename: str, debug: bool = False
117) -> np.ndarray:
118 """Calculates the NEP descriptors for a given structure. A NEP model defined by a nep.txt
119 can additionally be provided to get the NEP3 model specific descriptors.
121 Parameters
122 ----------
123 structure
124 Input structure
125 model_filename
126 Path to NEP model in ``nep.txt`` format.
127 debug
128 Flag to toggle debug mode. Prints GPUMD output. Defaults to ``False``.
130 Returns
131 -------
132 Descriptors for the supplied structure, with shape (number_of_atoms, descriptor components)
133 """
134 local_structure = structure.copy()
135 natoms = len(local_structure)
136 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
138 nepy = _setup_nepy(
139 model_filename, natoms, cell, symbols, positions, masses, debug
140 )
141 all_descriptors = nepy.get_descriptors()
142 descriptors_per_atom = np.array(all_descriptors).reshape(-1, natoms).T
143 return descriptors_per_atom
146def get_latent_space(
147 structure: Atoms, model_filename: Union[str, None] = None, debug: bool = False
148) -> np.ndarray:
149 """Calculates the latent space representation of a structure, i.e, the activiations in
150 the hidden layer. A NEP model defined by a `nep.txt` file needs to be provided.
152 Parameters
153 ----------
154 structure
155 Input structure
156 model_filename
157 Path to NEP model. Defaults to None.
158 debug
159 Flag to toggle debug mode. Prints GPUMD output. Defaults to False.
161 Returns
162 -------
163 Activation with shape `(natoms, N_neurons)`
164 """
165 if model_filename is None:
166 raise ValueError('Model is undefined')
167 local_structure = structure.copy()
168 natoms = len(local_structure)
169 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
171 nepy = _setup_nepy(
172 model_filename, natoms, cell, symbols, positions, masses, debug
173 )
175 latent = nepy.get_latent_space()
176 latent = np.array(latent).reshape(-1, natoms).T
177 return latent
180def get_potential_forces_and_virials(
181 structure: Atoms, model_filename: Optional[str] = None, debug: bool = False
182) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
183 """Calculates the per-atom potential, forces and virials for a given structure.
184 A NEP model defined by a `nep.txt` file needs to be provided.
186 Parameters
187 ----------
188 structure
189 Input structure
190 model_filename
191 Path to NEP model. Defaults to None.
192 debug
193 Flag to toggle debug mode. Prints GPUMD output. Defaults to False.
195 Returns
196 -------
197 potential with shape `(natoms,)`
198 forces with shape `(natoms, 3)`
199 virials with shape `(natoms, 9)`
200 """
201 if model_filename is None:
202 raise ValueError('Model is undefined')
204 model_type = _get_nep_contents(model_filename)[0]['model_type']
205 if model_type != 'potential':
206 raise ValueError(
207 'A NEP model trained for predicting energies and forces must be used.'
208 )
210 local_structure = structure.copy()
211 natoms = len(local_structure)
212 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
214 nepy = _setup_nepy(
215 model_filename, natoms, cell, symbols, positions, masses, debug
216 )
218 energies, forces, virials = nepy.get_potential_forces_and_virials()
219 forces_per_atom = np.array(forces).reshape(-1, natoms).T
220 virials_per_atom = np.array(virials).reshape(-1, natoms).T
221 return np.array(energies), forces_per_atom, virials_per_atom
224def get_polarizability(
225 structure: Atoms,
226 model_filename: Optional[str] = None,
227 debug: bool = False,
228) -> np.ndarray:
229 """Calculates the polarizability tensor for a given structure. A NEP model defined
230 by a ``nep.txt`` file needs to be provided. The model must be trained to predict the
231 polarizability.
233 Parameters
234 ----------
235 structure
236 Input structure
237 model_filename
238 Path to NEP model in ``nep.txt`` format. Defaults to ``None``.
239 debug
240 Flag to toggle debug mode. Prints GPUMD output. Defaults to ``False``.
242 Returns
243 -------
244 polarizability with shape ``(3, 3)``
245 """
246 if model_filename is None:
247 raise ValueError('Model is undefined')
249 model_type = _get_nep_contents(model_filename)[0]['model_type']
250 if model_type != 'polarizability':
251 raise ValueError(
252 'A NEP model trained for predicting polarizability must be used.'
253 )
255 local_structure = structure.copy()
256 natoms = len(local_structure)
257 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
259 nepy = _setup_nepy(
260 model_filename, natoms, cell, symbols, positions, masses, debug
261 )
262 # Components are in order xx yy zz xy yz zx.
263 pol = nepy.get_polarizability()
264 polarizability = np.array([
265 [pol[0], pol[3], pol[5]],
266 [pol[3], pol[1], pol[4]],
267 [pol[5], pol[4], pol[2]]
268 ])
270 return polarizability
273def get_dipole(
274 structure: Atoms,
275 model_filename: Optional[str] = None,
276 debug: bool = False,
277) -> np.ndarray:
278 """Calculates the dipole for a given structure. A NEP model defined by a
279 ``nep.txt`` file needs to be provided.
281 Parameters
282 ----------
283 structure
284 Input structure
285 model_filename
286 Path to NEP model in ``nep.txt`` format. Defaults to ``None``.
287 debug
288 Flag to toggle debug mode. Prints GPUMD output. Defaults to ``False``.
290 Returns
291 -------
292 dipole with shape ``(3,)``
293 """
294 if model_filename is None:
295 raise ValueError('Model is undefined')
297 model_type = _get_nep_contents(model_filename)[0]['model_type']
298 if model_type != 'dipole':
299 raise ValueError('A NEP model trained for predicting dipoles must be used.')
301 local_structure = structure.copy()
302 natoms = len(local_structure)
303 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
305 nepy = _setup_nepy(
306 model_filename, natoms, cell, symbols, positions, masses, debug
307 )
308 dipole = np.array(nepy.get_dipole())
310 return dipole
313def get_dipole_gradient(
314 structure: Atoms,
315 model_filename: Optional[str] = None,
316 backend: str = 'c++',
317 method: str = 'central difference',
318 displacement: float = 0.01,
319 charge: float = 1.0,
320 nep_command: str = 'nep',
321 debug: bool = False,
322) -> np.ndarray:
323 """Calculates the dipole gradient for a given structure using finite differences.
324 A NEP model defined by a `nep.txt` file needs to be provided.
326 Parameters
327 ----------
328 structure
329 Input structure
330 model_filename
331 Path to NEP model in ``nep.txt`` format. Defaults to ``None``.
332 backend
333 Backend to use for computing dipole gradient with finite differences.
334 One of ``'c++'`` (CPU), ``'python'`` (CPU) and ``'nep'`` (GPU).
335 Defaults to ``'c++'``.
336 method
337 Method for computing gradient with finite differences.
338 One of 'forward difference' and 'central difference'.
339 Defaults to 'central difference'
340 displacement
341 Displacement in Å to use for finite differences. Defaults to ``0.01``.
342 charge
343 System charge in units of the elemental charge.
344 Used for correcting the dipoles before computing the gradient.
345 Defaults to ``1.0``.
346 nep_command
347 Command for running the NEP executable. Defaults to ``'nep'``.
348 debug
349 Flag to toggle debug mode. Prints GPUMD output (if applicable). Defaults to ``False``.
352 Returns
353 -------
354 dipole gradient with shape ``(N, 3, 3)``
355 """
356 if model_filename is None:
357 raise ValueError('Model is undefined')
359 model_type = _get_nep_contents(model_filename)[0]['model_type']
360 if model_type != 'dipole':
361 raise ValueError('A NEP model trained for predicting dipoles must be used.')
363 local_structure = structure.copy()
365 if backend == 'c++':
366 dipole_gradient = _dipole_gradient_cpp(
367 local_structure,
368 model_filename,
369 displacement=displacement,
370 method=method,
371 charge=charge,
372 debug=debug,
373 )
374 elif backend == 'python':
375 dipole_gradient = _dipole_gradient_python(
376 local_structure,
377 model_filename,
378 displacement=displacement,
379 charge=charge,
380 method=method,
381 )
382 elif backend == 'nep':
383 dipole_gradient = _dipole_gradient_nep(
384 local_structure,
385 model_filename,
386 displacement=displacement,
387 method=method,
388 charge=charge,
389 nep_command=nep_command,
390 )
391 else:
392 raise ValueError(f'Invalid backend {backend}')
393 return dipole_gradient
396def _dipole_gradient_cpp(
397 structure: Atoms,
398 model_filename: str,
399 method: str = 'central difference',
400 displacement: float = 0.01,
401 charge: float = 1.0,
402 debug: bool = False,
403) -> np.ndarray:
404 """Calculates the dipole gradient with finite differences, using NEP_CPU.
406 Parameters
407 ----------
408 structure
409 Input structure
410 model_filename
411 Path to NEP model in ``nep.txt`` format.
412 method
413 Method for computing gradient with finite differences.
414 One of ``'forward difference'`` and ``'central difference'``.
415 Defaults to ``'central difference'``
416 displacement
417 Displacement in Å to use for finite differences. Defaults to ``0.01``.
418 charge
419 System charge in units of the elemental charge.
420 Used for correcting the dipoles before computing the gradient.
421 Defaults to ``1.0``.
423 Returns
424 -------
425 dipole gradient with shape ``(N, 3, 3)``
426 """
427 if displacement <= 0:
428 raise ValueError('Displacement must be > 0 Å')
430 implemented_methods = {
431 'forward difference': 0,
432 'central difference': 1,
433 'second order central difference': 2,
434 }
436 if method not in implemented_methods.keys():
437 raise ValueError(f'Invalid method {method} for calculating gradient')
439 local_structure = structure.copy()
440 natoms = len(local_structure)
441 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
442 nepy = _setup_nepy(
443 model_filename, natoms, cell, symbols, positions, masses, debug
444 )
445 dipole_gradient = np.array(
446 nepy.get_dipole_gradient(displacement, implemented_methods[method], charge)
447 ).reshape(natoms, 3, 3)
448 return dipole_gradient
451def _dipole_gradient_python(
452 structure: Atoms,
453 model_filename: str,
454 method: str = 'central difference',
455 displacement: float = 0.01,
456 charge: float = 1.0,
457) -> np.ndarray:
458 """Calculates the dipole gradient with finite differences, using the Python and get_dipole().
460 Parameters
461 ----------
462 structure
463 Input structure
464 model_filename
465 Path to NEP model in ``nep.txt`` format.
466 method
467 Method for computing gradient with finite differences.
468 One of ``'forward difference'`` and ``'central difference'``.
469 Defaults to ``'central difference'``
470 displacement
471 Displacement in Å to use for finite differences. Defaults to ``0.01``.
472 charge
473 System charge in units of the elemental charge.
474 Used for correcting the dipoles before computing the gradient.
475 Defaults to ``1.0``.
477 Returns
478 -------
479 dipole gradient with shape ``(N, 3, 3)``
480 """
481 if displacement <= 0:
482 raise ValueError('Displacement must be > 0 Å')
484 N = len(structure)
485 if method == 'forward difference':
486 # Correct all dipole by the permanent dipole, charge * center of mass
487 d = (
488 get_dipole(structure, model_filename)
489 + charge * structure.get_center_of_mass()
490 )
491 d_forward = np.zeros((N, 3, 3))
492 for atom in range(N):
493 for cartesian in range(3):
494 copy = structure.copy()
495 positions = copy.get_positions()
496 positions[atom, cartesian] += displacement
497 copy.set_positions(positions)
498 d_forward[atom, cartesian, :] = (
499 get_dipole(copy, model_filename)
500 + charge * copy.get_center_of_mass()
501 )
502 gradient = (d_forward - d[None, None, :]) / displacement
504 elif method == 'central difference':
505 d_forward = np.zeros((N, 3, 3))
506 d_backward = np.zeros((N, 3, 3))
507 for atom in range(N):
508 for cartesian in range(3):
509 # Forward displacements
510 copy_forward = structure.copy()
511 positions_forward = copy_forward.get_positions()
512 positions_forward[atom, cartesian] += displacement
513 copy_forward.set_positions(positions_forward)
514 d_forward[atom, cartesian, :] = (
515 get_dipole(copy_forward, model_filename)
516 + charge * copy_forward.get_center_of_mass()
517 )
518 # Backwards displacement
519 copy_backward = structure.copy()
520 positions_backward = copy_backward.get_positions()
521 positions_backward[atom, cartesian] -= displacement
522 copy_backward.set_positions(positions_backward)
523 d_backward[atom, cartesian, :] = (
524 get_dipole(copy_backward, model_filename)
525 + charge * copy_backward.get_center_of_mass()
526 )
527 gradient = (d_forward - d_backward) / (2 * displacement)
528 elif method == 'second order central difference':
529 # Coefficients from
530 # https://en.wikipedia.org/wiki/Finite_difference_coefficient#Central_finite_difference
531 d_forward_one_h = np.zeros((N, 3, 3))
532 d_forward_two_h = np.zeros((N, 3, 3))
533 d_backward_one_h = np.zeros((N, 3, 3))
534 d_backward_two_h = np.zeros((N, 3, 3))
535 for atom in range(N):
536 for cartesian in range(3):
537 copy = structure.copy()
538 positions = copy.get_positions()
539 # Forward displacements
540 positions[atom, cartesian] += displacement # + h
541 copy.set_positions(positions)
542 d_forward_one_h[atom, cartesian, :] = (
543 get_dipole(copy, model_filename)
544 + charge * copy.get_center_of_mass()
545 )
546 positions[atom, cartesian] += displacement # + 2h total
547 copy.set_positions(positions)
548 d_forward_two_h[atom, cartesian, :] = (
549 get_dipole(copy, model_filename)
550 + charge * copy.get_center_of_mass()
551 )
552 # Backwards displacement
553 positions[atom, cartesian] -= 3 * displacement # 2h - 3h = -h
554 copy.set_positions(positions)
555 d_backward_one_h[atom, cartesian, :] = (
556 get_dipole(copy, model_filename)
557 + charge * copy.get_center_of_mass()
558 )
559 positions[atom, cartesian] -= displacement # - 2h total
560 copy.set_positions(positions)
561 d_backward_two_h[atom, cartesian, :] = (
562 get_dipole(copy, model_filename)
563 + charge * copy.get_center_of_mass()
564 )
565 c0 = -1.0 / 12.0
566 c1 = 2.0 / 3.0
567 gradient = (
568 c0 * d_forward_two_h
569 + c1 * d_forward_one_h
570 - c1 * d_backward_one_h
571 - c0 * d_backward_two_h
572 ) / displacement
573 else:
574 raise ValueError(f'Invalid method {method} for calculating gradient')
575 return gradient
578def _dipole_gradient_nep(
579 structure: Atoms,
580 model_filename: str,
581 method: str = 'central difference',
582 displacement: float = 0.01,
583 charge: float = 1.0,
584 nep_command: str = 'nep',
585) -> np.ndarray:
586 """Calculates the dipole gradient with finite differences, using the NEP executable.
588 Parameters
589 ----------
590 structure
591 Input structure
592 model_filename
593 Path to NEP model in ``nep.txt`` format.
594 method
595 Method for computing gradient with finite differences.
596 One of ``'forward difference'`` and ``'central difference'``.
597 Defaults to ``'central difference'``
598 displacement
599 Displacement in Å to use for finite differences. Defaults to 0.01 Å.
600 Note that results are possibly unreliable for displacemen < 0.01,
601 which might be due to rounding errors.
602 charge
603 System charge in units of the elemental charge.
604 Used for correcting the dipoles before computing the gradient.
605 Defaults to 1.0.
606 nep_command
607 Command for running the NEP executable. Defaults to ``'nep'``.
610 Returns
611 -------
612 dipole gradient with shape ``(N, 3, 3)``
613 """
614 if displacement <= 0:
615 raise ValueError('Displacement must be > 0 Å')
617 if displacement < 1e-2:
618 warnings.warn(
619 'Dipole gradients with nep are unstable for displacements < 0.01 Å.'
620 )
622 N = len(structure)
623 if method == 'forward difference':
624 structure = _set_dummy_energy_forces(structure)
625 structures = [structure] # will hold 3N+1 structures
626 # Correct for the constant dipole, by adding charge * center of mass
627 corrections = np.zeros((3 * N + 1, 3))
628 corrections[0] = charge * structure.get_center_of_mass()
629 for atom in range(N):
630 for cartesian in range(3):
631 copy = structure.copy()
632 positions = copy.get_positions()
633 positions[atom, cartesian] += displacement
634 copy.set_positions(positions)
635 copy = _set_dummy_energy_forces(copy)
636 structures.append(copy)
637 corrections[1 + atom * 3 + cartesian] = (
638 charge * copy.get_center_of_mass()
639 )
641 dipoles = (
642 _predict_dipole_batch(structures, model_filename, nep_command) * N
643 ) # dipole/atom, shape (3N+1, 3)
644 dipoles += corrections
646 d = dipoles[0, :]
647 d_forward = dipoles[1:].reshape(N, 3, 3)
648 gradient = (d_forward - d[None, None, :]) / displacement
650 elif method == 'central difference':
651 structures_forward = [] # will hold 3N structures
652 structures_backward = [] # will hold 3N structures
654 # Correct for the constant dipole, by adding charge * center of mass
655 corrections_forward = np.zeros((3 * N, 3))
656 corrections_backward = np.zeros((3 * N, 3))
658 for atom in range(N):
659 for cartesian in range(3):
660 # Forward displacements
661 copy_forward = structure.copy()
662 positions_forward = copy_forward.get_positions()
663 positions_forward[atom, cartesian] += displacement
664 copy_forward.set_positions(positions_forward)
665 copy_forward = _set_dummy_energy_forces(copy_forward)
666 structures_forward.append(copy_forward)
667 corrections_forward[atom * 3 + cartesian] = (
668 charge * copy_forward.get_center_of_mass()
669 )
671 # Backwards displacement
672 copy_backward = structure.copy()
673 positions_backward = copy_backward.get_positions()
674 positions_backward[atom, cartesian] -= displacement
675 copy_backward.set_positions(positions_backward)
676 copy_backward = _set_dummy_energy_forces(copy_backward)
677 structures_backward.append(copy_backward)
678 corrections_backward[atom * 3 + cartesian] = (
679 charge * copy_backward.get_center_of_mass()
680 )
682 structures = structures_forward + structures_backward
683 dipoles = (
684 _predict_dipole_batch(structures, model_filename, nep_command) * N
685 ) # dipole/atom, shape (6N, 3)
686 d_forward = dipoles[: 3 * N, :]
687 d_backward = dipoles[3 * N :, :]
689 d_forward += corrections_forward
690 d_backward += corrections_backward
692 d_forward = d_forward.reshape(N, 3, 3)
693 d_backward = d_backward.reshape(N, 3, 3)
695 gradient = (d_forward - d_backward) / (2 * displacement)
696 else:
697 raise ValueError(f'Invalid method {method} for calculating gradient')
698 return gradient
701def _set_dummy_energy_forces(structure: Atoms) -> Atoms:
702 """Sets the energies and forces of structure to zero.
704 Parameters
705 ----------
706 structure
707 Input structure
710 Returns
711 ------- Copy of structure, with SinglePointCalculator with zero energy and force.
712 """
713 from ase.calculators.singlepoint import SinglePointCalculator
715 copy = structure.copy()
716 N = len(copy)
717 energy = 0
719 forces = np.zeros((N, 3))
720 dummy = SinglePointCalculator(copy, **{'energy': energy, 'forces': forces})
721 copy.calc = dummy
722 return copy
725def _predict_dipole_batch(
726 structures: List[Atoms], model_filename: str, nep_command: str = 'nep'
727) -> np.ndarray:
728 """Predicts dipoles for a set of structures using the NEP executable
729 Note that the units are in (dipole units)/atom.
731 Parameters
732 ----------
733 structure
734 Input structures
735 model_filename
736 Path to NEP model in ``nep.txt`` format.
737 nep_command
738 Command for running the NEP executable. Defaults to ``'nep'``.
741 Returns
742 ------- Predicted dipoles, with shape (len(structures), 3).
743 """
744 import shutil
745 from os.path import join as join_path
746 from subprocess import run
747 from tempfile import TemporaryDirectory
749 from calorine.nep import read_model, write_nepfile, write_structures
751 with TemporaryDirectory() as directory:
752 shutil.copy2(model_filename, join_path(directory, 'nep.txt'))
753 model = read_model(model_filename)
755 parameters = dict(
756 prediction=1,
757 mode=1,
758 version=model.version,
759 n_max=[model.n_max_radial, model.n_max_angular],
760 cutoff=[model.radial_cutoff, model.angular_cutoff],
761 basis_size=[model.n_basis_radial, model.n_basis_radial],
762 l_max=[model.l_max_3b, model.l_max_4b, model.l_max_5b],
763 neuron=model.n_neuron,
764 type=[len(model.types), *model.types],
765 )
767 write_nepfile(parameters, directory)
768 file = join_path(directory, 'train.xyz')
769 write_structures(file, structures)
771 # Execute nep
772 completed = run([nep_command], cwd=directory, capture_output=True)
773 completed.check_returncode()
775 # Read results
776 dipoles = np.loadtxt(join_path(directory, 'dipole_train.out'))
777 if len(dipoles.shape) == 1:
778 dipoles = dipoles.reshape(1, -1)
779 return dipoles[:, :3]
782def _check_components_polarizability_gradient(component: Union[str, List[str]]) -> List[int]:
783 """
784 Verifies that the selected components are ok.
785 """
786 allowed_components = {
787 'x': 0,
788 'y': 1,
789 'z': 2,
790 }
792 # Check if chosen components are ok
793 if component == 'full':
794 components_to_compute = [0, 1, 2]
795 else:
796 components_list = [component] if isinstance(component, str) else component
797 components_to_compute = []
798 for c in components_list:
799 if c in allowed_components.keys():
800 components_to_compute.append(allowed_components[c])
801 elif c == 'full':
802 raise ValueError('Write ``component="full"`` to get all components.')
803 else:
804 raise ValueError(f'Invalid component {c}')
805 assert len(components_list) == len(components_to_compute), \
806 'Number of components to compute does not match.'
807 return components_to_compute
810def get_polarizability_gradient(
811 structure: Atoms,
812 model_filename: Optional[str] = None,
813 displacement: float = 0.01,
814 component: Union[str, List[str]] = 'full',
815 debug: bool = False,
816) -> np.ndarray:
817 """Calculates the dipole gradient for a given structure using finite differences.
818 A NEP model defined by a ``nep.txt`` file needs to be provided.
819 This function computes the derivatives using the second-order central difference
820 method with a C++ backend.
822 Parameters
823 ----------
824 structure
825 Input structure.
826 model_filename
827 Path to NEP model in ``nep.txt`` format. Defaults to ``None``.
828 displacement
829 Displacement in Å to use for finite differences. Defaults to ``0.01``.
830 component
831 Component or components of the polarizability tensor that the gradient
832 should be computed for.
833 The following components are available: ``x`, ``y``, ``z``, ``full``.
834 Option ``full`` computes the derivative whilst moving the atoms in each Cartesian
835 direction, which yields a tensor of shape ``(N, 3, 6)``.
836 Multiple components may be specified.
837 Defaults to ``full``.
838 debug
839 Flag to toggle debug mode. Prints GPUMD output (if applicable). Defaults to ``False``.
842 Returns
843 -------
844 polarizability gradient with shape ``(N, C, 6)`` where ``C``
845 is the number of components chosen.
846 """
847 if model_filename is None:
848 raise ValueError('Model is undefined')
850 model_type = _get_nep_contents(model_filename)[0]['model_type']
851 if model_type != 'polarizability':
852 raise ValueError('A NEP model trained for predicting polarizability must be used.')
854 components_to_compute = _check_components_polarizability_gradient(component)
856 local_structure = structure.copy()
857 polarizability_gradient = _polarizability_gradient_cpp(
858 local_structure,
859 model_filename,
860 displacement=displacement,
861 components=components_to_compute,
862 debug=debug,
863 )
864 return polarizability_gradient
867def _polarizability_gradient_to_3x3(natoms, pg):
868 """
869 Converts a polarizability gradient tensor with
870 shape (Natoms, 3, 6) to (Natoms, 3, 3, 3).
871 The 6 items in the polarizability gradient have
872 the following order from GPUMD: xx, yy, zz, xy, yz, zx.
873 """
874 gpumd_to_3x3_indices = np.array([
875 [0, 3, 5],
876 [3, 1, 4],
877 [5, 4, 2]
878 ])
879 polgrad_3x3 = np.zeros((natoms, 3, 3, 3))
880 for i in range(3):
881 for j in range(3):
882 index = gpumd_to_3x3_indices[i, j]
883 polgrad_3x3[:, :, i, j] = pg[:, :, index]
884 return polgrad_3x3
887def _polarizability_gradient_cpp(
888 structure: Atoms,
889 model_filename: str,
890 displacement: float,
891 components: List[int],
892 debug: bool = False,
893) -> np.ndarray:
894 """Calculates the polarizability gradient with finite differences, using NEP_CPU.
896 Parameters
897 ----------
898 structure
899 Input structure.
900 model_filename
901 Path to NEP model in ``nep.txt`` format.
902 displacement
903 Displacement in Å to use for finite differences. Defaults to ``0.01``.
904 components
905 List of components to compute. Integer values from 0 to 2 that corresponds
906 to indices in the ordered list [x, y, z]. Index 1 corresponds to the
907 derivative with regards to only the y positions of the atoms, and so forth.
909 Returns
910 -------
911 dipole gradient with shape ``(N, len(components), 3, 3)``
912 """
913 if displacement <= 0:
914 raise ValueError('Displacement must be > 0 Å')
915 # TODO possibly use components later to only move atoms in one cartesian direction
916 local_structure = structure.copy()
917 natoms = len(local_structure)
918 cell, symbols, positions, masses = _get_atomic_properties(local_structure)
919 nepy = _setup_nepy(
920 model_filename, natoms, cell, symbols, positions, masses, debug
921 )
922 pg = np.array(
923 nepy.get_polarizability_gradient(displacement, components)
924 ).reshape(natoms, 3, 6)
925 # Convert to 3x3
926 polarizability_gradient = _polarizability_gradient_to_3x3(natoms, pg)
927 return polarizability_gradient[:, components, :, :] # Only return the relevant components