From 3d0fb7384bf07c090c287510e0c2492b958fe7cb Mon Sep 17 00:00:00 2001 From: Kyle Daniel Miller Date: Thu, 9 Nov 2023 19:19:37 -0500 Subject: [PATCH 1/8] added parameter to allow Structure.interpolate to extrapolate beyond the end_structure --- pymatgen/core/structure.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 4f6dd1938ca..00add45f7e0 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2138,6 +2138,7 @@ def interpolate( interpolate_lattices: bool = False, pbc: bool = True, autosort_tol: float = 0, + extrapolation: float = 0, ) -> list[IStructure | Structure]: """Interpolate between this structure and end_structure. Useful for construction of NEB inputs. @@ -2157,6 +2158,8 @@ def interpolate( closest points in this particular structure. This is usually what you want in a NEB calculation. 0 implies no sorting. Otherwise, a 0.5 value usually works pretty well. + extrapolation (float): A fractional amount by which to extend the + interpolation beyond the end_structure Returns: List of interpolated structures. The starting and ending @@ -2212,7 +2215,7 @@ def interpolate( end_coords = sorted_end_coords - vec = end_coords - start_coords + vec = (1+extrapolation)*(end_coords - start_coords) if pbc: vec[:, self.pbc] -= np.round(vec[:, self.pbc]) sp = self.species_and_occu @@ -2222,7 +2225,7 @@ def interpolate( # interpolate lattice matrices using polar decomposition # u is a unitary rotation, p is stretch u, p = polar(np.dot(end_structure.lattice.matrix.T, np.linalg.inv(self.lattice.matrix.T))) - lvec = p - np.identity(3) + lvec = (1+extrapolation)*(p - np.identity(3)) lstart = self.lattice.matrix.T for x in images: From efbfe3ab140a3968b7add3f05fd87522f41008de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 00:28:50 +0000 Subject: [PATCH 2/8] pre-commit auto-fixes --- pymatgen/core/structure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 00add45f7e0..54863035a66 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2215,7 +2215,7 @@ def interpolate( end_coords = sorted_end_coords - vec = (1+extrapolation)*(end_coords - start_coords) + vec = (1 + extrapolation) * (end_coords - start_coords) if pbc: vec[:, self.pbc] -= np.round(vec[:, self.pbc]) sp = self.species_and_occu @@ -2225,7 +2225,7 @@ def interpolate( # interpolate lattice matrices using polar decomposition # u is a unitary rotation, p is stretch u, p = polar(np.dot(end_structure.lattice.matrix.T, np.linalg.inv(self.lattice.matrix.T))) - lvec = (1+extrapolation)*(p - np.identity(3)) + lvec = (1 + extrapolation) * (p - np.identity(3)) lstart = self.lattice.matrix.T for x in images: From 273dcb5ba0e2f95765fdddb720f18735673fbdc4 Mon Sep 17 00:00:00 2001 From: Kyle D Miller Date: Wed, 29 Nov 2023 12:19:09 -0600 Subject: [PATCH 3/8] generalized extrapolation to end_amplitude, a cofactor of the distortion vector connecting structure to end_structure. This allows for reverse inter/extrapolation as well as partial inter/extrapolation. With this argument, 0 implies no distortion, 1 implies full distortion to end_structure (default), 0.5 implies distortion to a point halfway between structure and end_structure, and -1 implies full distortion in the opposite direction to end_structure. --- pymatgen/core/structure.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 54863035a66..55b4bf81026 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2138,7 +2138,7 @@ def interpolate( interpolate_lattices: bool = False, pbc: bool = True, autosort_tol: float = 0, - extrapolation: float = 0, + end_amplitude: float = 1, ) -> list[IStructure | Structure]: """Interpolate between this structure and end_structure. Useful for construction of NEB inputs. @@ -2158,8 +2158,7 @@ def interpolate( closest points in this particular structure. This is usually what you want in a NEB calculation. 0 implies no sorting. Otherwise, a 0.5 value usually works pretty well. - extrapolation (float): A fractional amount by which to extend the - interpolation beyond the end_structure + end_amplitude (float): The fractional amplitude of the endpoint of the interpolation, or a cofactor of the distortion vector connecting structure to end_structure. Thus, 0 implies no distortion, 1 implies full distortion to end_structure (default), 0.5 implies distortion to a point halfway between structure and end_structure, and -1 implies full distortion in the opposite direction to end_structure. Returns: List of interpolated structures. The starting and ending @@ -2215,7 +2214,7 @@ def interpolate( end_coords = sorted_end_coords - vec = (1 + extrapolation) * (end_coords - start_coords) + vec = end_amplitude * (end_coords - start_coords) if pbc: vec[:, self.pbc] -= np.round(vec[:, self.pbc]) sp = self.species_and_occu @@ -2225,7 +2224,7 @@ def interpolate( # interpolate lattice matrices using polar decomposition # u is a unitary rotation, p is stretch u, p = polar(np.dot(end_structure.lattice.matrix.T, np.linalg.inv(self.lattice.matrix.T))) - lvec = (1 + extrapolation) * (p - np.identity(3)) + lvec = end_amplitude * (p - np.identity(3)) lstart = self.lattice.matrix.T for x in images: From 9ddb8f89af8d05e805932835314664bf9c02fb1b Mon Sep 17 00:00:00 2001 From: Kyle D Miller Date: Wed, 29 Nov 2023 12:23:54 -0600 Subject: [PATCH 4/8] added tests for new end_amplitude argument in Structure.interpolate() --- tests/core/test_structure.py | 84 +++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/tests/core/test_structure.py b/tests/core/test_structure.py index 4e166c9db60..093d2c5e91f 100644 --- a/tests/core/test_structure.py +++ b/tests/core/test_structure.py @@ -384,6 +384,42 @@ def test_interpolate(self): int_s_pbc = struct_pbc.interpolate(struct2_pbc, nimages=2) assert_allclose(int_s_pbc[1][0].frac_coords, [1.05, 1.05, 0.55]) + # Test end_amplitude =/= 1 + coords = [[0, 0, 0], [0.75, 0.5, 0.75]] + struct = IStructure(self.lattice, ["Si"] * 2, coords) + coords2 = [] + coords2.extend(([0, 0, 0], [0.5, 0.5, 0.5])) + struct2 = IStructure(self.struct.lattice, ["Si"] * 2, coords2) + # testing large positive values + interpolated_structs = struct.interpolate(struct2, 20, end_amplitude=2) + for inter_struct in interpolated_structs: + assert inter_struct is not None, "Interpolation Failed!" + assert interpolated_structs[0].lattice == inter_struct.lattice + assert_array_equal(interpolated_structs[0][1].frac_coords, [0.75, 0.5, 0.75]) + assert_array_equal(interpolated_structs[10][1].frac_coords, [0.5, 0.5, 0.5]) + assert_array_equal(interpolated_structs[20][1].frac_coords, [0.25, 0.5, 0.25]) + # testing large negative values + interpolated_structs = struct.interpolate(struct2, 20, end_amplitude=-2) + for inter_struct in interpolated_structs: + assert inter_struct is not None, "Interpolation Failed!" + assert interpolated_structs[0].lattice == inter_struct.lattice + assert_array_equal(interpolated_structs[0][1].frac_coords, [0.75, 0.5, 0.75]) + assert_array_equal(interpolated_structs[10][1].frac_coords, [1.0, 0.5, 1.0]) + assert_array_equal(interpolated_structs[20][1].frac_coords, [1.25, 0.5, 1.25]) + # testing partial interpolation + interpolated_structs = struct.interpolate(struct2, 5, end_amplitude=-0.5) + for inter_struct in interpolated_structs: + assert inter_struct is not None, "Interpolation Failed!" + assert interpolated_structs[0].lattice == inter_struct.lattice + assert_array_equal(interpolated_structs[0][1].frac_coords, [0.75, 0.5, 0.75]) + assert_array_equal(interpolated_structs[5][1].frac_coords, [0.875, 0.5, 0.875]) + # testing end_amplitude=0 + interpolated_structs = struct.interpolate(struct2, 5, end_amplitude=0) + for inter_struct in interpolated_structs: + assert inter_struct is not None, "Interpolation Failed!" + assert interpolated_structs[0].lattice == inter_struct.lattice + assert_array_equal(inter_struct[1].frac_coords, [0.75, 0.5, 0.75]) + def test_interpolate_lattice(self): coords = [[0, 0, 0], [0.75, 0.5, 0.75]] struct = IStructure(self.lattice, ["Si"] * 2, coords) @@ -397,22 +433,60 @@ def test_interpolate_lattice(self): assert_allclose(struct2.lattice.angles, int_s[2].lattice.angles) int_angles = [110.3976469, 94.5359731, 64.5165856] assert_allclose(int_angles, int_s[1].lattice.angles) + # Assert that volume is monotonic + assert struct2.volume >= int_s[1].volume + assert int_s[1].volume >= struct.volume + # Repeat for end_amplitude = 0.5 + int_s = struct.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=0.5) + assert_allclose(struct.lattice.abc, int_s[0].lattice.abc) + assert_allclose(struct.lattice.angles, int_s[0].lattice.angles) + assert_allclose(int_angles, int_s[2].lattice.angles) + # Assert that volume is monotonic + assert struct2.volume >= int_s[1].volume + assert int_s[1].volume >= struct.volume + + # Repeat for end_amplitude = 2 + int_s = struct.interpolate(struct2, 4, interpolate_lattices=True, end_amplitude=2) + assert_allclose(struct.lattice.abc, int_s[0].lattice.abc) + assert_allclose(struct.lattice.angles, int_s[0].lattice.angles) + assert_allclose(int_angles, int_s[1].lattice.angles) # Assert that volume is monotonic assert struct2.volume >= int_s[1].volume assert int_s[1].volume >= struct.volume + # Repeat for end_amplitude = -1 + int_s = struct.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=-1) + assert_allclose(struct.lattice.abc, int_s[0].lattice.abc) + assert_allclose(struct.lattice.angles, int_s[0].lattice.angles) + int_angles = [127.72010946461334, 86.27613506707404, 56.52554566317311] + assert_allclose(int_angles, int_s[1].lattice.angles) + # Assert that volume is monotonic (should be shrinking for negative end_amplitude) + assert int_s[1].volume <= struct.volume + # Assert that coordinate shift is reversed + assert_array_equal(int_s[1][1].frac_coords, [0.875, 0.5, 0.875]) + assert_array_equal(int_s[2][1].frac_coords, [1.0, 0.5, 1.0]) + def test_interpolate_lattice_rotation(self): l1 = Lattice([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) l2 = Lattice([[-1.01, 0, 0], [0, -1.01, 0], [0, 0, 1]]) coords = [[0, 0, 0], [0.75, 0.5, 0.75]] struct1 = IStructure(l1, ["Si"] * 2, coords) struct2 = IStructure(l2, ["Si"] * 2, coords) - int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True) - - # Assert that volume is monotonic - assert struct2.volume >= int_s[1].volume - assert int_s[1].volume >= struct1.volume + + # Test positive end_amplitudes + for end_amplitude in [0,0.5,1,2]: + int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=end_amplitude) + # Assert that volume is monotonic + assert struct2.volume >= int_s[1].volume + assert int_s[1].volume >= struct1.volume + + # Test negative end_amplitudes + for end_amplitude in [-2,-0.5,0]: + int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=end_amplitude) + # Assert that volume is monotonic + assert struct2.volume >= int_s[1].volume + assert int_s[1].volume <= struct1.volume def test_get_primitive_structure(self): coords = [[0, 0, 0], [0.5, 0.5, 0], [0, 0.5, 0.5], [0.5, 0, 0.5]] From 492cc8204046c59c148c54724fe4bc61f96e6789 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:25:04 +0000 Subject: [PATCH 5/8] pre-commit auto-fixes --- tests/core/test_structure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/test_structure.py b/tests/core/test_structure.py index 093d2c5e91f..961520aaafd 100644 --- a/tests/core/test_structure.py +++ b/tests/core/test_structure.py @@ -473,16 +473,16 @@ def test_interpolate_lattice_rotation(self): coords = [[0, 0, 0], [0.75, 0.5, 0.75]] struct1 = IStructure(l1, ["Si"] * 2, coords) struct2 = IStructure(l2, ["Si"] * 2, coords) - + # Test positive end_amplitudes - for end_amplitude in [0,0.5,1,2]: + for end_amplitude in [0, 0.5, 1, 2]: int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=end_amplitude) # Assert that volume is monotonic assert struct2.volume >= int_s[1].volume assert int_s[1].volume >= struct1.volume # Test negative end_amplitudes - for end_amplitude in [-2,-0.5,0]: + for end_amplitude in [-2, -0.5, 0]: int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True, end_amplitude=end_amplitude) # Assert that volume is monotonic assert struct2.volume >= int_s[1].volume From cda24c98cebb55cf75ca546111fab3915b95b659 Mon Sep 17 00:00:00 2001 From: Kyle D Miller Date: Wed, 29 Nov 2023 12:59:28 -0600 Subject: [PATCH 6/8] fixed too-long comment line --- pymatgen/core/structure.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 55b4bf81026..14d41b553b2 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2158,7 +2158,13 @@ def interpolate( closest points in this particular structure. This is usually what you want in a NEB calculation. 0 implies no sorting. Otherwise, a 0.5 value usually works pretty well. - end_amplitude (float): The fractional amplitude of the endpoint of the interpolation, or a cofactor of the distortion vector connecting structure to end_structure. Thus, 0 implies no distortion, 1 implies full distortion to end_structure (default), 0.5 implies distortion to a point halfway between structure and end_structure, and -1 implies full distortion in the opposite direction to end_structure. + end_amplitude (float): The fractional amplitude of the endpoint + of the interpolation, or a cofactor of the distortion vector + connecting structure to end_structure. Thus, 0 implies no + distortion, 1 implies full distortion to end_structure + (default), 0.5 implies distortion to a point halfway + between structure and end_structure, and -1 implies full + distortion in the opposite direction to end_structure. Returns: List of interpolated structures. The starting and ending From 4b16b74d3049175ee077239a7d0d8d1823e35186 Mon Sep 17 00:00:00 2001 From: Kyle D Miller Date: Wed, 29 Nov 2023 13:02:20 -0600 Subject: [PATCH 7/8] added note about consistency between start and end structures to docstring -- ensuring consistency is required for obtaining useful results --- pymatgen/core/structure.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 14d41b553b2..3b12cf19671 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2141,11 +2141,13 @@ def interpolate( end_amplitude: float = 1, ) -> list[IStructure | Structure]: """Interpolate between this structure and end_structure. Useful for - construction of NEB inputs. + construction of NEB inputs. To obtain useful results, the cell setting + and order of sites must consistent across the start and end structures. Args: end_structure (Structure): structure to interpolate between this - structure and end. + structure and end. Must be in the same setting and have the + same site ordering to yield useful results. nimages (int,list): No. of interpolation images or a list of interpolation images. Defaults to 10 images. interpolate_lattices (bool): Whether to interpolate the lattices. From 56e0cc4a86e485d9c66cff38cd542df5acfc2cd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 19:05:35 +0000 Subject: [PATCH 8/8] pre-commit auto-fixes --- pymatgen/core/structure.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 3b12cf19671..e44acad6904 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -2160,12 +2160,12 @@ def interpolate( closest points in this particular structure. This is usually what you want in a NEB calculation. 0 implies no sorting. Otherwise, a 0.5 value usually works pretty well. - end_amplitude (float): The fractional amplitude of the endpoint - of the interpolation, or a cofactor of the distortion vector - connecting structure to end_structure. Thus, 0 implies no - distortion, 1 implies full distortion to end_structure - (default), 0.5 implies distortion to a point halfway - between structure and end_structure, and -1 implies full + end_amplitude (float): The fractional amplitude of the endpoint + of the interpolation, or a cofactor of the distortion vector + connecting structure to end_structure. Thus, 0 implies no + distortion, 1 implies full distortion to end_structure + (default), 0.5 implies distortion to a point halfway + between structure and end_structure, and -1 implies full distortion in the opposite direction to end_structure. Returns: