Repository: materialsproject/pymatgen

Issue:
Symmetrized structures with `no_oxi_states` flag throws error in `OrderDisorderedStructureTransformation`
I am new to disordered structures and pymatgen has come to the rescue once again! Below issue might be trivial but I think there is bug in the current implementation of `OrderDisorderedStructureTransformation. apply_transformation()`

If I have the below code:
```py
symmetrized_structures = True
has_no_oxi_states = True
occ_tol = 0.55

ALGO_RANDOM = -1

trans = OrderDisorderedStructureTransformation(algo=ALGO_RANDOM,symmetrized_structures=symmetrized_structures, no_oxi_states=has_no_oxi_states, occ_tol = occ_tol)

ss = trans.apply_transformation(chevrel, return_ranked_list=100)

print(len(ss))
```

Below error is encountered:
```log
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 12
      8 ALGO_RANDOM = -1
     10 trans = OrderDisorderedStructureTransformation(algo=ALGO_RANDOM,symmetrized_structures=symmetrized_structures, no_oxi_states=has_no_oxi_states, occ_tol = occ_tol)
---> 12 ss = trans.apply_transformation(chevrel, return_ranked_list=100)
     14 print(len(ss))
     16 print(ss[0]["structure"])

File [~/miniconda3/envs/dummy/lib/python3.10/site-packages/pymatgen/transformations/standard_transformations.py:527](http://localhost:8889/~/miniconda3/envs/uo2_dft/lib/python3.10/site-packages/pymatgen/transformations/standard_transformations.py#line=526), in OrderDisorderedStructureTransformation.apply_transformation(self, structure, return_ranked_list)
    525     continue
    526 if self.symmetrized_structures:
--> 527     sym_equiv = structure.find_equivalent_sites(ex)  # type:ignore[attr-defined]
    528     sym_test = site in sym_equiv
    529 else:

AttributeError: 'Structure' object has no attribute 'find_equivalent_sites'
```
This error makes sense, because in the code `SymmetrizedStructure` is changed to `Structure` here:
https://github.com/materialsproject/pymatgen/blob/0704037c2642c6744ca3516deb21e0a811a38deb/src/pymatgen/transformations/standard_transformations.py#L508-L511

Is this expected? If not, you might want to move the `if` condition after `equivalent_sites` and `exemplars` is populated. Again, please let me know if I am missing something here.

