23
23
from pymatgen .transformations .transformation_abc import AbstractTransformation
24
24
25
25
if TYPE_CHECKING :
26
+ from numpy .random import Generator
26
27
from typing_extensions import Self
27
28
28
29
from pymatgen .core .sites import PeriodicSite
@@ -451,6 +452,7 @@ class OrderDisorderedStructureTransformation(AbstractTransformation):
451
452
ALGO_FAST = 0
452
453
ALGO_COMPLETE = 1
453
454
ALGO_BEST_FIRST = 2
455
+ ALGO_RANDOM = - 1
454
456
455
457
def __init__ (self , algo = ALGO_FAST , symmetrized_structures = False , no_oxi_states = False ):
456
458
"""
@@ -467,7 +469,9 @@ def __init__(self, algo=ALGO_FAST, symmetrized_structures=False, no_oxi_states=F
467
469
self .no_oxi_states = no_oxi_states
468
470
self .symmetrized_structures = symmetrized_structures
469
471
470
- def apply_transformation (self , structure : Structure , return_ranked_list : bool | int = False ) -> Structure :
472
+ def apply_transformation (
473
+ self , structure : Structure , return_ranked_list : bool | int = False , occ_tol = 0.25
474
+ ) -> Structure :
471
475
"""For this transformation, the apply_transformation method will return
472
476
only the ordered structure with the lowest Ewald energy, to be
473
477
consistent with the method signature of the other transformations.
@@ -478,6 +482,9 @@ def apply_transformation(self, structure: Structure, return_ranked_list: bool |
478
482
structure: Oxidation state decorated disordered structure to order
479
483
return_ranked_list (bool | int, optional): If return_ranked_list is int, that number of structures
480
484
is returned. If False, only the single lowest energy structure is returned. Defaults to False.
485
+ occ_tol (float): Occupancy tolerance. If the total occupancy of a group is within this value
486
+ of an integer, it will be rounded to that integer otherwise raise a ValueError.
487
+ Defaults to 0.25.
481
488
482
489
Returns:
483
490
Depending on returned_ranked list, either a transformed structure
@@ -529,14 +536,25 @@ def apply_transformation(self, structure: Structure, return_ranked_list: bool |
529
536
# generate the list of manipulations and input structure
530
537
struct = Structure .from_sites (structure )
531
538
539
+ # We will first create an initial ordered structure by filling all sites
540
+ # with the species that has the highest oxidation state (initial_sp)
541
+ # replacing all other species on a given site.
542
+ # then, we process a list of manipulations to get the final structure.
543
+ # The manipulations are of the format:
544
+ # [oxi_ratio, 1, [0,1,2,3], Li+]
545
+ # which means -- Place 1 Li+ in any of these 4 sites
546
+ # the oxi_ratio is the ratio of the oxidation state of the species to
547
+ # the initial species. This is used to determine the energy of the
548
+ # manipulation in the EwaldMinimizer, but is not used in the purely random
549
+ # algorithm.
532
550
manipulations = []
533
551
for group in equivalent_sites :
534
552
total_occupancy = dict (
535
553
sum ((structure [idx ].species for idx in group ), Composition ()).items () # type: ignore[attr-defined]
536
554
)
537
555
# round total occupancy to possible values
538
556
for key , val in total_occupancy .items ():
539
- if abs (val - round (val )) > 0.25 :
557
+ if abs (val - round (val )) > occ_tol :
540
558
raise ValueError ("Occupancy fractions not consistent with size of unit cell" )
541
559
total_occupancy [key ] = round (val )
542
560
# start with an ordered structure
@@ -555,6 +573,16 @@ def apply_transformation(self, structure: Structure, return_ranked_list: bool |
555
573
if empty > 0.5 :
556
574
manipulations .append ([0 , empty , list (group ), None ])
557
575
576
+ if self .algo == self .ALGO_RANDOM :
577
+ rand_structures = get_randomly_manipulated_structures (
578
+ struct = struct , manipulations = manipulations , n_return = n_to_return
579
+ )
580
+ if return_ranked_list :
581
+ return [
582
+ {"energy" : 0.0 , "energy_above_minimum" : 0.0 , "structure" : s } for s in rand_structures [:n_to_return ]
583
+ ]
584
+ return rand_structures [0 ]
585
+
558
586
matrix = EwaldSummation (struct ).total_energy_matrix
559
587
ewald_m = EwaldMinimizer (matrix , manipulations , n_to_return , self .algo )
560
588
@@ -891,3 +919,82 @@ def apply_transformation(self, structure):
891
919
892
920
def __repr__ (self ):
893
921
return "ScaleToRelaxedTransformation"
922
+
923
+
924
+ def _sample_random_manipulation (manipulation , rng , manipulated ) -> list [tuple [int , SpeciesLike ]]:
925
+ """Sample a single random manipulation.
926
+
927
+ Each manipulation is given in the form of a tuple
928
+ `(oxi_ratio, nsites, indices, sp)` where:
929
+ Which means choose nsites from the list of indices and replace them
930
+ With the species `sp`.
931
+ """
932
+ _ , nsites , indices , sp = manipulation
933
+ maniped_indices = [i for i , _ in manipulated ]
934
+ allowed_sites = [i for i in indices if i not in maniped_indices ]
935
+ if len (allowed_sites ) < nsites :
936
+ raise RuntimeError (
937
+ "No valid manipulations possible. "
938
+ f" You have already applied a manipulation to each site in this group { indices } "
939
+ )
940
+ sampled_sites = rng .choice (allowed_sites , nsites , replace = False ).tolist ()
941
+ sampled_sites .sort ()
942
+ return [(i , sp ) for i in sampled_sites ]
943
+
944
+
945
+ def _get_manipulation (manipulations : list , rng : Generator , max_attempts , seen : set [tuple ]) -> tuple :
946
+ """Apply each manipulation."""
947
+ for _ in range (max_attempts ):
948
+ manipulated : list [tuple ] = []
949
+ for manip_ in manipulations :
950
+ new_manips = _sample_random_manipulation (manip_ , rng , manipulated )
951
+ manipulated += new_manips
952
+ tm_ = tuple (manipulated )
953
+ if tm_ not in seen :
954
+ return tm_
955
+ raise RuntimeError (
956
+ "Could not apply manipulations to structure"
957
+ "this is likely because you have already applied all the possible manipulations"
958
+ )
959
+
960
+
961
+ def _apply_manip (struct , manipulations ) -> Structure :
962
+ """Apply manipulations to a structure."""
963
+ struct_copy = struct .copy ()
964
+ rm_indices = []
965
+ for manip in manipulations :
966
+ idx , sp = manip
967
+ if sp is None :
968
+ rm_indices .append (idx )
969
+ else :
970
+ struct_copy .replace (idx , sp )
971
+ struct_copy .remove_sites (rm_indices )
972
+ return struct_copy
973
+
974
+
975
+ def get_randomly_manipulated_structures (
976
+ struct : Structure , manipulations : list , seed = None , n_return : int = 1
977
+ ) -> list [Structure ]:
978
+ """Get a structure with random manipulations applied.
979
+
980
+ Args:
981
+ struct: Input structure
982
+ manipulations: List of manipulations to apply
983
+ seed: Seed for random number generator
984
+ n_return: Number of structures to return
985
+
986
+ Returns:
987
+ List of structures with manipulations applied.
988
+ """
989
+ rng = np .random .default_rng (seed )
990
+ seen : set [tuple ] = set ()
991
+ sampled_manips = []
992
+
993
+ for _ in range (n_return ):
994
+ manip_ = _get_manipulation (manipulations , rng , 1000 , seen )
995
+ seen .add (manip_ )
996
+ sampled_manips .append (manip_ )
997
+ output_structs = []
998
+ for manip_ in sampled_manips :
999
+ output_structs .append (_apply_manip (struct , manip_ ))
1000
+ return output_structs
0 commit comments