Repository: GEMDAT-repos/GEMDAT (language: python)

Issue:
Load known materials from crystallographic information format
CIF files are the [standard file format for storing crystallographic information](https://www.iucr.org/resources/cif).

Crystal structures for the materials we work with are available:

E.g. for argyrodite:
- https://chemistry-europe.onlinelibrary.wiley.com/doi/abs/10.1002/chem.200902470
- http://www.crystallography.net/cod/result.php?CODSESSION=blufg6et4cvidhmsmmj58v8h17&page=1&count=20&order_by=sg&order=asc

In the matlab code these are coded by hand in `known_materials.m`. I would like to have these available in standard CIF format, so that 

1. Any crystallographic software can be used to visualize/inspect/modify the crystal structures.
2. Any known crystal structure can be used as a basis for the analysis without having to modify the code. This makes our tool more accessible.
3. Errors are reduced by having a fixed crystallographic format


Discussion / hints:
`pymatgen` also supports loading from CIF. Once we have that we can look into supporting additional formats if necessary.

The analysis code works in P1 (no symmetry). So the goal here is to load these CIF files using `pymatgen` and extract a symmetrized set of coordinates ([SymmetrizedStructure](https://pymatgen.org/pymatgen.symmetry.structure.html)?), convert them to P1 (i.e. apply all symmetry operations to get a [Structure](https://pymatgen.org/pymatgen.core.structure.html#pymatgen.core.structure.Structure) object), and perform any super cell operations (expand along x y or z directions).




Fix patch:
diff --git a/src/data.py b/src/data.py
index 5868ed66..5d16c0ef 100644
--- a/src/data.py
+++ b/src/data.py
@@ -9,6 +9,7 @@
 
 
 class Data(BaseModel):
+    """Dataclass to store simulation data."""
 
     class Config:
         arbitrary_types_allowed = True
@@ -18,46 +19,85 @@ class Config:
     species: object
     lattice: object
     time_step: float
+    temperature: float
+    parameters: dict
 
     @classmethod
     def from_cache(cls, cache: Path):
+        """Load data from cache using pickle.
+
+        Parameters
+        ----------
+        cache : Path
+            Name of cache file
+
+        Returns
+        -------
+        data : Data
+            Dataclass with simulation data
+        """
         with open(cache, 'rb') as f:
             data = pickle.load(f)
         return cls(**data)
 
-    def to_cache(self, cache):
+    def to_cache(self, cache: Path):
+        """Dump data to cache using pickle.
+
+        Parameters
+        ----------
+        cache : Path
+            Name of cache file
+        """
         with open(cache, 'wb') as f:
             pickle.dump(self.dict(), f)
 
     @classmethod
     def from_vasprun(cls, xml_file: Path, cache: Optional[Path] = None):
+        """Load data from vasprun.xml.
+
+        Parameters
+        ----------
+        xml_file : Path
+            Path to vasprun.xml
+        cache : Optional[Path], optional
+            Path to cache data for vasprun.xml
+
+        Returns
+        -------
+        data : Data
+            Dataclass with simulation data
+        """
         if cache and cache.exists():
             return Data.from_cache(cache)
-        else:
-            run = vasp.Vasprun(
-                xml_file,
-                parse_dos=False,
-                parse_eigen=False,
-                parse_projected_eigen=False,
-                parse_potcar_file=False,
-            )
-
-            structure = run.structures[0]
-            trajectory = run.get_trajectory()
-            trajectory.to_positions()
-
-            data = {
-                'structure': structure,
-                'trajectory_coords': trajectory.coords,
-                'species': structure.species,
-                'lattice': structure.lattice,
-                'time_step': run.parameters['POTIM'] * 1e-15,
-            }
-
-            # first create object to check pydantic before caching
-            ret = cls(**data)
-
-            if cache:
-                ret.to_cache(cache)
-
-            return ret
+
+        run = vasp.Vasprun(
+            xml_file,
+            parse_dos=False,
+            parse_eigen=False,
+            parse_projected_eigen=False,
+            parse_potcar_file=False,
+        )
+
+        structure = run.structures[0]
+        trajectory = run.get_trajectory()
+        trajectory.to_positions()
+
+        data = {
+            'structure': structure,
+            'trajectory_coords': trajectory.coords,
+            'species': structure.species,
+            'lattice': structure.lattice,
+            # size of the time step (*1e-15 = in femtoseconds)
+            'time_step': run.parameters['POTIM'] * 1e-15,
+            # temperature of the MD simulation
+            'temperature': run.parameters['TEBEG'],
+            'parameters': run.parameters,
+        }
+
+        # first create object to check pydantic before caching
+        ret = cls(**data)
+
+        if cache:
+            ret.to_cache(cache)
+
+        return ret
diff --git a/src/data/argyrodite.cif b/src/data/argyrodite.cif
new file mode 100644
index 00000000..05ecdce1
--- /dev/null
+++ b/src/data/argyrodite.cif
@@ -0,0 +1,119 @@
+data_argyrodite
+_symmetry_space_group_name_H-M   'F -4 3 m'
+_cell_length_a   9.924
+_cell_length_b   9.924
+_cell_length_c   9.924
+_cell_angle_alpha   90
+_cell_angle_beta   90
+_cell_angle_gamma   90
+_symmetry_Int_Tables_number   216
+_cell_volume   977.329
+
+loop_
+_symmetry_equiv_pos_as_xyz
+   'x, y, z'
+   'x, 1/2+y, 1/2+z'
+   '1/2+x, y, 1/2+z'
+   '1/2+x, 1/2+y, z'
+   '-z, x, -y'
+   '-z, 1/2+x, 1/2-y'
+   '1/2-z, x, 1/2-y'
+   '1/2-z, 1/2+x, -y'
+   '-y, z, -x'
+   '-y, 1/2+z, 1/2-x'
+   '1/2-y, z, 1/2-x'
+   '1/2-y, 1/2+z, -x'
+   '-x, y, -z'
+   '-x, 1/2+y, 1/2-z'
+   '1/2-x, y, 1/2-z'
+   '1/2-x, 1/2+y, -z'
+   'x, -z, -y'
+   'x, 1/2-z, 1/2-y'
+   '1/2+x, -z, 1/2-y'
+   '1/2+x, 1/2-z, -y'
+   'z, -y, -x'
+   'z, 1/2-y, 1/2-x'
+   '1/2+z, -y, 1/2-x'
+   '1/2+z, 1/2-y, -x'
+   'y, -x, -z'
+   'y, 1/2-x, 1/2-z'
+   '1/2+y, -x, 1/2-z'
+   '1/2+y, 1/2-x, -z'
+   'x, z, y'
+   'x, 1/2+z, 1/2+y'
+   '1/2+x, z, 1/2+y'
+   '1/2+x, 1/2+z, y'
+   'z, y, x'
+   'z, 1/2+y, 1/2+x'
+   '1/2+z, y, 1/2+x'
+   '1/2+z, 1/2+y, x'
+   'y, x, z'
+   'y, 1/2+x, 1/2+z'
+   '1/2+y, x, 1/2+z'
+   '1/2+y, 1/2+x, z'
+   '-z, -x, y'
+   '-z, 1/2-x, 1/2+y'
+   '1/2-z, -x, 1/2+y'
+   '1/2-z, 1/2-x, y'
+   '-y, -z, x'
+   '-y, 1/2-z, 1/2+x'
+   '1/2-y, -z, 1/2+x'
+   '1/2-y, 1/2-z, x'
+   '-x, -y, z'
+   '-x, 1/2-y, 1/2+z'
+   '1/2-x, -y, 1/2+z'
+   '1/2-x, 1/2-y, z'
+   'z, -x, -y'
+   'z, 1/2-x, 1/2-y'
+   '1/2+z, -x, 1/2-y'
+   '1/2+z, 1/2-x, -y'
+   'y, -z, -x'
+   'y, 1/2-z, 1/2-x'
+   '1/2+y, -z, 1/2-x'
+   '1/2+y, 1/2-z, -x'
+   'x, -y, -z'
+   'x, 1/2-y, 1/2-z'
+   '1/2+x, -y, 1/2-z'
+   '1/2+x, 1/2-y, -z'
+   '-x, z, -y'
+   '-x, 1/2+z, 1/2-y'
+   '1/2-x, z, 1/2-y'
+   '1/2-x, 1/2+z, -y'
+   '-z, y, -x'
+   '-z, 1/2+y, 1/2-x'
+   '1/2-z, y, 1/2-x'
+   '1/2-z, 1/2+y, -x'
+   '-y, x, -z'
+   '-y, 1/2+x, 1/2-z'
+   '1/2-y, x, 1/2-z'
+   '1/2-y, 1/2+x, -z'
+   '-x, -z, y'
+   '-x, 1/2-z, 1/2+y'
+   '1/2-x, -z, 1/2+y'
+   '1/2-x, 1/2-z, y'
+   '-z, -y, x'
+   '-z, 1/2-y, 1/2+x'
+   '1/2-z, -y, 1/2+x'
+   '1/2-z, 1/2-y, x'
+   '-y, -x, z'
+   '-y, 1/2-x, 1/2+z'
+   '1/2-y, -x, 1/2+z'
+   '1/2-y, 1/2-x, z'
+   'z, x, y'
+   'z, 1/2+x, 1/2+y'
+   '1/2+z, x, 1/2+y'
+   '1/2+z, 1/2+x, y'
+   'y, z, x'
+   'y, 1/2+z, 1/2+x'
+   '1/2+y, z, 1/2+x'
+   '1/2+y, 1/2+z, x'
+
+loop_
+ _atom_site_type_symbol
+ _atom_site_label
+ _atom_site_symmetry_multiplicity
+ _atom_site_fract_x
+ _atom_site_fract_y
+ _atom_site_fract_z
+ _atom_site_occupancy
+  Li  Li48h  48  0.183  0.183  0.024  1
diff --git a/src/io.py b/src/io.py
new file mode 100644
index 00000000..38148bb2
--- /dev/null
+++ b/src/io.py
@@ -0,0 +1,24 @@
+from importlib.resources import files
+from pathlib import Path
+
+from pymatgen.core import Structure
+from pymatgen.io import cif
+
+DATA = Path(files('gemdat') / 'data')  # type: ignore
+
+
+def load_cif(filename: Path | str) -> Structure:
+    """Load cif file and return first item as pymatgen structure."""
+    cifdata = cif.CifParser(filename)
+    structure = cifdata.get_structures(primitive=False)[0]
+    return structure
+
+
+def load_known_material(name: str) -> Structure:
+    """Load known material."""
+    filename = (DATA / name).with_suffix('.cif')
+
+    if not filename.exists():
+        raise ValueError(f'Unknown material: {name}')
+
+    return load_cif(filename)


Test patch:
diff --git a/tests/io_test.py b/tests/io_test.py
index e0227fc7..669b156c 100644
--- a/tests/io_test.py
+++ b/tests/io_test.py
@@ -1,2 +1,8 @@
-def test_read_vasp():
-    assert (True)
+from gemdat.io import load_known_material
+from pymatgen.core import Structure
+
+
+def test_load_known_material():
+    structure = load_known_material('argyrodite')
+
+    assert isinstance(structure, Structure)