Hints from issue comments:
@esoteric-ephemera: Hey @R1j1t : from the [doc string](https://github.com/materialsproject/pymatgen/blob/f42028a5dc9e96771963e3ef632e73570d976d72/src/pymatgen/transformations/standard_transformations.py#L461), if you specify `symmetrized_structures` as `True`, then the structures you order need to be instances of `SymmetrizedStructure`, whereas it looks like `chevrel` in your example is a normal `Structure`

You can either set `symmetrized_structures=False`, or generate the symmetrized structure as follows:
```
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer

symm_struct = SpacegroupAnalyzer(chevrel).get_symmetrized_structure()
ordered = trans.apply_transformation(symm_struct, return_ranked_list=100)
```@shyuep: @esoteric-ephemera Maybe one option is to automatically generate the symmetrized structure if symmetrized_structures is set to True? In fact, I would argue we probably want to change the parameter to say `symmetrize_structure_tol: float = -0.1` and if that number is positive, we will call SpaceGroupAnalyzer to automatically symmetrize the structure with that tolerance (or if a SymmetrizedStructure is already provided, it will just use it).@esoteric-ephemera: That's a good idea @shyuep, I can get that added quickly@R1j1t: Hi @esoteric-ephemera, indeed I was passing chevrel as `SymmetrizedStructure` but execution of `if` block changed it to `Structure`. @shyuep's suggestions would work and I can see that being used in my flow. @esoteric-ephemera let me know if I can contribute this, if you haven't already started working.  And, thanks for the quick response. Cheers!@esoteric-ephemera: OK I see so yes there's also a bug in that passing `no_oxi_states = True` converts a `SymmetrizedStructure` into a regular `Structure`. Fixed this and added similar behavior in #4403

Solution patch:
diff --git a/src/pymatgen/transformations/standard_transformations.py b/src/pymatgen/transformations/standard_transformations.py
index 5615e4d614..628fb4ea21 100644
--- a/src/pymatgen/transformations/standard_transformations.py
+++ b/src/pymatgen/transformations/standard_transformations.py
@@ -19,6 +19,7 @@ from pymatgen.core import Composition, get_el_sp
 from pymatgen.core.operations import SymmOp
 from pymatgen.core.structure import Lattice, Structure
 from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
+from pymatgen.symmetry.structure import SymmetrizedStructure
 from pymatgen.transformations.site_transformations import PartialRemoveSitesTransformation
 from pymatgen.transformations.transformation_abc import AbstractTransformation
 
@@ -454,7 +455,15 @@ class OrderDisorderedStructureTransformation(AbstractTransformation):
     ALGO_BEST_FIRST = 2
     ALGO_RANDOM = -1
 
-    def __init__(self, algo=ALGO_FAST, symmetrized_structures=False, no_oxi_states=False, occ_tol=0.25):
+    def __init__(
+        self,
+        algo: int = ALGO_FAST,
+        symmetrized_structures: bool = False,
+        no_oxi_states: bool = False,
+        occ_tol: float = 0.25,
+        symprec: float | None = None,
+        angle_tolerance: float | None = None,
+    ):
         """
         Args:
             algo (int): Algorithm to use.
@@ -466,15 +475,23 @@ class OrderDisorderedStructureTransformation(AbstractTransformation):
             occ_tol (float): Occupancy tolerance. If the total occupancy of a group is within this value
                 of an integer, it will be rounded to that integer otherwise raise a ValueError.
                 Defaults to 0.25.
+            symprec : float or None (default)
+                If a float, and symmetrized_structures is True, the linear tolerance
+                used to symmetrize structures with SpacegroupAnalyzer.
+            angle_tolerance : float or None (default)
+                If a float, and symmetrized_structures is True, the angle tolerance
+                used to symmetrize structures with SpacegroupAnalyzer.
         """
         self.algo = algo
         self._all_structures: list = []
         self.no_oxi_states = no_oxi_states
         self.symmetrized_structures = symmetrized_structures
+        self.symprec = symprec
+        self.angle_tolerance = angle_tolerance
         self.occ_tol = occ_tol
 
     def apply_transformation(
-        self, structure: Structure, return_ranked_list: bool | int = False
+        self, structure: Structure | SymmetrizedStructure, return_ranked_list: bool | int = False
     ) -> Structure | list[Structure] | list[dict[str, Any]]:
         """For this transformation, the apply_transformation method will return
         only the ordered structure with the lowest Ewald energy, to be
@@ -510,6 +527,11 @@ class OrderDisorderedStructureTransformation(AbstractTransformation):
             for idx, site in enumerate(structure):
                 structure[idx] = {f"{k.symbol}0+": v for k, v in site.species.items()}  # type: ignore[assignment]
 
+        if self.symmetrized_structures and not isinstance(structure, SymmetrizedStructure):
+            structure = SpacegroupAnalyzer(
+                structure, **{k: getattr(self, k) for k in ("symprec", "angle_tolerance") if getattr(self, k, None)}
+            ).get_symmetrized_structure()
+
         equivalent_sites: list[list[int]] = []
         exemplars: list[PeriodicSite] = []
         # generate list of equivalent sites to order


Test patch:
diff --git a/tests/transformations/test_standard_transformations.py b/tests/transformations/test_standard_transformations.py
index 5e489890a5..93dc9d66aa 100644
--- a/tests/transformations/test_standard_transformations.py
+++ b/tests/transformations/test_standard_transformations.py
@@ -14,6 +14,7 @@ from pytest import approx
 from pymatgen.alchemy.transmuters import StandardTransmuter
 from pymatgen.core import Element, PeriodicSite
 from pymatgen.core.lattice import Lattice
+from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
 from pymatgen.symmetry.structure import SymmetrizedStructure
 from pymatgen.transformations.standard_transformations import (
     AutoOxiStateDecorationTransformation,
@@ -358,10 +359,33 @@ class TestOrderDisorderedStructureTransformation:
         ]
         struct = Structure(lattice, [{"Si4+": 1}, *[{"Si4+": 0.5}] * 4], coords)
         test_site = PeriodicSite("Si4+", coords[2], lattice)
-        struct = SymmetrizedStructure(struct, "not_real", [0, 1, 1, 2, 2], ["a", "b", "b", "c", "c"])
-        output = trafo.apply_transformation(struct)
+        symm_struct = SymmetrizedStructure(struct, "not_real", [0, 1, 1, 2, 2], ["a", "b", "b", "c", "c"])
+        output = trafo.apply_transformation(symm_struct)
         assert test_site in output
 
+        ds_coords = [
+            [0.0, 0.0, 0.0],
+            [0.0, 0.5, 0.5],
+            [0.5, 0.0, 0.5],
+            [0.5, 0.5, 0.0],
+            [0.25, 0.25, 0.75],
+            [0.25, 0.75, 0.25],
+            [0.75, 0.25, 0.25],
+            [0.75, 0.75, 0.75],
+        ]
+        structure = Structure(
+            Lattice.cubic(5),
+            [*[{"Si4-": 1}] * 4, *[{"Si4+": 0.5, "Ge4+": 0.5}] * 4],
+            ds_coords,
+        )
+        symm_struct = SpacegroupAnalyzer(structure).get_symmetrized_structure()
+
+        for no_oxi_states in (True, False):
+            _kwargs = {"symmetrized_structures": True, "no_oxi_states": no_oxi_states}
+            assert OrderDisorderedStructureTransformation(**_kwargs).apply_transformation(
+                symm_struct
+            ) == OrderDisorderedStructureTransformation(**_kwargs).apply_transformation(structure)
+
     def test_too_small_cell(self):
         trafo = OrderDisorderedStructureTransformation()
         coords = [[0.5, 0.5, 0.5]]