<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 bsym – a basic symmetry module
bsym is a basic Python symmetry module. It consists of some core classes that describe configuration vector spaces, their symmetry operations, and specific configurations of objects withing these spaces. The module also contains an interface for working with pymatgen Structure objects, to allow simple generation of disordered symmetry-inequivalent structures from a symmetric parent crystal structure.
API documentation is here.
Configuration Spaces, Symmetry Operations, and Groups
The central object described by bsym is the configuration space. This defines a vector space that can be occupied by other objects. For example; the three points $a, b, c$ defined by an equilateral triangle,
<img src='figures/triangular_configuration_space.pdf'>
which can be described by a length 3 vector
Step1:  Each SymmetryOperation has an optional label attribute. This can be set at records the matrix representation of the symmetry operation and an optional label. We can provide the label when creating a SymmetryOperation
Step2:  or set it afterwards
Step3:  Or for $C_3$
Step4:  Vector representations of symmetry operations
The matrix representation of a symmetry operation is a permutation matrix. Each row maps one position in the corresponding configuration space to one other position. An alternative, condensed, representation for each symmetry operation matrix uses vector notation, where each element gives the row containing 1 in the equivalent matrix column. e.g. for $C_3$ the vector mapping is given by $\left[2,3,1\right]$, corresponding to the mapping $1\to2$, $2\to3$, $3\to1$.
Step5:  The vector representation of a SymmetryOperation can be accessed using the as_vector() method.
Step6:  Inverting symmetry operations
For every symmetry operation, $A$, there is an inverse operation, $A^{-1}$, such that 
\begin{equation}
A \cdot A^{-1}=E.
\end{equation}
For example, the inverse of $C_3$ (clockwise rotation by 120°) is $C_3^\prime$ (anticlockwise rotation by 120°)
Step7:  The product of $C_3$ and $C_3^\prime$ is the identity, $E$.
Step8:  <img src="figures/triangular_c3_inversion.pdf" />
c_3_inv can also be generated using the .invert() method
Step9:  The resulting SymmetryOperation does not have a label defined. This can be set directly, or by chaining the .set_label() method, e.g.
Step10:  The SymmetryGroup class
A SymmetryGroup is a collections of SymmetryOperation objects. A SymmetryGroup is not required to contain all the symmetry operations of a particular configuration space, and therefore is not necessarily a complete mathematical <a href="https
Step11:  <img src="figures/triangular_c3v_symmetry_operations.pdf" />
Step12:  The ConfigurationSpace class
A ConfigurationSpace consists of a set of objects that represent the configuration space vectors, and the SymmetryGroup containing the relevant symmetry operations.
Step13:  The Configuration class
A Configuration instance describes a particular configuration, i.e. how a set of objects are arranged within a configuration space. Internally, a Configuration is represented as a vector (as a numpy array).
Each element in a configuration is represented by a single digit non-negative integer.
Step14:  The effect of a particular symmetry operation acting on a configuration can now be calculated using the SymmetryOperation.operate_on() method, or by direct multiplication, e.g.
Step15:  <img src="figures/triangular_rotation_operation.pdf" />
Finding symmetry-inequivalent permutations.
A common question that comes up when considering the symmetry properties of arrangements of objects is
Step16:  This ConfigurationSpace has been created without a symmetry_group argument. The default behaviour in this case is to create a SymmetryGroup containing only the identity, $E$.
Step17:  We can now calculate all symmetry inequivalent arrangements where two sites are occupied and two are unoccupied, using the unique_configurations() method. This takes as a argument a dict with the numbers of labels to be arranged in the configuration space. Here, we use the labels 1 and 0 to represent occupied and unoccupied sites, respectively, and the distribution of sites is given by { 1
Step18:  Because we have not yet taken into account the symmetry of the configuration space, we get
\begin{equation}
\frac{4\times3}{2}
\end{equation}
unique configurations (where the factor of 2 comes from the occupied sites being indistinguishable).
The configurations generated by unique_configurations have a count attribute that records the number of symmetry equivalent configurations of each case
Step19:  We can also calculate the result when all symmetry operations of this configuration space are included.
Step20:  Taking symmetry in to account, we now only have two unique configurations
Step21:  <img src="figures/square_unique_configurations_2.pdf">
Working with crystal structures using pymatgen
One example where the it can be useful to identify symmetry-inequivalent arrangements of objects in a vector space, is when considering the possible arrangements of disordered atoms on a crystal lattice. 
To solve this problem for an arbitrary crystal structure, bsym contains an interface to pymatgen that will identify symmetry-inequivalent atom substitutions in a given pymatgen Structure.
As an example, consider a $4\times4$ square-lattice supercell populated by lithium atoms.
Step22:  We can use the bsym.interface.pymatgen.unique_structure_substitutions() function to identify symmetry-inequivalent structures generated by substituting at different sites.
Step23:  As a trivial example, when substituting one Li atom for Na, we get a single unique structure
Step24:  <img src="figures/pymatgen_example_one_site.pdf">
Step25:  This Li$\to$Na substitution breaks the symmetry of the $4\times4$ supercell. 
If we now replace a second lithium with a magnesium atom, we generate five symmetry inequivalent structures
Step26:  number_of_equivalent_configurations only lists the number of equivalent configurations found when performing the second substitution, when the list of structures unique_structures_with_Mg was created. The full configuration degeneracy relative to the initial empty 4×4 lattice can be queried using full_configuration_degeneracy.
Step27:  <img src="figures/pymatgen_example_two_sites.pdf">
Step28:  This double substitution can also be done in a single step
Step29:  Because both substitutions were performed in a single step, number_of_equivalent_configurations and full_configuration_degeneracy now contain the same data
Step30:  Constructing SpaceGroup and ConfigurationSpace objects using pymatgen
The bsym.interface.pymatgen module contains functions for generating SpaceGroup and ConfigurationSpace objects directly from pymatgen Structure objects.
Step31:  Documentation
Step32:  Progress bars
bsym.ConfigurationSpace.unique_configurations() and bsym.interface.pymatgen.unique_structure_substitutions() both accept optional show_progress arguments, which can be used to display progress bars (using tqdm(https
<ASSISTANT_TASK:>
Python Code:
from bsym import SymmetryOperation
SymmetryOperation([[ 1, 0, 0 ], 
                   [ 0, 1, 0 ], 
                   [ 0, 0, 1 ]])
Explanation: bsym – a basic symmetry module
bsym is a basic Python symmetry module. It consists of some core classes that describe configuration vector spaces, their symmetry operations, and specific configurations of objects withing these spaces. The module also contains an interface for working with pymatgen Structure objects, to allow simple generation of disordered symmetry-inequivalent structures from a symmetric parent crystal structure.
API documentation is here.
Configuration Spaces, Symmetry Operations, and Groups
The central object described by bsym is the configuration space. This defines a vector space that can be occupied by other objects. For example; the three points $a, b, c$ defined by an equilateral triangle,
<img src='figures/triangular_configuration_space.pdf'>
which can be described by a length 3 vector:
\begin{pmatrix}a\b\c\end{pmatrix}
If these points can be coloured black or white, then we can define a configuration for each different colouring (0 for white, 1 for black), e.g. 
<img src='figures/triangular_configuration_example_1.pdf'>
with the corresponding vector
\begin{pmatrix}1\1\0\end{pmatrix}
A specific configuration therefore defines how objects are distributed within a particular configuration space.
The symmetry relationships between the different vectors in a configuration space are described by symmetry operations. A symmetry operation describes a transformation of a configuration space that leaves it indistinguishable. Each symmetry operation can be describes as a matrix that maps the vectors in a configuration space onto each other, e.g. in the case of the equiateral triangle the simplest symmetry operation is the identity, $E$, which leaves every corner unchanged, and can be represented by the matrix 
\begin{equation}
E=\begin{pmatrix}1 & 0 & 0\0 & 1 & 0 \ 0 & 0 & 1\end{pmatrix}
\end{equation}
For this triangular example, there are other symmetry operations, including reflections, $\sigma$ and rotations, $C_n$:
<img src='figures/triangular_example_symmetry_operations.pdf'>
In this example reflection operation, $b$ is mapped to $c$; $b\to c$, and $c$ is mapped to $b$; $b\to c$. 
The matrix representation of this symmetry operation is
\begin{equation}
\sigma_\mathrm{a}=\begin{pmatrix}1 & 0 & 0\0 & 0 & 1 \ 0 & 1 & 0\end{pmatrix}
\end{equation}
For the example rotation operation, $a\to b$, $b\to c$, and $c\to a$, with matrix representation
\begin{equation}
C_3=\begin{pmatrix}0 & 0 & 1\ 1 & 0 & 0 \ 0 & 1 & 0\end{pmatrix}
\end{equation}
Using this matrix and vector notation, the effect of a symmetry operation on a specific configuration can be calculated as the matrix product of the symmetry operation matrix and the configuration vector:
<img src='figures/triangular_rotation_operation.pdf'>
In matrix notation this is represented as
\begin{equation}
\begin{pmatrix}0\1\1\end{pmatrix} = \begin{pmatrix}0 & 0 & 1\ 1 & 0 & 0 \ 0 & 1 & 
0\end{pmatrix}\begin{pmatrix}1\1\0\end{pmatrix}
\end{equation}
or more compactly
\begin{equation}
c_\mathrm{f} = C_3 c_\mathrm{i}.
\end{equation}
The set of all symmetry operations for a particular configuration space is a group. 
For an equilateral triangle this group is the $C_{3v}$ point group, which contains six symmetry operations: the identity, three reflections (each with a mirror plane bisecting the triangle and passing through $a$, $b$, or $c$ respectively) and two rotations (120° clockwise and counterclockwise).
\begin{equation}
C_{3v} = \left{ E, \sigma_\mathrm{a}, \sigma_\mathrm{b}, \sigma_\mathrm{c}, C_3, C_3^\prime \right}
\end{equation}
Modelling this using bsym
The SymmetryOperation class
In bsym, a symmetry operation is represented by an instance of the SymmetryOperation class. A SymmetryOperation instance can be initialised from the matrix representation of the corresponding symmetry operation. 
For example, in the trigonal configuration space above, a SymmetryOperation describing the identify, $E$, can be created with
End of explanation
SymmetryOperation([[ 1, 0, 0 ], 
                   [ 0, 1, 0 ], 
                   [ 0, 0, 1 ]], label='E' )
Explanation: Each SymmetryOperation has an optional label attribute. This can be set at records the matrix representation of the symmetry operation and an optional label. We can provide the label when creating a SymmetryOperation:
End of explanation
e = SymmetryOperation([[ 1, 0, 0 ], 
                       [ 0, 1, 0 ], 
                       [ 0, 0, 1 ]])
e.label = 'E'
e
Explanation: or set it afterwards:
End of explanation
c_3 = SymmetryOperation( [ [ 0, 0, 1 ],
                           [ 1, 0, 0 ],
                           [ 0, 1, 0 ] ], label='C3' )
c_3
Explanation: Or for $C_3$:
End of explanation
c_3_from_vector = SymmetryOperation.from_vector( [ 2, 3, 1 ], label='C3' )
c_3_from_vector
Explanation: Vector representations of symmetry operations
The matrix representation of a symmetry operation is a permutation matrix. Each row maps one position in the corresponding configuration space to one other position. An alternative, condensed, representation for each symmetry operation matrix uses vector notation, where each element gives the row containing 1 in the equivalent matrix column. e.g. for $C_3$ the vector mapping is given by $\left[2,3,1\right]$, corresponding to the mapping $1\to2$, $2\to3$, $3\to1$.
End of explanation
c_3.as_vector()
Explanation: The vector representation of a SymmetryOperation can be accessed using the as_vector() method.
End of explanation
c_3     = SymmetryOperation.from_vector( [ 2, 3, 1 ], label='C3' )
c_3_inv = SymmetryOperation.from_vector( [ 3, 1, 2 ], label='C3_inv' )
print( c_3, '\n' )
print( c_3_inv, '\n' )
Explanation: Inverting symmetry operations
For every symmetry operation, $A$, there is an inverse operation, $A^{-1}$, such that 
\begin{equation}
A \cdot A^{-1}=E.
\end{equation}
For example, the inverse of $C_3$ (clockwise rotation by 120°) is $C_3^\prime$ (anticlockwise rotation by 120°):
End of explanation
c_3 * c_3_inv
Explanation: The product of $C_3$ and $C_3^\prime$ is the identity, $E$.
End of explanation
c_3.invert()
Explanation: <img src="figures/triangular_c3_inversion.pdf" />
c_3_inv can also be generated using the .invert() method
End of explanation
c_3.invert( label= 'C3_inv')
c_3.invert().set_label( 'C3_inv' )
Explanation: The resulting SymmetryOperation does not have a label defined. This can be set directly, or by chaining the .set_label() method, e.g.
End of explanation
from bsym import PointGroup
# construct SymmetryOperations for C_3v group
e       = SymmetryOperation.from_vector( [ 1, 2, 3 ], label='e' )
c_3     = SymmetryOperation.from_vector( [ 2, 3, 1 ], label='C_3' )
c_3_inv = SymmetryOperation.from_vector( [ 3, 1, 2 ], label='C_3_inv' )
sigma_a = SymmetryOperation.from_vector( [ 1, 3, 2 ], label='S_a' )
sigma_b = SymmetryOperation.from_vector( [ 3, 2, 1 ], label='S_b' )
sigma_c = SymmetryOperation.from_vector( [ 2, 1, 3 ], label='S_c' )
Explanation: The SymmetryGroup class
A SymmetryGroup is a collections of SymmetryOperation objects. A SymmetryGroup is not required to contain all the symmetry operations of a particular configuration space, and therefore is not necessarily a complete mathematical <a href="https://en.wikipedia.org/wiki/Group_(mathematics)#Definition">group</a>.
For convenience bsym has PointGroup and SpaceGroup classes, that are equivalent to the SymmetryGroup parent class.
End of explanation
c3v = PointGroup( [ e, c_3, c_3_inv, sigma_a, sigma_b, sigma_c ] )
c3v
Explanation: <img src="figures/triangular_c3v_symmetry_operations.pdf" />
End of explanation
from bsym import ConfigurationSpace
c = ConfigurationSpace( objects=['a', 'b', 'c' ], symmetry_group=c3v )
c
Explanation: The ConfigurationSpace class
A ConfigurationSpace consists of a set of objects that represent the configuration space vectors, and the SymmetryGroup containing the relevant symmetry operations.
End of explanation
from bsym import Configuration
conf_1 = Configuration( [ 1, 1, 0 ] )
conf_1
Explanation: The Configuration class
A Configuration instance describes a particular configuration, i.e. how a set of objects are arranged within a configuration space. Internally, a Configuration is represented as a vector (as a numpy array).
Each element in a configuration is represented by a single digit non-negative integer.
End of explanation
c1 = Configuration( [ 1, 1, 0 ] )
c_3 = SymmetryOperation.from_vector( [ 2, 3, 1 ] )
c_3.operate_on( c1 )
c_3 * conf_1
Explanation: The effect of a particular symmetry operation acting on a configuration can now be calculated using the SymmetryOperation.operate_on() method, or by direct multiplication, e.g.
End of explanation
c = ConfigurationSpace( [ 'a', 'b', 'c', 'd' ] ) # four vector configuration space
Explanation: <img src="figures/triangular_rotation_operation.pdf" />
Finding symmetry-inequivalent permutations.
A common question that comes up when considering the symmetry properties of arrangements of objects is: how many ways can these be arranged that are not equivalent by symmetry?
As a simple example of solving this problem using bsym consider four equivalent sites arranged in a square.
<img src="figures/square_configuration_space.pdf">
End of explanation
c
Explanation: This ConfigurationSpace has been created without a symmetry_group argument. The default behaviour in this case is to create a SymmetryGroup containing only the identity, $E$.
End of explanation
c.unique_configurations( {1:2, 0:2} )
Explanation: We can now calculate all symmetry inequivalent arrangements where two sites are occupied and two are unoccupied, using the unique_configurations() method. This takes as a argument a dict with the numbers of labels to be arranged in the configuration space. Here, we use the labels 1 and 0 to represent occupied and unoccupied sites, respectively, and the distribution of sites is given by { 1:2, 0:2 }.
End of explanation
[ uc.count for uc in c.unique_configurations( {1:2, 0:2} ) ]
Explanation: Because we have not yet taken into account the symmetry of the configuration space, we get
\begin{equation}
\frac{4\times3}{2}
\end{equation}
unique configurations (where the factor of 2 comes from the occupied sites being indistinguishable).
The configurations generated by unique_configurations have a count attribute that records the number of symmetry equivalent configurations of each case:
In this example, each configuration appears once:
End of explanation
# construct point group
e        = SymmetryOperation.from_vector( [ 1, 2, 3, 4 ], label='E' )
c4       = SymmetryOperation.from_vector( [ 2, 3, 4, 1 ], label='C4' )
c4_inv   = SymmetryOperation.from_vector( [ 4, 1, 2, 3 ], label='C4i' )
c2       = SymmetryOperation.from_vector( [ 3, 4, 1, 2 ], label='C2' )
sigma_x  = SymmetryOperation.from_vector( [ 4, 3, 2, 1 ], label='s_x' )
sigma_y  = SymmetryOperation.from_vector( [ 2, 1, 4, 3 ], label='s_y' )
sigma_ac = SymmetryOperation.from_vector( [ 1, 4, 3, 2 ], label='s_ac' )
sigma_bd = SymmetryOperation.from_vector( [ 3, 2, 1, 4 ], label='s_bd' )
c4v = PointGroup( [ e, c4, c4_inv, c2, sigma_x, sigma_y, sigma_ac, sigma_bd ] )
# create ConfigurationSpace with the c4v PointGroup.
c = ConfigurationSpace( [ 'a', 'b', 'c', 'd' ], symmetry_group=c4v )
c
c.unique_configurations( {1:2, 0:2} )
[ uc.count for uc in c.unique_configurations( {1:2, 0:2 } ) ]
Explanation: We can also calculate the result when all symmetry operations of this configuration space are included.
End of explanation
c.unique_configurations( {2:1, 1:1, 0:2} )
[ uc.count for uc in c.unique_configurations( {2:1, 1:1, 0:2 } ) ]
Explanation: Taking symmetry in to account, we now only have two unique configurations: either two adjacent site are occupied (four possible ways), or two diagonal sites are occupied (two possible ways):
<img src="figures/square_unique_configurations.pdf" >
The unique_configurations() method can also handle non-binary site occupations:
End of explanation
from pymatgen.core.lattice import Lattice
from pymatgen.core.structure import Structure
import numpy as np
# construct a pymatgen Structure instance using the site fractional coordinates
coords = np.array( [ [ 0.0, 0.0, 0.0 ] ] )
atom_list = [ 'Li' ]
lattice = Lattice.from_parameters( a=1.0, b=1.0, c=1.0, alpha=90, beta=90, gamma=90 )
parent_structure = Structure( lattice, atom_list, coords ) * [ 4, 4, 1 ]
parent_structure.cart_coords.round(2)
Explanation: <img src="figures/square_unique_configurations_2.pdf">
Working with crystal structures using pymatgen
One example where the it can be useful to identify symmetry-inequivalent arrangements of objects in a vector space, is when considering the possible arrangements of disordered atoms on a crystal lattice. 
To solve this problem for an arbitrary crystal structure, bsym contains an interface to pymatgen that will identify symmetry-inequivalent atom substitutions in a given pymatgen Structure.
As an example, consider a $4\times4$ square-lattice supercell populated by lithium atoms.
End of explanation
from bsym.interface.pymatgen import unique_structure_substitutions
print( unique_structure_substitutions.__doc__ )
Explanation: We can use the bsym.interface.pymatgen.unique_structure_substitutions() function to identify symmetry-inequivalent structures generated by substituting at different sites.
End of explanation
unique_structures = unique_structure_substitutions( parent_structure, 'Li', { 'Na':1, 'Li':15 } )
len( unique_structures )
Explanation: As a trivial example, when substituting one Li atom for Na, we get a single unique structure
End of explanation
na_substituted = unique_structures[0]
Explanation: <img src="figures/pymatgen_example_one_site.pdf">
End of explanation
unique_structures_with_Mg = unique_structure_substitutions( na_substituted, 'Li', { 'Mg':1, 'Li':14 } )
len( unique_structures_with_Mg )
[ s.number_of_equivalent_configurations for s in unique_structures_with_Mg ]
Explanation: This Li$\to$Na substitution breaks the symmetry of the $4\times4$ supercell. 
If we now replace a second lithium with a magnesium atom, we generate five symmetry inequivalent structures:
End of explanation
[ s.full_configuration_degeneracy for s in unique_structures_with_Mg ]
Explanation: number_of_equivalent_configurations only lists the number of equivalent configurations found when performing the second substitution, when the list of structures unique_structures_with_Mg was created. The full configuration degeneracy relative to the initial empty 4×4 lattice can be queried using full_configuration_degeneracy.
End of explanation
# Check the squared distances between the Na and Mg sites in these unique structures are [1, 2, 4, 5, 8]
np.array( sorted( [ s.get_distance( s.indices_from_symbol('Na')[0], 
                                    s.indices_from_symbol('Mg')[0] )**2 for s in unique_structures_with_Mg ] ) )
Explanation: <img src="figures/pymatgen_example_two_sites.pdf">
End of explanation
unique_structures = unique_structure_substitutions( parent_structure, 'Li', { 'Mg':1, 'Na':1, 'Li':14 } )
len(unique_structures)
np.array( sorted( [ s.get_distance( s.indices_from_symbol('Na')[0], 
                                    s.indices_from_symbol('Mg')[0] ) for s in unique_structures ] ) )**2
[ s.number_of_equivalent_configurations for s in unique_structures ]
Explanation: This double substitution can also be done in a single step:
End of explanation
[ s.full_configuration_degeneracy for s in unique_structures ]
Explanation: Because both substitutions were performed in a single step, number_of_equivalent_configurations and full_configuration_degeneracy now contain the same data:
End of explanation
from bsym.interface.pymatgen import ( space_group_symbol_from_structure, 
                                      space_group_from_structure, 
                                      configuration_space_from_structure )
Explanation: Constructing SpaceGroup and ConfigurationSpace objects using pymatgen
The bsym.interface.pymatgen module contains functions for generating SpaceGroup and ConfigurationSpace objects directly from pymatgen Structure objects.
End of explanation
coords = np.array( [ [ 0.0, 0.0, 0.0 ],
                     [ 0.5, 0.5, 0.0 ],
                     [ 0.0, 0.5, 0.5 ],
                     [ 0.5, 0.0, 0.5 ] ] )
atom_list = [ 'Li' ] * len( coords )
lattice = Lattice.from_parameters( a=3.0, b=3.0, c=3.0, alpha=90, beta=90, gamma=90 )
structure = Structure( lattice, atom_list, coords )
space_group_symbol_from_structure( structure )
space_group_from_structure( structure )
configuration_space_from_structure( structure )
Explanation: Documentation:
space_group_symbol_from_structure
space_group_from_structure
configuration_space_from_structure
End of explanation
a = 3.798 # lattice parameter
coords = np.array( [ [ 0.0, 0.0, 0.0 ],
                     [ 0.5, 0.0, 0.0 ],
                     [ 0.0, 0.5, 0.0 ],
                     [ 0.0, 0.0, 0.5 ] ] )
atom_list = [ 'Ti', 'X', 'X', 'X' ]
lattice = Lattice.from_parameters( a=a, b=a, c=a, alpha=90, beta=90, gamma=90 )
unit_cell = Structure( lattice, atom_list, coords )
parent_structure = unit_cell * [ 2, 2, 2 ]
unique_structures = unique_structure_substitutions( parent_structure, 'X', { 'O':8, 'F':16 }, 
                                                    show_progress='notebook' )
%load_ext version_information
%version_information bsym, numpy, jupyter, pymatgen, tqdm
Explanation: Progress bars
bsym.ConfigurationSpace.unique_configurations() and bsym.interface.pymatgen.unique_structure_substitutions() both accept optional show_progress arguments, which can be used to display progress bars (using tqdm(https://tqdm.github.io). 
Setting show_progress=True will give a simple progress bar. If you are running bsym in a Jupyter notebook, setting show_progress="notebook" will give you a progress bar as a notebook widget.
(note, the widget status is not saved with this notebook, and may not display correctly on GitHub or using nbviewer)
In the example below, we find all unique configurations for the pseudo-ReO<sub>3</sub> structured TiOF<sub>2</sub> in a 2&times;2&times;2 supercell.
End of explanation
<END_TASK>