<jupyter_start><jupyter_text>**crystal_torture:** A Python/Fortran crystal tortuosity module   `crystal_torture` is a Python, Fortran and OpenMP crystal structure analysis module. The module contains an interface to [pymatgen](http://www.pymatgen.org) and a set of classes that enables:* a crystal structure to be converted into a simple graph for network analysis* connected clusters of crystal sites (nodes) to be retrieved and output* periodicity of connected clusters of crystal sites to be determined* relative path tortuosity to traverse a crystal within a connected cluster to be calculated for each site in the clusterThe module is written in Python and Python wrapped Fortran (with OpenMP).                                                                         **Crystals as Graphs, Connectivity and Relative Tortuosity**A crystal structure can be described as a graph, consisting of clusters, which in turn are comprised of connected nodes. Taking a crystal structure we can treat each site within it it as a node. Nodes can be considered connected or disconnected based on some user determined inter-nodal distance and occupying species criteria.Selecting only  nodes:we obtain a graph comprised of connected clusters that extends across the periodic crystal boundary:Selecting only the periodic clusters:We can select one node and calculate the minimum number of internodal steps to traverse the full crystal and return to a periodic nodal site:This is what we define as the site tortuosity, &tau;. The relative site tortuosity therefore is the ratio of this value to the minimum pathway if all nodes were available to form the pathway.   **Setting up a graph and torturing it**Using pymatgen we can set up a graph structure directly from a pymatgen compatible filetype  (in this instance a POSCAR file). We have stipulated an internodal distance of 4.0 &8491; and selected only sites occupied by Li atoms as nodes:<jupyter_code>import sys
import time
sys.path.append('../crystal_torture')

from crystal_torture.pymatgen_interface import graph_from_file

graph = graph_from_file("POSCAR.vasp",4.0,["Li"])
print(len(graph.clusters))<jupyter_output>1<jupyter_text>We can list the indices of nodes within each cluster:<jupyter_code>for cluster in graph.clusters:
    print("Cluster: (len) (indices in cluster))")
    print( cluster.return_uc_indices())<jupyter_output>Cluster: (len) (indices in cluster))
{'6', '3', '4', '1', '5', '2', '0', '7'}<jupyter_text>We can also list the periodicity of the clusters within the graph (i.e. whether the cluster traverses the entire unit cell in 1,2 or 3 dimensions):<jupyter_code>print([cluster.periodic for cluster in graph.clusters])<jupyter_output>[3]<jupyter_text>We can print the fraction of nodes in the graph that are in a percolating (i.e. periodic) cluster:<jupyter_code>print('{} {}\n'.format("Frac Perc:",graph.return_frac_percolating()))<jupyter_output>Frac Perc: 1.0<jupyter_text>We can torture the periodic clusters in the graph, meaning for each node in a cluster we can calculate the tortuosity of the minimum pathway from the node to any of its periodic images (i.e. the number of internodal steps to reach a periodic image). The ratio of this tortuosity to the minimum pathway is the relative site tortuosity. The algorithm performed is a  breadth first search. Depending on system size, this algorithm can be computationally expensive and there are two ways of running it. There is a pure python version, which can be used for small systems:<jupyter_code>start_time = time.time()
graph.torture_py()
end_time = time.time()

print("Time:", end_time - start_time)<jupyter_output>Time: 0.0041239261627197266<jupyter_text>Or a version that performs the search using Fortran90, coupled with OpenMP. The algorithm in this case is parallelised across nodes in the graph, calculating the tortuosity for each node in parallel, and therefore will perform the search much more quickly for large systems.<jupyter_code>graph = graph_from_file("POSCAR.vasp",4.0,["Li"])

time1=time.time()

start_time = time.time()
graph.torture()
end_time = time.time()

print("Time:", end_time - start_time)<jupyter_output>Time: 0.032588958740234375<jupyter_text>Following the tortuosity analysis we can see the average tortuosity for the nodes in a cluster:<jupyter_code>for no,cluster in enumerate(graph.minimal_clusters):
        print('{}{}{}\n'.format("****** Cluster: ",no, " ********"))
        print('{} {}\n'.format("Size:",cluster.size))
        print('{} {}\n'.format("Tortuosity:",cluster.tortuosity))
        print('{} {}\n'.format("Periodicity:",cluster.periodic))<jupyter_output>****** Cluster: 0 ********

Size: 8

Tortuosity: 4.0

Periodicity: 3<jupyter_text>Or indeed exmaine the tortuosity of every node in a cluster:<jupyter_code>print(graph.tortuosity)<jupyter_output>{'0': 4, '2': 4, '4': 4, '3': 4, '5': 4, '7': 4, '1': 4, '6': 4}<jupyter_text>It is also possible to output the clusters found to a filetype which can be viewed in a crystal structure viewer such as VMD or VESTA<jupyter_code>graph.output_clusters(fmt='poscar',periodic=False)<jupyter_output><empty_output><jupyter_text>**Worked example: Doping and torturing Spinel MgAl2O4**Taking the spinel structure, there are 56 ions in the conventional unit cell with stoichiometry (A2+)[B3+]O4+. In normal magnesium spinel (Mg)[Al2]O4, the Mg2+ reside on the (A) sites, Al3+ reside on the [B]-sites.Taking this spinel structured magnesium spinel we can co-dope with {Li-Al}, upon which pairs of Mg2+ cations are substituted in equal proportions by Li+ and Al3+, giving    a composition of (AlxMg1-2xLix)[Al2]O4                          MgAl2O4              Doping removes two Mgs+ and inserts one Li+ and one Al3+              Doped structure (x=0.25)(LixMg1−2xAl2+xO4)                 One potential use for this material could be as a solid Li+ electrolyte in Li-ion batteries. In this case we are interested in the connectivity of the Li-ions within the crystal, and the tortuosity of possible pathways through the crystal for these Li-ions. We wish to examine the networks formed by Li-ions occupying the [A] sites. Taking the spinel unit cell we can make a pymatgen structure object:<jupyter_code>from pymatgen import Structure
spinel=Structure.from_file("POSCAR_SPINEL.vasp")<jupyter_output><empty_output><jupyter_text>We can label the sites of the structure:<jupyter_code>spinel.add_site_property("label",["A"]*8+["B"]*16+["O"]*32)<jupyter_output><empty_output><jupyter_text>and make a (3x3x3) supercell:<jupyter_code>spinel.make_supercell([3,3,3])<jupyter_output><empty_output><jupyter_text>and we can randomly dope the structure using crystal torture (we have included a very simple module for this task):<jupyter_code>import crystal_torture.pymatgen_doping as pd

