lammpsparser 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ from typing import Optional
5
+
6
+ import pandas
7
+ from ase.atoms import Atoms
8
+
9
+ from lammpsparser.compatibility.calculate import (
10
+ calc_md,
11
+ calc_minimize,
12
+ calc_static,
13
+ )
14
+ from lammpsparser.compatibility.constraints import set_selective_dynamics
15
+ from lammpsparser.output import parse_lammps_output
16
+ from lammpsparser.potential import get_potential_by_name
17
+ from lammpsparser.structure import write_lammps_datafile
18
+
19
+
20
+ def lammps_file_interface_function(
21
+ working_directory: str,
22
+ structure: Atoms,
23
+ potential: str,
24
+ calc_mode: str = "static",
25
+ calc_kwargs: Optional[dict] = None,
26
+ units: str = "metal",
27
+ lmp_command: str = "mpiexec -n 1 --oversubscribe lmp_mpi -in lmp.in",
28
+ resource_path: Optional[str] = None,
29
+ input_control_file: Optional[dict] = None,
30
+ write_restart_file: bool = False,
31
+ read_restart_file: bool = False,
32
+ restart_file: str = "restart.out",
33
+ ):
34
+ """
35
+ A single function to execute a LAMMPS calculation based on the LAMMPS job implemented in pyiron
36
+
37
+ Examples:
38
+
39
+ >>> import os
40
+ >>> from ase.build import bulk
41
+ >>> from pyiron_atomistics.lammps.lammps import lammps_function
42
+ >>>
43
+ >>> shell_output, parsed_output, job_crashed = lammps_function(
44
+ ... working_directory=os.path.abspath("lmp_working_directory"),
45
+ ... structure=bulk("Al", cubic=True),
46
+ ... potential='2009--Mendelev-M-I--Al-Mg--LAMMPS--ipr1',
47
+ ... calc_mode="md",
48
+ ... calc_kwargs={"temperature": 500.0, "pressure": 0.0, "n_ionic_steps": 1000, "n_print": 100},
49
+ ... cutoff_radius=None,
50
+ ... units="metal",
51
+ ... bonds_kwargs={},
52
+ ... enable_h5md=False,
53
+ ... )
54
+
55
+ Args:
56
+ working_directory (str): directory in which the LAMMPS calculation is executed
57
+ structure (Atoms): ase.atoms.Atoms - atomistic structure
58
+ potential (str): Name of the LAMMPS potential based on the NIST database and the OpenKIM database
59
+ calc_mode (str): select mode of calculation ["static", "md", "minimize", "vcsgc"]
60
+ calc_kwargs (dict): key-word arguments for the calculate function, the input parameters depend on the calc_mode:
61
+ "static": No parameters
62
+ "md": "temperature", "pressure", "n_ionic_steps", "time_step", "n_print", "temperature_damping_timescale",
63
+ "pressure_damping_timescale", "seed", "tloop", "initial_temperature", "langevin", "delta_temp",
64
+ "delta_press", job_name", "rotation_matrix"
65
+ "minimize": "ionic_energy_tolerance", "ionic_force_tolerance", "max_iter", "pressure", "n_print", "style",
66
+ "rotation_matrix"
67
+ "vcsgc": "mu", "ordered_element_list", "target_concentration", "kappa", "mc_step_interval", "swap_fraction",
68
+ "temperature_mc", "window_size", "window_moves", "temperature", "pressure", "n_ionic_steps",
69
+ "time_step", "n_print", "temperature_damping_timescale", "pressure_damping_timescale", "seed",
70
+ "initial_temperature", "langevin", "job_name", "rotation_matrix"
71
+ cutoff_radius (float): cut-off radius for the interatomic potential
72
+ units (str): Units for LAMMPS
73
+ bonds_kwargs (dict): key-word arguments to create atomistic bonds:
74
+ "species", "element_list", "cutoff_list", "max_bond_list", "bond_type_list", "angle_type_list",
75
+ server_kwargs (dict): key-word arguments to create server object - the available parameters are:
76
+ "user", "host", "run_mode", "queue", "qid", "cores", "threads", "new_h5", "structure_id", "run_time",
77
+ "memory_limit", "accept_crash", "additional_arguments", "gpus", "conda_environment_name",
78
+ "conda_environment_path"
79
+ enable_h5md (bool): activate h5md mode for LAMMPS
80
+ write_restart_file (bool): enable writing the LAMMPS restart file
81
+ read_restart_file (bool): enable loading the LAMMPS restart file
82
+ restart_file (str): file name of the LAMMPS restart file to copy
83
+ executable_version (str): LAMMPS version to for the execution
84
+ executable_path (str): path to the LAMMPS executable
85
+ input_control_file (str|list|dict): Option to modify the LAMMPS input file directly
86
+
87
+ Returns:
88
+ str, dict, bool: Tuple consisting of the shell output (str), the parsed output (dict) and a boolean flag if
89
+ the execution raised an accepted error.
90
+ """
91
+ if calc_kwargs is None:
92
+ calc_kwargs = {}
93
+ else:
94
+ calc_kwargs = calc_kwargs.copy()
95
+
96
+ os.makedirs(working_directory, exist_ok=True)
97
+ potential_lst, potential_replace, species = _get_potential(
98
+ potential=potential, resource_path=resource_path
99
+ )
100
+
101
+ lmp_str_lst = []
102
+ atom_type = "atomic"
103
+ for l in lammps_file_initialization(
104
+ structure=structure,
105
+ units=units,
106
+ read_restart_file=read_restart_file,
107
+ restart_file=restart_file,
108
+ ):
109
+ if l.strip().startswith("units") and "units" in potential_replace:
110
+ lmp_str_lst.append(potential_replace["units"])
111
+ elif l.strip().startswith("atom_style") and "atom_style" in potential_replace:
112
+ lmp_str_lst.append(potential_replace["atom_style"])
113
+ atom_type = potential_replace["atom_style"].split()[-1]
114
+ elif l.strip().startswith("dimension") and "dimension" in potential_replace:
115
+ lmp_str_lst.append(potential_replace["dimension"])
116
+ else:
117
+ lmp_str_lst.append(l)
118
+
119
+ lmp_str_lst += potential_lst
120
+ lmp_str_lst += ["variable dumptime equal {} ".format(calc_kwargs.get("n_print", 1))]
121
+ lmp_str_lst += [
122
+ "dump 1 all custom ${dumptime} dump.out id type xsu ysu zsu fx fy fz vx vy vz",
123
+ 'dump_modify 1 sort id format line "%d %d %20.15g %20.15g %20.15g %20.15g %20.15g %20.15g %20.15g %20.15g %20.15g"',
124
+ ]
125
+
126
+ if calc_mode == "static":
127
+ lmp_str_lst += [
128
+ k + " " + v
129
+ for k, v in set_selective_dynamics(
130
+ structure=structure, calc_md=False
131
+ ).items()
132
+ ]
133
+ lmp_str_tmp_lst = calc_static()
134
+ lmp_str_lst = _modify_input_dict(
135
+ input_control_file=input_control_file,
136
+ lmp_str_lst=lmp_str_lst + lmp_str_tmp_lst[:-1],
137
+ )
138
+ lmp_str_lst.append(lmp_str_tmp_lst[-1])
139
+ elif calc_mode == "md":
140
+ lmp_str_lst += [
141
+ k + " " + v
142
+ for k, v in set_selective_dynamics(
143
+ structure=structure, calc_md=True
144
+ ).items()
145
+ ]
146
+ if "n_ionic_steps" in calc_kwargs.keys():
147
+ n_ionic_steps = int(calc_kwargs.pop("n_ionic_steps"))
148
+ else:
149
+ n_ionic_steps = 1
150
+ if read_restart_file:
151
+ calc_kwargs["initial_temperature"] = 0.0
152
+ calc_kwargs["units"] = units
153
+ lmp_str_lst += calc_md(**calc_kwargs)
154
+ lmp_str_lst = _modify_input_dict(
155
+ input_control_file=input_control_file,
156
+ lmp_str_lst=lmp_str_lst,
157
+ )
158
+ if read_restart_file:
159
+ lmp_str_lst += ["reset_timestep 0"]
160
+ lmp_str_lst += ["run {} ".format(n_ionic_steps)]
161
+ elif calc_mode == "minimize":
162
+ calc_kwargs["units"] = units
163
+ lmp_str_lst += [
164
+ k + " " + v
165
+ for k, v in set_selective_dynamics(
166
+ structure=structure, calc_md=False
167
+ ).items()
168
+ ]
169
+ lmp_str_tmp_lst, structure = calc_minimize(structure=structure, **calc_kwargs)
170
+ lmp_str_lst = _modify_input_dict(
171
+ input_control_file=input_control_file,
172
+ lmp_str_lst=lmp_str_lst + lmp_str_tmp_lst[:-1],
173
+ )
174
+ lmp_str_lst.append(lmp_str_tmp_lst[-1])
175
+ else:
176
+ raise ValueError(
177
+ f"calc_mode must be one of: static, md or minimize, not {calc_mode}"
178
+ )
179
+ if read_restart_file:
180
+ shutil.copyfile(
181
+ os.path.abspath(restart_file),
182
+ os.path.join(working_directory, os.path.basename(restart_file)),
183
+ )
184
+ if write_restart_file:
185
+ lmp_str_lst.append(f"write_restart {os.path.basename(restart_file)}")
186
+
187
+ with open(os.path.join(working_directory, "lmp.in"), "w") as f:
188
+ f.writelines([l + "\n" for l in lmp_str_lst])
189
+
190
+ write_lammps_datafile(
191
+ structure=structure,
192
+ potential_elements=species,
193
+ bond_dict=None,
194
+ units=units,
195
+ file_name="lammps.data",
196
+ working_directory=working_directory,
197
+ atom_type=atom_type,
198
+ )
199
+
200
+ shell = subprocess.check_output(
201
+ lmp_command,
202
+ cwd=working_directory,
203
+ shell=True,
204
+ universal_newlines=True,
205
+ env=os.environ.copy(),
206
+ )
207
+ output = parse_lammps_output(
208
+ working_directory=working_directory,
209
+ structure=structure,
210
+ potential_elements=species,
211
+ units=units,
212
+ prism=None,
213
+ dump_h5_file_name="dump.h5",
214
+ dump_out_file_name="dump.out",
215
+ log_lammps_file_name="log.lammps",
216
+ )
217
+ return shell, output, False
218
+
219
+
220
+ def lammps_file_initialization(
221
+ structure, dimension=3, units="metal", read_restart_file=False, restart_file=None
222
+ ):
223
+ init_commands = ["units " + units]
224
+ boundary = " ".join(["p" if coord else "f" for coord in structure.pbc])
225
+ if read_restart_file:
226
+ init_commands.append(f"read_restart {os.path.basename(restart_file)}")
227
+ else:
228
+ init_commands += [
229
+ "dimension " + str(dimension),
230
+ "boundary " + boundary + "",
231
+ "atom_style atomic",
232
+ "read_data lammps.data",
233
+ ]
234
+ return init_commands
235
+
236
+
237
+ def _modify_input_dict(
238
+ input_control_file: Optional[dict] = None,
239
+ lmp_str_lst: list[str] = [],
240
+ ):
241
+ if input_control_file is not None:
242
+ lmp_tmp_lst, keys_used = [], []
243
+ for l in lmp_str_lst:
244
+ ls = l.split()
245
+ if len(ls) >= 1: # Remove empty lines
246
+ key = ls[0]
247
+ if key in input_control_file.keys():
248
+ lmp_tmp_lst.append(key + " " + input_control_file[key])
249
+ keys_used.append(key)
250
+ else:
251
+ lmp_tmp_lst.append(l)
252
+ for k, v in input_control_file.items():
253
+ if k not in keys_used:
254
+ lmp_tmp_lst.append(k + " " + v)
255
+
256
+ return lmp_tmp_lst
257
+ else:
258
+ return lmp_str_lst
259
+
260
+
261
+ def _get_potential(potential, resource_path: Optional[str] = None):
262
+ if isinstance(potential, str):
263
+ potential_dataframe = get_potential_by_name(
264
+ potential_name=potential, resource_path=resource_path
265
+ )
266
+ elif isinstance(potential, pandas.DataFrame):
267
+ potential_dataframe = potential.iloc[0]
268
+ elif isinstance(potential, pandas.Series):
269
+ potential_dataframe = potential
270
+ else:
271
+ raise TypeError()
272
+
273
+ potential_replace = {}
274
+ potential_lst = []
275
+ for l in potential_dataframe["Config"]:
276
+ if l.startswith("units"):
277
+ potential_replace["units"] = l
278
+ elif l.startswith("atom_style"):
279
+ potential_replace["atom_style"] = l
280
+ elif l.startswith("dimension"):
281
+ potential_replace["dimension"] = l
282
+ else:
283
+ potential_lst.append(l)
284
+
285
+ return potential_lst, potential_replace, potential_dataframe["Species"]
@@ -0,0 +1,336 @@
1
+ from collections import OrderedDict
2
+ from typing import Dict, Optional
3
+
4
+ import numpy as np
5
+ from ase.atoms import Atoms
6
+ from structuretoolkit.analyse import get_neighbors
7
+ from structuretoolkit.common import select_index
8
+
9
+ from lammpsparser.structure import LammpsStructure
10
+
11
+
12
+ class LammpsStructureCompatibility(LammpsStructure):
13
+ def __init__(
14
+ self,
15
+ bond_dict: Optional[Dict] = None,
16
+ units: str = "metal",
17
+ atom_type: str = "atomic",
18
+ ):
19
+ super().__init__(bond_dict=bond_dict, units=units, atom_type=atom_type)
20
+ self._molecule_ids = []
21
+
22
+ @property
23
+ def structure(self) -> Optional[Atoms]:
24
+ """
25
+
26
+ Returns:
27
+
28
+ """
29
+ return self._structure
30
+
31
+ @structure.setter
32
+ def structure(self, structure):
33
+ """
34
+
35
+ Args:
36
+ structure:
37
+
38
+ Returns:
39
+
40
+ """
41
+ self._structure = structure
42
+ if self.atom_type == "full":
43
+ input_str = self.structure_full()
44
+ elif self.atom_type == "bond":
45
+ input_str = self.structure_bond()
46
+ elif self.atom_type == "charge":
47
+ input_str = self.structure_charge()
48
+ else: # self.atom_type == 'atomic'
49
+ input_str = self.structure_atomic()
50
+
51
+ self._string_input = input_str + self._get_velocities_input_string()
52
+
53
+ def structure_bond(self):
54
+ """
55
+
56
+ Returns:
57
+
58
+ """
59
+ species_lammps_id_dict = self.get_lammps_id_dict(self.el_eam_lst)
60
+ self.molecule_ids = None
61
+ # analyze structure to get molecule_ids, bonds, angles etc
62
+ coords = self.rotate_positions(self._structure)
63
+
64
+ elements = self._structure.get_chemical_symbols()
65
+
66
+ ## Standard atoms stuff
67
+ atoms = "Atoms \n\n"
68
+ # atom_style bond
69
+ # format: atom-ID, molecule-ID, atom_type, x, y, z
70
+ format_str = "{0:d} {1:d} {2:d} {3:f} {4:f} {5:f} "
71
+ if len(self._structure.positions[0]) == 3:
72
+ for id_atom, (x, y, z) in enumerate(coords):
73
+ id_mol = self.molecule_ids[id_atom]
74
+ atoms += (
75
+ format_str.format(
76
+ id_atom + 1,
77
+ id_mol,
78
+ species_lammps_id_dict[elements[id_atom]],
79
+ x,
80
+ y,
81
+ z,
82
+ )
83
+ + "\n"
84
+ )
85
+ elif len(self._structure.positions[0]) == 2:
86
+ for id_atom, (x, y) in enumerate(coords):
87
+ id_mol = self.molecule_ids[id_atom]
88
+ atoms += (
89
+ format_str.format(
90
+ id_atom + 1,
91
+ id_mol,
92
+ species_lammps_id_dict[elements[id_atom]],
93
+ x,
94
+ y,
95
+ 0.0,
96
+ )
97
+ + "\n"
98
+ )
99
+ else:
100
+ raise ValueError("dimension 1 not yet implemented")
101
+
102
+ ## Bond related.
103
+ # This seems independent from the lammps atom type ids, because bonds only use atom ids
104
+ el_list = set(self._structure.get_chemical_symbols())
105
+ el_dict = OrderedDict()
106
+ for object_id, el in enumerate(el_list):
107
+ el_dict[el] = object_id
108
+
109
+ n_s = len(el_list)
110
+ bond_type = np.ones([n_s, n_s], dtype=int)
111
+ count = 0
112
+ for i in range(n_s):
113
+ for j in range(i, n_s):
114
+ count += 1
115
+ bond_type[i, j] = count
116
+ bond_type[j, i] = count
117
+
118
+ if self.structure.bonds is None:
119
+ if self.cutoff_radius is None:
120
+ bonds_lst = get_bonds(structure=self.structure, max_shells=1)
121
+ else:
122
+ bonds_lst = get_bonds(
123
+ structure=self.structure, radius=self.cutoff_radius
124
+ )
125
+ bonds = []
126
+
127
+ for ia, i_bonds in enumerate(bonds_lst):
128
+ el_i = el_dict[elements[ia]]
129
+ for el_j, b_lst in i_bonds.items():
130
+ b_type = bond_type[el_i][el_dict[el_j]]
131
+ for i_shell, ib_shell_lst in enumerate(b_lst):
132
+ for ib in np.unique(ib_shell_lst):
133
+ if ia < ib: # avoid double counting of bonds
134
+ bonds.append([ia + 1, ib + 1, b_type])
135
+
136
+ self.structure.bonds = np.array(bonds)
137
+ bonds = self.structure.bonds
138
+
139
+ bonds_str = "Bonds \n\n"
140
+ for i_bond, (i_a, i_b, b_type) in enumerate(bonds):
141
+ bonds_str += (
142
+ "{0:d} {1:d} {2:d} {3:d}".format(i_bond + 1, b_type, i_a, i_b) + "\n"
143
+ )
144
+
145
+ return (
146
+ self.lammps_header(
147
+ structure=self.structure,
148
+ cell_dimensions=self.simulation_cell(),
149
+ species_lammps_id_dict=species_lammps_id_dict,
150
+ nbonds=len(bonds),
151
+ nbond_types=np.max(np.array(bonds)[:, 2]),
152
+ )
153
+ + "\n"
154
+ + atoms
155
+ + "\n"
156
+ + bonds_str
157
+ + "\n"
158
+ )
159
+
160
+ def structure_full(self):
161
+ """
162
+ Write routine to create atom structure static file for atom_type='full' that can be loaded by LAMMPS
163
+
164
+ Returns:
165
+
166
+ """
167
+ species_lammps_id_dict = self.get_lammps_id_dict(self.el_eam_lst)
168
+ self.molecule_ids = None
169
+ coords = self.rotate_positions(self._structure)
170
+
171
+ # extract electric charges from potential file
172
+ q_dict = {
173
+ species_name: self.potential.get_charge(species_name)
174
+ for species_name in set(self.structure.get_chemical_symbols())
175
+ }
176
+
177
+ bonds_lst, angles_lst = [], []
178
+ bond_type_lst, angle_type_lst = [], []
179
+ # Using a cutoff distance to draw the bonds instead of the number of neighbors
180
+ # Only if any bonds are defined
181
+ if len(self._bond_dict.keys()) > 0:
182
+ cutoff_list = list()
183
+ for val in self._bond_dict.values():
184
+ cutoff_list.append(np.max(val["cutoff_list"]))
185
+ max_cutoff = np.max(cutoff_list)
186
+ # Calculate neighbors only once
187
+ neighbors = get_neighbors(
188
+ structure=self.structure, cutoff_radius=max_cutoff
189
+ )
190
+
191
+ # Draw bonds between atoms is defined in self._bond_dict
192
+ # Go through all elements for which bonds are defined
193
+ for element, val in self._bond_dict.items():
194
+ el_1_list = select_index(structure=self._structure, element=element)
195
+ if el_1_list is not None:
196
+ if len(el_1_list) > 0:
197
+ for i, v in enumerate(val["element_list"]):
198
+ el_2_list = select_index(
199
+ structure=self._structure, element=v
200
+ )
201
+ cutoff_dist = val["cutoff_list"][i]
202
+ for j, ind in enumerate(
203
+ np.array(neighbors.indices, dtype=object)[el_1_list]
204
+ ):
205
+ # Only chose those indices within the cutoff distance and which belong
206
+ # to the species defined in the element_list
207
+ # i is the index of each bond type, and j is the element index
208
+ id_el = el_1_list[j]
209
+ bool_1 = (
210
+ np.array(neighbors.distances, dtype=object)[id_el]
211
+ <= cutoff_dist
212
+ )
213
+ act_ind = ind[bool_1]
214
+ bool_2 = np.in1d(act_ind, el_2_list)
215
+ final_ind = act_ind[bool_2]
216
+ # Get the bond and angle type
217
+ bond_type = val["bond_type_list"][i]
218
+ angle_type = val["angle_type_list"][i]
219
+ # Draw only maximum allowed bonds
220
+ final_ind = final_ind[: val["max_bond_list"][i]]
221
+ for fi in final_ind:
222
+ bonds_lst.append([id_el + 1, fi + 1])
223
+ bond_type_lst.append(bond_type)
224
+ # Draw angles if at least 2 bonds are present and if an angle type is defined for this
225
+ # particular set of bonds
226
+ if (
227
+ len(final_ind) >= 2
228
+ and val["angle_type_list"][i] is not None
229
+ ):
230
+ angles_lst.append(
231
+ [final_ind[0] + 1, id_el + 1, final_ind[1] + 1]
232
+ )
233
+ angle_type_lst.append(angle_type)
234
+
235
+ if len(bond_type_lst) == 0:
236
+ num_bond_types = 0
237
+ else:
238
+ num_bond_types = int(np.max(bond_type_lst))
239
+ if len(angle_type_lst) == 0:
240
+ num_angle_types = 0
241
+ else:
242
+ num_angle_types = int(np.max(angle_type_lst))
243
+
244
+ atoms = "Atoms \n\n"
245
+
246
+ # format: atom-ID, molecule-ID, atom_type, q, x, y, z
247
+ format_str = "{0:d} {1:d} {2:d} {3:f} {4:f} {5:f} {6:f}"
248
+ el_lst = self.structure.get_chemical_symbols()
249
+ for id_atom, (el, coord) in enumerate(zip(el_lst, coords)):
250
+ atoms += (
251
+ format_str.format(
252
+ id_atom + 1,
253
+ self.molecule_ids[id_atom],
254
+ species_lammps_id_dict[el],
255
+ q_dict[el],
256
+ coord[0],
257
+ coord[1],
258
+ coord[2],
259
+ )
260
+ + "\n"
261
+ )
262
+
263
+ if len(bonds_lst) > 0:
264
+ bonds_str = "Bonds \n\n"
265
+ for i_bond, id_vec in enumerate(bonds_lst):
266
+ bonds_str += (
267
+ "{0:d} {1:d} {2:d} {3:d}".format(
268
+ i_bond + 1, bond_type_lst[i_bond], id_vec[0], id_vec[1]
269
+ )
270
+ + "\n"
271
+ )
272
+ else:
273
+ bonds_str = "\n"
274
+
275
+ if len(angles_lst) > 0:
276
+ angles_str = "Angles \n\n"
277
+ for i_angle, id_vec in enumerate(angles_lst):
278
+ angles_str += (
279
+ "{0:d} {1:d} {2:d} {3:d} {4:d}".format(
280
+ i_angle + 1,
281
+ angle_type_lst[i_angle],
282
+ id_vec[0],
283
+ id_vec[1],
284
+ id_vec[2],
285
+ )
286
+ + "\n"
287
+ )
288
+ else:
289
+ angles_str = "\n"
290
+ return (
291
+ self.lammps_header(
292
+ structure=self.structure,
293
+ cell_dimensions=self.simulation_cell(),
294
+ species_lammps_id_dict=species_lammps_id_dict,
295
+ nbonds=len(bonds_lst),
296
+ nangles=len(angles_lst),
297
+ nbond_types=num_bond_types,
298
+ nangle_types=num_angle_types,
299
+ )
300
+ + " \n"
301
+ + atoms
302
+ + "\n"
303
+ + bonds_str
304
+ + "\n"
305
+ + angles_str
306
+ + "\n"
307
+ )
308
+
309
+
310
+ def get_bonds(
311
+ structure: Atoms, radius=np.inf, max_shells=None, prec=0.1, num_neighbors=20
312
+ ):
313
+ """
314
+
315
+ Args:
316
+ structure (ase.Atoms):
317
+ radius (float):
318
+ max_shells:
319
+ prec: minimum distance between any two clusters (if smaller considered to be single cluster)
320
+ num_neighbors:
321
+
322
+ Returns:
323
+
324
+ """
325
+ neighbors = get_neighbors(
326
+ structure=structure,
327
+ cutoff_radius=radius,
328
+ num_neighbors=num_neighbors,
329
+ tolerance=2,
330
+ id_list=None,
331
+ width_buffer=1.2,
332
+ allow_ragged=None,
333
+ mode="ragged",
334
+ norm_order=2,
335
+ )
336
+ return neighbors.get_bonds(radius=radius, max_shells=max_shells, prec=prec)