interfacemethod 2.0__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,29 @@
1
+ from interfacemethod.helper import (
2
+ initialise_iterators,
3
+ round_temperature_next,
4
+ get_strain_lst,
5
+ )
6
+ from interfacemethod.lammps import (
7
+ minimize_structure_positions,
8
+ minimize_structure_volume,
9
+ npt_solid,
10
+ npt_liquid,
11
+ run_npt_step,
12
+ run_strain_point,
13
+ )
14
+ from interfacemethod.plot import (
15
+ check_for_holes,
16
+ plot_solid_liquid_ratio,
17
+ plot_equilibration,
18
+ plot_melting_point_prediction,
19
+ ratio_selection,
20
+ )
21
+ from interfacemethod.structure import (
22
+ check_diamond,
23
+ analyse_minimized_structure,
24
+ remove_selective_dynamics,
25
+ )
26
+ from interfacemethod.workflow import (
27
+ bisection_step,
28
+ validate_convergence,
29
+ )
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '2.0'
22
+ __version_tuple__ = version_tuple = (2, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,88 @@
1
+ from ase.atoms import Atoms
2
+ from ase.constraints import FixAtoms
3
+ import numpy as np
4
+
5
+
6
+ def initialise_iterators(project_parameter: dict):
7
+ return (
8
+ iter(project_parameter["timestep_lst"]),
9
+ iter(project_parameter["fit_range_lst"]),
10
+ iter(project_parameter["nve_run_time_steps_lst"]),
11
+ )
12
+
13
+
14
+ def freeze_one_half(basis: Atoms) -> FixAtoms:
15
+ """
16
+ Split the structure into two parts along the z-axis and then freeze the position of the atoms
17
+ of the upper part (z>0.5) by attaching an ASE FixAtoms constraint to them.
18
+
19
+ Args:
20
+ basis (ase.atoms.Atoms): Atomistic structure object
21
+
22
+ Returns:
23
+ ase.constraints.FixAtoms: Constraint fixing the upper half of the structure
24
+ """
25
+ basis = basis.copy()
26
+ z = basis.get_scaled_positions()[:, 2]
27
+ return FixAtoms(indices=np.where(z >= 0.5)[0])
28
+
29
+
30
+ def round_temperature_next(temperature_next: float) -> float:
31
+ """
32
+ Round temperature to the last two dicits
33
+
34
+ Args:
35
+ temperature_next (float): Temperature
36
+
37
+ Returns:
38
+ float: rounded temperature
39
+ """
40
+ return np.round(temperature_next, 2)
41
+
42
+
43
+ def get_nve_job_name(
44
+ temperature_next: float,
45
+ strain: float,
46
+ steps_lst: list[int],
47
+ nve_run_time_steps: int,
48
+ ):
49
+ temperature_next = round_temperature_next(temperature_next)
50
+ temp_str = str(temperature_next).replace(".", "_")
51
+ strain_str = str(strain).replace(".", "_")
52
+ steps_str = str(steps_lst.index(nve_run_time_steps))
53
+ return "ham_nve_" + strain_str + "_" + temp_str + "_" + steps_str
54
+
55
+
56
+ def get_center_point(strain_result_lst=None, pressure_result_lst=None, center=None):
57
+ if (
58
+ strain_result_lst is not None
59
+ and len(strain_result_lst) != 0
60
+ and pressure_result_lst is not None
61
+ and len(pressure_result_lst) != 0
62
+ ):
63
+ center_point = np.round(
64
+ np.roots(np.polyfit(strain_result_lst, pressure_result_lst, 1))[0], 2
65
+ )
66
+ elif center is not None:
67
+ center_point = center
68
+ else:
69
+ center_point = 1.0
70
+ return center_point
71
+
72
+
73
+ def get_strain_lst(
74
+ fit_range=0.02,
75
+ points=21,
76
+ strain_result_lst=None,
77
+ pressure_result_lst=None,
78
+ center=None,
79
+ ):
80
+ center_point = get_center_point(
81
+ strain_result_lst=strain_result_lst,
82
+ pressure_result_lst=pressure_result_lst,
83
+ center=center,
84
+ )
85
+ return [
86
+ np.round(s, 3)
87
+ for s in np.linspace(center_point - fit_range, center_point + fit_range, points)
88
+ ]
@@ -0,0 +1,379 @@
1
+ import os
2
+ from ase.atoms import Atoms
3
+ from lammpsparser import lammps_file_interface_function
4
+ import numpy as np
5
+ import pandas
6
+
7
+ from interfacemethod.helper import (
8
+ freeze_one_half,
9
+ round_temperature_next,
10
+ get_nve_job_name,
11
+ )
12
+ from interfacemethod.result import StrainPointResult
13
+
14
+
15
+ def structure_from_parsed_output(
16
+ initial_structure: Atoms, parsed_output: dict, *, wrap: bool = False
17
+ ) -> Atoms:
18
+ """Construct an `Atoms` object from parsed output data.
19
+
20
+ Args:
21
+ initial_structure: The initial atomic structure to use as a template.
22
+ parsed_output: Parsed output containing atomic positions, cell, and indices.
23
+ wrap: Whether to wrap the atomic positions to the simulation cell (default is False).
24
+ Keeping the unwrapped positions is more beneficial if structures are passed between
25
+ different LAMMPS simulations in one workflow to ensure continuity.
26
+
27
+ Returns:
28
+ An `Atoms` object with updated positions and cell.
29
+
30
+ Example:
31
+ >>> new_atoms = structure_from_parsed_output(atoms, lammps_output)
32
+
33
+ """
34
+ atoms_copy = initial_structure.copy()
35
+ atoms_copy.set_array("indices", parsed_output["generic"]["indices"][-1])
36
+ atoms_copy.set_cell(parsed_output["generic"]["cells"][-1], scale_atoms=True)
37
+ atoms_copy.set_positions(parsed_output["generic"]["positions"][-1])
38
+ atoms_copy.set_velocities(parsed_output["generic"]["velocities"][-1])
39
+ atoms_copy.set_pbc(True)
40
+ if wrap:
41
+ atoms_copy.wrap()
42
+
43
+ return atoms_copy
44
+
45
+
46
+ def minimize_structure_positions(
47
+ structure: Atoms,
48
+ potential: pandas.DataFrame,
49
+ project_path: str,
50
+ max_iter: int = 1000,
51
+ lmp_command: str = "lmp -in lmp.in",
52
+ ) -> Atoms:
53
+ """Relax the atomic positions of a structure at fixed cell shape."""
54
+ _, parsed_output, _ = lammps_file_interface_function(
55
+ working_directory=os.path.join(project_path, "minimize_pos"),
56
+ structure=structure,
57
+ potential=potential,
58
+ calc_mode="minimize",
59
+ calc_kwargs={
60
+ "max_iter": max_iter,
61
+ "ionic_energy_tolerance": 1.0e-9,
62
+ "ionic_force_tolerance": 1.0e-8,
63
+ "n_print": max_iter,
64
+ },
65
+ lmp_command=lmp_command,
66
+ )
67
+ return structure_from_parsed_output(structure, parsed_output, wrap=True)
68
+
69
+
70
+ def minimize_structure_volume(
71
+ structure: Atoms,
72
+ potential: pandas.DataFrame,
73
+ project_path: str,
74
+ max_iter: int = 1000,
75
+ lmp_command: str = "lmp -in lmp.in",
76
+ ) -> Atoms:
77
+ """Relax both atomic positions and cell volume of a structure at zero pressure."""
78
+ _, parsed_output, _ = lammps_file_interface_function(
79
+ working_directory=os.path.join(project_path, "minimize_vol"),
80
+ structure=structure,
81
+ potential=potential,
82
+ calc_mode="minimize",
83
+ calc_kwargs={
84
+ "max_iter": max_iter,
85
+ "ionic_energy_tolerance": 1.0e-9,
86
+ "ionic_force_tolerance": 1.0e-8,
87
+ "n_print": max_iter,
88
+ "pressure": 0.0,
89
+ },
90
+ input_control_file={"fix": "ensemble all box/relax iso 0.0 vmax 0.001"},
91
+ lmp_command=lmp_command,
92
+ )
93
+ return structure_from_parsed_output(structure, parsed_output, wrap=True)
94
+
95
+
96
+ def run_npt_step(
97
+ structure: Atoms,
98
+ potential: pandas.DataFrame,
99
+ temperature: float,
100
+ seed: int,
101
+ project_path: str,
102
+ run_time_steps: int = 10000,
103
+ lmp_command: str = "lmp -in lmp.in",
104
+ ):
105
+ """
106
+ Calculate NPT ensemble at a given temperature using the job defined in the project parameters:
107
+ - job_type: Type of Simulation code to be used
108
+ - project: Project object used to create the job
109
+ - potential: Interatomic Potential
110
+ - queue (optional): HPC Job queue to be used
111
+
112
+ Args:
113
+ structure (ase.atoms.Atoms): Atomistic Structure object to be set to the job as input sturcture
114
+ temperature (float): Temperature of the Molecular dynamics calculation
115
+ run_time_steps (int): Number of Molecular dynamics steps
116
+
117
+ Returns:
118
+ Final Atomistic Structure object
119
+ """
120
+ _, parsed_output, _ = lammps_file_interface_function(
121
+ working_directory=os.path.join(
122
+ project_path, "temp_heating", str(temperature).replace(".", "_")
123
+ ),
124
+ structure=structure,
125
+ potential=potential,
126
+ calc_mode="md",
127
+ calc_kwargs={
128
+ "temperature": temperature,
129
+ "initial_temperature": temperature,
130
+ "temperature_damping_timescale": 100.0,
131
+ "pressure": 0.0,
132
+ "pressure_damping_timescale": 1000.0,
133
+ "n_print": run_time_steps,
134
+ "n_ionic_steps": run_time_steps,
135
+ "seed": seed,
136
+ },
137
+ input_control_file={
138
+ "fix": f"ensemble all npt temp {temperature} {temperature} 0.1 iso 0.0 0.0 1.0 couple xyz"
139
+ },
140
+ lmp_command=lmp_command,
141
+ )
142
+ return structure_from_parsed_output(structure, parsed_output, wrap=True)
143
+
144
+
145
+ def npt_solid(
146
+ temperature: float,
147
+ basis: Atoms,
148
+ project_parameter: dict,
149
+ project_path: str,
150
+ timestep: float = 1.0,
151
+ lmp_command: str = "lmp -in lmp.in",
152
+ ) -> Atoms:
153
+ """
154
+ Calculate NPT ensemble at a given temperature using lammps_file_interface_function.
155
+
156
+ Args:
157
+ temperature (float): Temperature of the Molecular dynamics calculation
158
+ basis (ase.atoms.Atoms): Atomistic Structure object to be used as input structure
159
+ project_parameter (dict): Dictionary with the project parameters
160
+ project_path (str): Working directory the calculation is executed in
161
+ timestep (float): Molecular dynamics time step
162
+
163
+ Returns:
164
+ Atoms: Final Atomistic Structure object
165
+ """
166
+ _, parsed_output, _ = lammps_file_interface_function(
167
+ working_directory=os.path.join(
168
+ project_path, "npt_solid", str(temperature).replace(".", "_")
169
+ ),
170
+ structure=basis,
171
+ potential=project_parameter["potential"],
172
+ calc_mode="md",
173
+ calc_kwargs={
174
+ "temperature": temperature,
175
+ "initial_temperature": temperature,
176
+ "temperature_damping_timescale": 100.0,
177
+ "time_step": timestep,
178
+ "pressure": 0.0,
179
+ "pressure_damping_timescale": 1000.0,
180
+ "n_print": project_parameter["run_time_steps"],
181
+ "n_ionic_steps": project_parameter["run_time_steps"],
182
+ "seed": project_parameter["seed"],
183
+ },
184
+ input_control_file={
185
+ "fix": f"ensemble all npt temp {temperature} {temperature} 0.1 iso 0.0 0.0 1.0 couple xyz"
186
+ },
187
+ lmp_command=lmp_command,
188
+ )
189
+ return structure_from_parsed_output(basis, parsed_output, wrap=True)
190
+
191
+
192
+ def setup_liquid_job(
193
+ job_name: str,
194
+ basis: Atoms,
195
+ temperature: float,
196
+ project_parameter: dict,
197
+ project_path: str,
198
+ timestep: float = 1.0,
199
+ lmp_command: str = "lmp -in lmp.in",
200
+ ):
201
+ """
202
+ Calculate NPT ensemble at a given temperature while freezing the position of the atoms
203
+ of the upper part (z>0.5) using lammps_file_interface_function. Only the z-component of
204
+ the pressure is coupled to a barostat, matching the previous fix_z_dir behaviour.
205
+
206
+ Args:
207
+ job_name (str): Name used for the working directory of the calculation
208
+ basis (ase.atoms.Atoms): Atomistic Structure object to be used as input structure
209
+ temperature (float): Temperature of the Molecular dynamics calculation
210
+ project_parameter (dict): Dictionary with the project parameters
211
+ project_path (str): Working directory the calculation is executed in
212
+ timestep (float): Molecular dynamics time step
213
+
214
+ Returns:
215
+ Atoms: Final Atomistic Structure object
216
+ """
217
+ _, parsed_output, _ = lammps_file_interface_function(
218
+ working_directory=os.path.join(project_path, "liquid", job_name),
219
+ structure=basis,
220
+ potential=project_parameter["potential"],
221
+ calc_mode="md",
222
+ calc_kwargs={
223
+ "temperature": temperature,
224
+ "initial_temperature": temperature,
225
+ "temperature_damping_timescale": 100.0,
226
+ "time_step": timestep,
227
+ "pressure": [None, None, 0.0],
228
+ "pressure_damping_timescale": 1000.0,
229
+ "n_print": project_parameter["run_time_steps"],
230
+ "n_ionic_steps": project_parameter["run_time_steps"],
231
+ "seed": project_parameter["seed"],
232
+ },
233
+ lmp_command=lmp_command,
234
+ )
235
+ return structure_from_parsed_output(basis, parsed_output, wrap=True)
236
+
237
+
238
+ def npt_liquid(
239
+ temperature_solid: float,
240
+ temperature_liquid: float,
241
+ basis: Atoms,
242
+ project_parameter: dict,
243
+ project_path: str,
244
+ lmp_command: str = "lmp -in lmp.in",
245
+ timestep: float = 1.0,
246
+ ):
247
+ """
248
+ Calculate NPT ensemble at a given temperature while initially freezing the position of the atoms
249
+ of the upper part (z>0.5) and afterwards calculating the full sample at a lower temperature.
250
+ These steps are used to construct the solid liquid interface as part of the coexistence approach.
251
+
252
+ Args:
253
+ temperature_solid (float): Temperature to simulate the whole structure
254
+ temperature_liquid (float): Temperature to simulate the upper half of the structure
255
+ basis (ase.atoms.Atoms): Atomistic Structure object to be used as input structure
256
+ project_parameter (dict): Dictionary with the project parameters
257
+ project_path (str): Working directory the calculation is executed in
258
+ timestep (float): Molecular dynamics time step
259
+
260
+ Returns:
261
+ Atoms: Final Atomistic Structure object
262
+ """
263
+ constraint = freeze_one_half(basis)
264
+ basis.set_constraint(constraint)
265
+ structure_liquid_high = setup_liquid_job(
266
+ job_name="high_" + str(temperature_liquid).replace(".", "_"),
267
+ basis=basis,
268
+ temperature=temperature_liquid,
269
+ project_parameter=project_parameter,
270
+ project_path=project_path,
271
+ timestep=timestep,
272
+ lmp_command=lmp_command,
273
+ )
274
+ structure_liquid_high.set_constraint(constraint)
275
+ structure_liquid_low = setup_liquid_job(
276
+ job_name="low_" + str(temperature_solid).replace(".", "_"),
277
+ basis=structure_liquid_high,
278
+ temperature=temperature_solid,
279
+ project_parameter=project_parameter,
280
+ project_path=project_path,
281
+ timestep=timestep,
282
+ lmp_command=lmp_command,
283
+ )
284
+ return structure_liquid_low
285
+
286
+
287
+ def get_press(parsed_output, step: int = 20):
288
+ """
289
+ Args:
290
+ parsed_output (dict): Output parsed from a LAMMPS MD calculation via
291
+ lammps_file_interface_function
292
+ step (int): Number of steps counted from the end of the trajectory to average over
293
+ """
294
+ return np.mean(
295
+ parsed_output["generic"]["pressures"][step:, :, :].diagonal(0, 2), axis=1
296
+ )
297
+
298
+
299
+ def run_strain_point(
300
+ strain: float,
301
+ basis_relative: Atoms,
302
+ temperature_next: float,
303
+ nve_run_time_steps: int,
304
+ project_parameter: dict,
305
+ project_path: str,
306
+ timestep: float = 1.0,
307
+ lmp_command: str = "lmp -in lmp.in",
308
+ ) -> StrainPointResult:
309
+ """
310
+ Apply one strain to the interface structure along z and measure the resulting pressure and
311
+ temperature via a short NVT equilibration followed by an NVE production run.
312
+
313
+ Every strain point is independent of every other, so this is the unit of work for the strain
314
+ scan: a `for strain in strain_lst: run_strain_point(strain, ...)` loop can be replaced with
315
+ `executor.map(...)` on a `concurrent.futures.ProcessPoolExecutor` or an `executorlib.Executor`
316
+ to run the scan in parallel.
317
+ """
318
+ temperature_next = round_temperature_next(temperature_next)
319
+ job_name = get_nve_job_name(
320
+ temperature_next=temperature_next,
321
+ strain=strain,
322
+ steps_lst=project_parameter["nve_run_time_steps_lst"],
323
+ nve_run_time_steps=nve_run_time_steps,
324
+ )
325
+ nvt_working_directory = os.path.join(
326
+ project_path, "strain_circle", job_name.replace("nve", "nvt")
327
+ )
328
+ nve_working_directory = os.path.join(project_path, "strain_circle", job_name)
329
+ basis_strain = basis_relative.copy()
330
+ cell = basis_strain.cell.copy()
331
+ cell[2, 2] *= strain
332
+ basis_strain.set_cell(cell=cell, scale_atoms=True)
333
+ lammps_file_interface_function(
334
+ working_directory=nvt_working_directory,
335
+ structure=basis_strain,
336
+ potential=project_parameter["potential"],
337
+ calc_mode="md",
338
+ calc_kwargs={
339
+ "temperature": temperature_next,
340
+ "initial_temperature": temperature_next,
341
+ "time_step": timestep,
342
+ "temperature_damping_timescale": 100.0,
343
+ "n_print": project_parameter["nvt_run_time_steps"],
344
+ "n_ionic_steps": project_parameter["nvt_run_time_steps"],
345
+ "seed": project_parameter["seed"],
346
+ },
347
+ input_control_file={
348
+ "fix": f"ensemble all nvt temp {temperature_next} {temperature_next} 0.1 drag 1"
349
+ },
350
+ write_restart_file=True,
351
+ lmp_command=lmp_command,
352
+ )
353
+ restart_file_path = os.path.join(nvt_working_directory, "restart.out")
354
+ _, parsed_output, _ = lammps_file_interface_function(
355
+ working_directory=nve_working_directory,
356
+ structure=basis_strain,
357
+ potential=project_parameter["potential"],
358
+ calc_mode="md",
359
+ calc_kwargs={
360
+ "time_step": timestep,
361
+ "n_print": max(1, int(nve_run_time_steps / 100)),
362
+ "n_ionic_steps": nve_run_time_steps,
363
+ "seed": project_parameter["seed"],
364
+ },
365
+ read_restart_file=True,
366
+ restart_file=restart_file_path,
367
+ dump_final_structure=True,
368
+ lmp_command=lmp_command,
369
+ )
370
+ structure_nve = structure_from_parsed_output(basis_strain, parsed_output, wrap=True)
371
+ return StrainPointResult(
372
+ strain=strain,
373
+ pressure=np.mean(get_press(parsed_output=parsed_output, step=-20)),
374
+ pressure_std=np.std(get_press(parsed_output=parsed_output, step=-20)),
375
+ temperature=np.mean(parsed_output["generic"]["temperature"][-20:]),
376
+ temperature_std=np.std(parsed_output["generic"]["temperature"][-20:]),
377
+ structure=structure_nve,
378
+ parsed_output=parsed_output,
379
+ )