spinel = pd.dope_structure(spinel,conc=0.9,species_to_rem="Mg",species_to_insert=["Li","Al"],label_to_remove="A")

print(spinel)<jupyter_output>Full Formula (Li97 Mg22 Al529 O864)
Reduced Formula: Li97Mg22Al529O864
abc   :  24.240000  24.240000  24.240000
angles:  90.000000  90.000000  90.000000
Sites (1512)
   #  SP           a         b         c
----  ----  --------  --------  --------
   0  Li    0.125     0.125     0.125
   1  Li    0.125     0.125     0.458333
   2  Li    0.125     0.458333  0.458333
   3  Li    0.125     0.458333  0.791667
   4  Li    0.125     0.791667  0.458333
   5  Li    0.125     0.791667  0.791667
   6  Li    0.458333  0.125     0.458333
   7  Li    0.458333  0.458333  0.458333
   8  Li    0.458333  0.791667  0.125
   9  Li    0.458333  0.791667  0.458333
  10  Li    0.458333  0.791667  0.791667
  11  Li    0.791667  0.125     0.125
  12  Li    0.791667  0.125     0.458333
  13  Li    0.791667  0.125     0.791667
  14  Li    0.791667  0.458333  0.125
  15  Li    0.791667  0.458333  0.458333
  16  Li    0.791667  0.791667  0.791667
  17  Li    0.291667  0.625     0.125
  18  Li    0.291667  0.625  [...]<jupyter_text>we can output the full structure for inspection:<jupyter_code>print(spinel)
spinel.to(filename="POSCAR_full.vasp")<jupyter_output>Full Formula (Li97 Mg22 Al529 O864)
Reduced Formula: Li97Mg22Al529O864
abc   :  24.240000  24.240000  24.240000
angles:  90.000000  90.000000  90.000000
Sites (1512)
   #  SP           a         b         c
----  ----  --------  --------  --------
   0  Li    0.125     0.125     0.125
   1  Li    0.125     0.125     0.458333
   2  Li    0.125     0.458333  0.458333
   3  Li    0.125     0.458333  0.791667
   4  Li    0.125     0.791667  0.458333
   5  Li    0.125     0.791667  0.791667
   6  Li    0.458333  0.125     0.458333
   7  Li    0.458333  0.458333  0.458333
   8  Li    0.458333  0.791667  0.125
   9  Li    0.458333  0.791667  0.458333
  10  Li    0.458333  0.791667  0.791667
  11  Li    0.791667  0.125     0.125
  12  Li    0.791667  0.125     0.458333
  13  Li    0.791667  0.125     0.791667
  14  Li    0.791667  0.458333  0.125
  15  Li    0.791667  0.458333  0.458333
  16  Li    0.791667  0.791667  0.791667
  17  Li    0.291667  0.625     0.125
  18  Li    0.291667  0.625  [...]<jupyter_text>now we can get a graph from the structure, including only [A] sites occuptied by Li ions:<jupyter_code>from crystal_torture.pymatgen_interface import graph_from_structure
graph = graph_from_structure(structure=spinel,rcut=4.0,elements={"Li"})<jupyter_output><empty_output><jupyter_text>We can torture the graph:<jupyter_code>graph.torture()<jupyter_output><empty_output><jupyter_text>output the clusters:<jupyter_code>graph.output_clusters('poscar',periodic=False)<jupyter_output><empty_output><jupyter_text>Now we can examine the size, periodicity and tortuosity of the clusters in graph:<jupyter_code>for cluster in graph.minimal_clusters:
    print("**************")
    print("Cluster size",cluster.size)
    print("Cluster Tortuosity",cluster.tortuosity)
    print("Cluster Periodicity",cluster.periodic)

print(graph.return_frac_percolating())<jupyter_output>**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 2
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 8
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 1
Cluster Tortuosity None
Cluster Periodicity 0
**************
Cluster size 77
Cluster Tortuosity 16.103896103896105
Cluster Periodicity[...]
{'fraction_non_alphanumeric': 0.10516722865214617, 'fraction_numerical': 0.09505149002096054, 'mean_word_length': 3.8600531443755535, 'pattern_counts': {'":': 0, '<': 62, '<?xml version=': 0, '>': 62, 'https://': 0, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '190477', 'license': 'MIT', 'n_tokens_mistral': 4156, 'n_tokens_neox': 3528, 'n_words': 1214, 'path': 'examples/crystal_torture_examples.ipynb', 'repo_name': 'alexsquires/crystal_torture', 'size': '10973'}