Coverage for hiphive/cutoffs.py: 90%
122 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 11:20 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 11:20 +0000
1import pickle
2import numpy as np
3from ase.neighborlist import NeighborList
4from hiphive.input_output.pretty_table_prints import table_array_to_string
7class Cutoffs:
8 """ This class maintains information about the cutoff configuration,
9 i.e. which clusters will be included (="inside cutoff"). It also
10 encapsulates functionality that is used e.g., during cluster space
11 construction.
13 Here, `n-body` refers to number of atoms in a cluster. For example
14 the cluster (0011) is a two-body cluster of fourth order and the
15 cluster (123) is a three-body cluster of third order.
17 Parameters
18 ----------
19 cutoff_matrix : numpy.ndarray
20 the matrix element `ij` provides to the cutoff for order `j+2`
21 and nbody `i+2`; elements with `i>j` will be ignored
22 """
24 def __init__(self, cutoff_matrix):
26 self._cutoff_matrix = np.array(cutoff_matrix, dtype=float)
28 if len(self._cutoff_matrix.shape) != 2: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 raise ValueError('Please specify cutoff matrix as a 2D array')
30 for i, row in enumerate(self._cutoff_matrix):
31 if np.any(row[i:] < 0): 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true
32 raise ValueError('Negative number as cutoff')
33 row[:i] = np.nan
34 self._cutoff_matrix = self._cutoff_matrix[:(self.max_nbody-1), :(self.max_order-1)]
36 @property
37 def cutoff_matrix(self):
38 """ numpy.ndarray : copy of cutoff matrix """
39 return self._cutoff_matrix.copy()
41 @property
42 def orders(self):
43 """ list(int) : allowed orders """
44 return list(range(2, self.max_order + 1))
46 @property
47 def nbodies(self):
48 """ list(int) : allowed bodies """
49 return list(range(2, self.max_nbody + 1))
51 def get_cutoff(self, order, nbody):
52 """
53 Returns cutoff for a given body and order.
55 Parameters
56 ----------
57 order : int
58 nbody : int
60 Raises
61 ------
62 ValueError
63 if order is not in orders
64 ValueError
65 if nbody is not in nbodies
66 ValueError
67 if nbody is larger than order
69 Returns
70 -------
71 float
72 """
73 if order not in self.orders: 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 raise ValueError('order not in orders')
75 if nbody not in self.nbodies: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 raise ValueError('nbody not in nbodies')
77 if nbody > order: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 raise ValueError('nbody can not be larger than order')
79 return self._cutoff_matrix[nbody - 2, order - 2]
81 @property
82 def max_cutoff(self):
83 """ float : maximum cutoff """
84 max_cutoff = 0
85 for i, row in enumerate(self._cutoff_matrix):
86 max_cutoff = max(max_cutoff, np.max(row[i:]))
87 return max_cutoff
89 @property
90 def max_nbody(self):
91 """ int : maximum body """
92 nbody = 1
93 for i, row in enumerate(self._cutoff_matrix):
94 if np.any(row[i:]): 94 ↛ 93line 94 didn't jump to line 93 because the condition on line 94 was always true
95 nbody = i + 2
96 return nbody
98 @property
99 def max_order(self):
100 """ int : maximum order """
101 order = None
102 for col in range(self._cutoff_matrix.shape[1]):
103 if np.any(self._cutoff_matrix[:col + 1, col]): 103 ↛ 102line 103 didn't jump to line 102 because the condition on line 103 was always true
104 order = col + 2
105 return order
107 def max_nbody_cutoff(self, nbody):
108 """ Return maximum cutoff for a given body. """
109 if nbody not in self.nbodies: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise ValueError('nbody not in nbodies')
111 return np.max(self._cutoff_matrix[nbody - 2, max(0, nbody - 2):])
113 def max_nbody_order(self, nbody):
114 """ Returns maximum order for a given body """
115 if nbody not in self.nbodies: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 raise ValueError('nbody not in nbodies')
117 row = self._cutoff_matrix[nbody - 2]
118 max_order = None
119 for order, cutoff in enumerate(row[nbody-2:], start=nbody):
120 if cutoff: 120 ↛ 119line 120 didn't jump to line 119 because the condition on line 120 was always true
121 max_order = order
122 return max_order
124 def write(self, fileobj):
125 """ Writes instance to file.
127 Parameters
128 ----------
129 fileobj : file-like object
130 file-like object to which the cutoffs will be written to
131 """
132 pickle.dump(self._cutoff_matrix, fileobj)
134 def read(fileobj):
135 """ Reads an instance from file.
137 Parameters
138 ----------
139 fileobj : file-like object
140 input file to read from
141 """
142 data = pickle.load(fileobj)
143 return Cutoffs(data)
145 def to_filename_tag(self):
146 """ Simple function turning cutoffs into a string to be used in e.g.
147 filenames. """
148 s = []
149 for i, c in enumerate(self._cutoff_matrix.tolist(), start=2):
150 s.append('{}body-{}'.format(i, '_'.join(map(str, c))))
151 return '_'.join(s)
153 def __str__(self):
154 cutoff_matrix = self._cutoff_matrix.copy()
155 cutoff_matrix = np.vstack(([[None] * len(self.orders)], cutoff_matrix))
156 s = table_array_to_string(cutoff_matrix)
158 width = max(len(c) for c in s.split('\n'))
159 header = ' Cutoffs '.center(width, '=') + '\n'
160 bottom = '\n' + ''.center(width, '=')
161 s = header + s + bottom
162 return s
164 def __repr__(self):
165 return 'Cutoffs({!r})'.format(self._cutoff_matrix)
168class CutoffMaximumBody(Cutoffs):
169 """ Specify cutoff-list plus maximum body
171 Usefull when creating e.g. 6th order expansions but with only 3-body
172 interactions.
174 Parameters
175 ----------
176 cutoff_list : list
177 list of cutoffs for order 2, 3, etc. Must be in decresing order
178 max_nbody : int
179 No clusters containing more than max_nbody atoms will be generated
180 """
182 def __init__(self, cutoff_list, max_nbody):
183 cutoff_matrix = np.zeros((max_nbody - 1, len(cutoff_list)))
184 for order, cutoff in enumerate(cutoff_list, start=2):
185 cutoff_matrix[:, order - 2] = cutoff
186 super().__init__(cutoff_matrix)
189def is_cutoff_allowed(atoms, cutoff):
190 """ Checks if atoms is compatible with cutoff
192 Parameters
193 ----------
194 atoms : ase.Atoms
195 structure used for checking compatibility with cutoff
196 cutoff : float
197 cutoff to be tested
199 Returns
200 -------
201 bool
202 True if cutoff compatible with atoms object, else False
203 """
204 nbrlist = NeighborList(cutoffs=[cutoff / 2] * len(atoms), skin=0,
205 self_interaction=False, bothways=True)
206 nbrlist.update(atoms)
208 for i in range(len(atoms)):
209 neighbors, _ = nbrlist.get_neighbors(i)
210 if i in neighbors:
211 return False
212 if len(neighbors) != len(set(neighbors)):
213 return False
214 return True
217def estimate_maximum_cutoff(atoms, max_iter=11):
218 """ Estimates the maximum possible cutoff given the atoms object
220 Parameters
221 ----------
222 atoms : ase.Atoms
223 structure used for checking compatibility with cutoff
224 max_iter : int
225 number of iterations in binary search
226 """
228 # First upper boundary of cutoff
229 upper_cutoff = min(np.linalg.norm(atoms.cell, axis=1))
231 # generate all possible offsets given upper_cutoff
232 nbrlist = NeighborList(cutoffs=[upper_cutoff / 2] * len(atoms), skin=0,
233 self_interaction=False, bothways=True)
234 nbrlist.update(atoms)
235 all_offsets = []
236 for i in range(len(atoms)):
237 _, offsets = nbrlist.get_neighbors(i)
238 all_offsets.extend([tuple(offset) for offset in offsets])
240 # find lower boundary and new upper boundary
241 unique_offsets = set(all_offsets)
242 unique_offsets.discard((0, 0, 0))
243 upper = min(np.linalg.norm(np.dot(offset, atoms.cell))
244 for offset in unique_offsets)
245 lower = upper / 2.0
247 # run binary search between the upper and lower bounds
248 for _ in range(max_iter):
249 cutoff = (upper + lower) / 2
250 if is_cutoff_allowed(atoms, cutoff):
251 lower = cutoff
252 else:
253 upper = cutoff
254 return lower
257class BaseClusterFilter:
258 """Base cluster filter class.
260 This filter simply accepts all proposed clusters. A proper
261 subclass must implement the same methods.
262 """
263 def setup(self, atoms):
264 """ The filter is passed the environment of the primitive cell.
266 Parameters
267 ----------
268 atoms : ase.Atoms
269 non-pbc primitive cell plus neighboring atoms
270 """
271 self._atoms = atoms
273 def __call__(self, cluster):
274 """ Returns True or False when a cluster is proposed.
276 Parameters
277 ----------
278 cluster : tuple(int)
279 indices of proposed cluster referenced to the internal
280 atoms object
281 """
282 return True