Skip to content

Commit 2ee022c

Browse files
authored
Merge pull request #7 from bjmorgan/dev
random structure generation
2 parents db034c2 + 793016c commit 2ee022c

11 files changed

Lines changed: 1192 additions & 28 deletions

File tree

bsym/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def tolist(self) -> list[int]:
185185
Returns:
186186
(List)
187187
"""
188-
return list(self.vector)
188+
return self.vector.tolist() # type: ignore[no-any-return]
189189

190190
def pprint(self) -> None:
191191
print(" ".join([str(e) for e in self.tolist()]))

bsym/configuration_space.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,59 @@ def unique_configurations(self,
121121
)
122122
return self.enumerate_configurations(generator, verbose=verbose)
123123

124+
def random_unique_configurations(
125+
self,
126+
site_distribution: dict[int, int],
127+
n: int,
128+
sampling: str = 'degeneracy_weighted',
129+
seed: int | None = None,
130+
) -> list[Configuration]:
131+
"""Generate n random symmetry-inequivalent configurations.
132+
133+
Args:
134+
site_distribution: Dictionary mapping species labels to counts.
135+
n: Number of unique configurations to generate.
136+
sampling: Sampling method. Either 'degeneracy_weighted' (default) or
137+
'uniform'. 'degeneracy_weighted' samples configurations with
138+
probability proportional to their degeneracy. 'uniform' samples
139+
uniformly over equivalence classes.
140+
seed: Random seed for reproducibility.
141+
142+
Returns:
143+
List of n unique Configuration objects with count attributes set.
144+
145+
Raises:
146+
ValueError: If sampling is not 'degeneracy_weighted' or 'uniform'.
147+
"""
148+
if sampling not in ('degeneracy_weighted', 'uniform'):
149+
raise ValueError(
150+
f"sampling must be 'degeneracy_weighted' or 'uniform', got '{sampling}'"
151+
)
152+
153+
rng = np.random.default_rng(seed)
154+
seen: set[bytes] = set()
155+
unique_configs: list[Configuration] = []
156+
157+
while len(unique_configs) < n:
158+
config = self._generate_random_configuration(site_distribution, rng)
159+
config_hash = config.as_bytes()
160+
161+
if config_hash in seen:
162+
continue
163+
164+
equivalents = config.get_byte_equivalents(self.symmetry_group)
165+
degeneracy = len(equivalents)
166+
167+
if sampling == 'uniform':
168+
if rng.random() >= 1.0 / degeneracy:
169+
continue
170+
171+
seen.update(equivalents)
172+
config.count = degeneracy
173+
unique_configs.append(config)
174+
175+
return unique_configs
176+
124177
def unique_colourings(self, colours, verbose=False):
125178
"""
126179
Find the symmetry inequivalent colourings for a given number of 'colours'.
@@ -221,6 +274,38 @@ def unique_configurations_by_composition(self,
221274
print(f" Total unique configurations: {sum(len(configs) for configs in results.values())}")
222275

223276
return results
277+
278+
def _generate_random_configuration(
279+
self,
280+
site_distribution: dict[int, int],
281+
rng: np.random.Generator,
282+
) -> Configuration:
283+
"""Generate a random configuration with the given site distribution.
284+
285+
Args:
286+
site_distribution: Dictionary mapping species labels to counts.
287+
rng: Random number generator.
288+
289+
Returns:
290+
A random Configuration with the specified distribution.
291+
"""
292+
n_sites = sum(site_distribution.values())
293+
config = np.empty(n_sites, dtype=int)
294+
available_indices = np.arange(n_sites)
295+
296+
# Process all but the last species
297+
species_list = list(site_distribution.items())
298+
for species, count in species_list[:-1]:
299+
selected = _select_random_indices(available_indices, count, rng)
300+
config[selected] = species
301+
# Remove selected indices from available
302+
available_indices = np.setdiff1d(available_indices, selected)
303+
304+
# Last species gets remaining indices
305+
last_species, _ = species_list[-1]
306+
config[available_indices] = last_species
307+
308+
return Configuration(config)
224309

225310
def apply_species_mapping(config, mapping_vector):
226311
"""
@@ -273,3 +358,20 @@ def permutation_as_config_number(p):
273358
tot *= 10
274359
tot += num
275360
return tot
361+
362+
def _select_random_indices(
363+
available_indices: np.ndarray,
364+
count: int,
365+
rng: np.random.Generator,
366+
) -> np.ndarray:
367+
"""Select count random indices from available_indices.
368+
369+
Args:
370+
available_indices: Array of indices to select from.
371+
count: Number of indices to select.
372+
rng: Random number generator.
373+
374+
Returns:
375+
Array of selected indices.
376+
"""
377+
return rng.choice(available_indices, size=count, replace=False)

bsym/interface/pymatgen.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,4 +496,63 @@ def unique_structure_substitutions_by_composition(
496496

497497
results[composition_tuple] = structures
498498

499-
return results
499+
return results
500+
501+
def random_unique_structure_substitutions(
502+
structure,
503+
to_substitute,
504+
site_distribution,
505+
n,
506+
sampling='degeneracy_weighted',
507+
seed=None,
508+
atol=1e-5,
509+
):
510+
"""
511+
Generate n random symmetry-unique structures by substituting sites in a pymatgen structure.
512+
513+
Args:
514+
structure (pymatgen.Structure): The parent structure.
515+
to_substitute (str): Atom label for the sites to be substituted.
516+
site_distribution (dict): Dictionary mapping species to counts, e.g. {'O': 8, 'F': 8}.
517+
n (int): Number of unique structures to generate.
518+
sampling (str): Sampling method. Either 'degeneracy_weighted' (default) or 'uniform'.
519+
'degeneracy_weighted' samples configurations with probability proportional
520+
to their degeneracy. 'uniform' samples uniformly over equivalence classes.
521+
seed (int, optional): Random seed for reproducibility.
522+
atol (float): Tolerance factor for coordinate mapping. Default=1e-5.
523+
524+
Returns:
525+
list[Structure]: A list of n unique Structure objects. Each has a
526+
`number_of_equivalent_configurations` attribute.
527+
"""
528+
site_substitution_index = list(structure.indices_from_symbol(to_substitute))
529+
530+
config_space = configuration_space_from_structure(
531+
structure,
532+
subset=site_substitution_index,
533+
atol=atol
534+
)
535+
536+
numeric_site_distribution, numeric_site_mapping = parse_site_distribution(
537+
site_distribution
538+
)
539+
540+
configurations = config_space.random_unique_configurations(
541+
site_distribution=numeric_site_distribution,
542+
n=n,
543+
sampling=sampling,
544+
seed=seed,
545+
)
546+
547+
unique_structures = []
548+
for config in configurations:
549+
species_for_sites = [numeric_site_mapping[i] for i in config.tolist()]
550+
new_structure = new_structure_from_substitution(
551+
structure,
552+
site_substitution_index,
553+
species_for_sites
554+
)
555+
new_structure.number_of_equivalent_configurations = config.count
556+
unique_structures.append(new_structure)
557+
558+
return unique_structures

bsym/symmetry_group.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ class SymmetryGroup:
1414
1515
e.g.::
1616
17-
SymmetryGroup( symmetry_operations=[ s1, s2, s3 ] )
17+
SymmetryGroup( symmetry_operations=[s1, s2, s3])
1818
1919
where `s1`, `s2`, and `s3` are :any:`SymmetryOperation` objects.
2020
2121
:any:`SymmetryGroup` objects can also be created from files using the class methods::
2222
23-
SymmetryGroup.read_from_file( filename )
23+
SymmetryGroup.read_from_file(filename)
2424
2525
and::
2626
27-
SymmetryGroup.read_from_file_with_labels( filename )
27+
SymmetryGroup.read_from_file_with_labels(filename)
2828
"""
2929

3030
class_str = 'SymmetryGroup'
@@ -74,8 +74,8 @@ def unique_index_mappings(self) -> NDArray[np.int_]:
7474
return self._unique_mappings
7575

7676
def operate_on(self,
77-
configuration: Configuration,
78-
minimal_set: bool=False) -> list[Configuration]:
77+
configuration: Configuration,
78+
minimal_set: bool=False) -> list[Configuration]:
7979
"""
8080
Returns a list of Configurations generated by applying every symmetry operation in this symmetry group.
8181

bsym/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.0"
1+
__version__ = "2.1.0"

docs/source/user_guide/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ These guides show you how to solve common crystallographic problems using the ``
1212
fixed_composition
1313
varying_composition
1414
multi_level_disorder
15+
random_sampling

0 commit comments

Comments
 (0)