Skip to content

Commit

Permalink
Merge branch 'master' of github.com:materialsproject/pymatgen
Browse files Browse the repository at this point in the history
  • Loading branch information
shyuep committed Jan 28, 2025
2 parents 9656ff9 + f82ce1f commit 90a1ef7
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 21 deletions.
10 changes: 5 additions & 5 deletions src/pymatgen/core/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4635,24 +4635,24 @@ def rotate_sites(

return self

def perturb(self, distance: float, min_distance: float | None = None) -> Self:
def perturb(self, distance: float, min_distance: float | None = None, seed: int = 0) -> Self:
"""Perform a random perturbation of the sites in a structure to break
symmetries. Modifies the structure in place.
Args:
distance (float): Distance in angstroms by which to perturb each site.
min_distance (None, int, or float): if None, all displacements will
be equal amplitude. If int or float, perturb each site a
distance drawn from the uniform distribution between
'min_distance' and 'distance'.
be equal amplitude. If int or float, perturb each site a distance drawn
from the uniform distribution between 'min_distance' and 'distance'.
seed (int): Seed for the random number generator. Defaults to 0.
Returns:
Structure: self with perturbed sites.
"""

def get_rand_vec():
# Deal with zero vectors
rng = np.random.default_rng()
rng = np.random.default_rng(seed=seed)
vector = rng.standard_normal(3)
vnorm = np.linalg.norm(vector)
dist = distance
Expand Down
52 changes: 36 additions & 16 deletions tests/core/test_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1213,22 +1213,42 @@ def test_propertied_structure(self):
assert dct == struct.as_dict()

def test_perturb(self):
dist = 0.1
pre_perturbation_sites = self.struct.copy()
returned = self.struct.perturb(distance=dist)
assert returned is self.struct
post_perturbation_sites = self.struct.sites

for idx, site in enumerate(pre_perturbation_sites):
assert site.distance(post_perturbation_sites[idx]) == approx(dist), "Bad perturbation distance"

structure2 = pre_perturbation_sites.copy()
structure2.perturb(distance=dist, min_distance=0)
post_perturbation_sites2 = structure2.sites

for idx, site in enumerate(pre_perturbation_sites):
assert site.distance(post_perturbation_sites2[idx]) <= dist
assert site.distance(post_perturbation_sites2[idx]) >= 0
struct = self.get_structure("Li2O")
struct_orig = struct.copy()
struct.perturb(0.1)
# Ensure all sites were perturbed by a distance of at most 0.1 Angstroms
for site, site_orig in zip(struct, struct_orig, strict=True):
cart_dist = site.distance(site_orig)
# allow 1e-6 to account for numerical precision
assert cart_dist <= 0.1 + 1e-6, f"Distance {cart_dist} > 0.1"

# Test that same seed gives same perturbation
s1 = self.get_structure("Li2O")
s2 = self.get_structure("Li2O")
s1.perturb(0.1, seed=42)
s2.perturb(0.1, seed=42)
for site1, site2 in zip(s1, s2, strict=True):
assert site1.distance(site2) < 1e-7 # should be exactly equal up to numerical precision

# Test that different seeds give different perturbations
s3 = self.get_structure("Li2O")
s3.perturb(0.1, seed=100)
any_different = False
for site1, site3 in zip(s1, s3, strict=True):
if site1.distance(site3) > 1e-7:
any_different = True
break
assert any_different, "Different seeds should give different perturbations"

# Test min_distance
s4 = self.get_structure("Li2O")
s4.perturb(0.1, min_distance=0.05, seed=42)
any_different = False
for site1, site4 in zip(s1, s4, strict=True):
if site1.distance(site4) > 1e-7:
any_different = True
break
assert any_different, "Using min_distance should give different perturbations"

def test_add_oxidation_state_by_element(self):
oxidation_states = {"Si": -4}
Expand Down

0 comments on commit 90a1ef7

Please sign in to comment.