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,

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.

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$:

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:

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


In [1]:
from bsym import SymmetryOperation

In [2]:
SymmetryOperation([[ 1, 0, 0 ], 
                   [ 0, 1, 0 ], 
                   [ 0, 0, 1 ]])


Out[2]:
SymmetryOperation
label(---)
array([[1, 0, 0],
       [0, 1, 0],
       [0, 0, 1]])

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:


In [3]:
SymmetryOperation([[ 1, 0, 0 ], 
                   [ 0, 1, 0 ], 
                   [ 0, 0, 1 ]], label='E' )


Out[3]:
SymmetryOperation
label(E)
array([[1, 0, 0],
       [0, 1, 0],
       [0, 0, 1]])

or set it afterwards:


In [4]:
e = SymmetryOperation([[ 1, 0, 0 ], 
                       [ 0, 1, 0 ], 
                       [ 0, 0, 1 ]])
e.label = 'E'
e


Out[4]:
SymmetryOperation
label(E)
array([[1, 0, 0],
       [0, 1, 0],
       [0, 0, 1]])

Or for $C_3$:


In [5]:
c_3 = SymmetryOperation( [ [ 0, 0, 1 ],
                           [ 1, 0, 0 ],
                           [ 0, 1, 0 ] ], label='C3' )
c_3


Out[5]:
SymmetryOperation
label(C3)
array([[0, 0, 1],
       [1, 0, 0],
       [0, 1, 0]])

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$.


In [6]:
c_3_from_vector = SymmetryOperation.from_vector( [ 2, 3, 1 ], label='C3' )
c_3_from_vector


Out[6]:
SymmetryOperation
label(C3)
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]])

The vector representation of a SymmetryOperation can be accessed using the as_vector() method.


In [7]:
c_3.as_vector()


Out[7]:
[2, 3, 1]

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°):


In [8]:
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' )


SymmetryOperation
label(C3)
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]]) 

SymmetryOperation
label(C3_inv)
array([[0., 1., 0.],
       [0., 0., 1.],
       [1., 0., 0.]]) 

The product of $C_3$ and $C_3^\prime$ is the identity, $E$.


In [9]:
c_3 * c_3_inv


Out[9]:
SymmetryOperation
label(---)
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

c_3_inv can also be generated using the .invert() method


In [10]:
c_3.invert()


Out[10]:
SymmetryOperation
label(---)
array([[0, 1, 0],
       [0, 0, 1],
       [1, 0, 0]])

The resulting SymmetryOperation does not have a label defined. This can be set directly, or by chaining the .set_label() method, e.g.


In [11]:
c_3.invert( label= 'C3_inv')


Out[11]:
SymmetryOperation
label(C3_inv)
array([[0, 1, 0],
       [0, 0, 1],
       [1, 0, 0]])

In [12]:
c_3.invert().set_label( 'C3_inv' )


Out[12]:
SymmetryOperation
label(C3_inv)
array([[0, 1, 0],
       [0, 0, 1],
       [1, 0, 0]])

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 group.

For convenience bsym has PointGroup and SpaceGroup classes, that are equivalent to the SymmetryGroup parent class.


In [13]:
from bsym import PointGroup

In [14]:
# 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' )


In [15]:
c3v = PointGroup( [ e, c_3, c_3_inv, sigma_a, sigma_b, sigma_c ] )

In [16]:
c3v


Out[16]:
PointGroup
e	[1, 2, 3]
C_3	[2, 3, 1]
C_3_inv	[3, 1, 2]
S_a	[1, 3, 2]
S_b	[3, 2, 1]
S_c	[2, 1, 3]

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.


In [17]:
from bsym import ConfigurationSpace

In [18]:
c = ConfigurationSpace( objects=['a', 'b', 'c' ], symmetry_group=c3v )

In [19]:
c


Out[19]:
ConfigurationSpace
['a', 'b', 'c']
e	[1, 2, 3]
C_3	[2, 3, 1]
C_3_inv	[3, 1, 2]
S_a	[1, 3, 2]
S_b	[3, 2, 1]
S_c	[2, 1, 3]

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.


In [20]:
from bsym import Configuration

conf_1 = Configuration( [ 1, 1, 0 ] )
conf_1


Out[20]:
Configuration([1 1 0])

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.


In [21]:
c1 = Configuration( [ 1, 1, 0 ] )
c_3 = SymmetryOperation.from_vector( [ 2, 3, 1 ] )
c_3.operate_on( c1 )


Out[21]:
Configuration([0 1 1])

In [22]:
c_3 * conf_1


Out[22]:
Configuration([0 1 1])

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.


In [23]:
c = ConfigurationSpace( [ 'a', 'b', 'c', 'd' ] ) # four vector configuration space

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$.


In [24]:
c


Out[24]:
ConfigurationSpace
['a', 'b', 'c', 'd']
E	[1, 2, 3, 4]

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 }.


In [25]:
c.unique_configurations( {1:2, 0:2} )


Out[25]:
[Configuration([0 0 1 1]),
 Configuration([0 1 0 1]),
 Configuration([0 1 1 0]),
 Configuration([1 0 0 1]),
 Configuration([1 0 1 0]),
 Configuration([1 1 0 0])]

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:


In [26]:
[ uc.count for uc in c.unique_configurations( {1:2, 0:2} ) ]


Out[26]:
[1, 1, 1, 1, 1, 1]

We can also calculate the result when all symmetry operations of this configuration space are included.


In [27]:
# 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


Out[27]:
ConfigurationSpace
['a', 'b', 'c', 'd']
E	[1, 2, 3, 4]
C4	[2, 3, 4, 1]
C4i	[4, 1, 2, 3]
C2	[3, 4, 1, 2]
s_x	[4, 3, 2, 1]
s_y	[2, 1, 4, 3]
s_ac	[1, 4, 3, 2]
s_bd	[3, 2, 1, 4]

In [28]:
c.unique_configurations( {1:2, 0:2} )


Out[28]:
[Configuration([0 0 1 1]), Configuration([0 1 0 1])]

In [29]:
[ uc.count for uc in c.unique_configurations( {1:2, 0:2 } ) ]


Out[29]:
[4, 2]

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):

The unique_configurations() method can also handle non-binary site occupations:


In [30]:
c.unique_configurations( {2:1, 1:1, 0:2} )


Out[30]:
[Configuration([0 0 1 2]), Configuration([0 1 0 2])]

In [31]:
[ uc.count for uc in c.unique_configurations( {2:1, 1:1, 0:2 } ) ]


Out[31]:
[8, 4]

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.


In [32]:
from pymatgen import Lattice, Structure
import numpy as np

In [33]:
# 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)


Out[33]:
array([[ 0.,  0.,  0.],
       [-0.,  1.,  0.],
       [-0.,  2.,  0.],
       [-0.,  3.,  0.],
       [ 1.,  0.,  0.],
       [ 1.,  1.,  0.],
       [ 1.,  2.,  0.],
       [ 1.,  3.,  0.],
       [ 2.,  0.,  0.],
       [ 2.,  1.,  0.],
       [ 2.,  2.,  0.],
       [ 2.,  3.,  0.],
       [ 3.,  0.,  0.],
       [ 3.,  1.,  0.],
       [ 3.,  2.,  0.],
       [ 3.,  3.,  0.]])

We can use the bsym.interface.pymatgen.unique_structure_substitutions() function to identify symmetry-inequivalent structures generated by substituting at different sites.


In [34]:
from bsym.interface.pymatgen import unique_structure_substitutions

In [35]:
print( unique_structure_substitutions.__doc__ )


    Generate all symmetry-unique structures formed by substituting a set of sites in a `pymatgen` structure.

    Args:
        structure (pymatgen.Structure): The parent structure.
        to_substitute (str): atom label for the sites to be substituted.
        site_distribution (dict): A dictionary that defines the number of each substituting element.
        verbose (bool): verbose output.
        atol      (Optional [float]):       tolerance factor for the ``pymatgen`` `coordinate mapping`_ under each symmetry operation. Default=1e-5.
        show_progress (opt:default=False): Show a progress bar.
                                           Setting to `True` gives a simple progress bar.
                                           Setting to `"notebook"` gives a Jupyter notebook compatible progress bar.


    Returns:
        (list[Structure]): A list of Structure objects for each unique substitution.
    
    Notes:
        The number of symmetry-equivalent configurations for each structure 
        is stored in the `number_of_equivalent_configurations` attribute. 
     
        If the parent structure was previously generated using this function
        (as part of a sequence of substitutions) the full configuration
        degeneracy of each symmetry inequivalent configuration is stored in
        the `full_configuration_degeneracy` attribute. If the parent structure
        is a standard Pymatgen Structure object, `number_of_equivalent_configurations`
        and `full_configuration_degeneracy` will be equal.

    .. _coordinate mapping:
        http://pymatgen.org/pymatgen.util.coord_utils.html#pymatgen.util.coord_utils.coord_list_mapping_pbc

    

As a trivial example, when substituting one Li atom for Na, we get a single unique structure


In [36]:
unique_structures = unique_structure_substitutions( parent_structure, 'Li', { 'Na':1, 'Li':15 } )
len( unique_structures )


Out[36]:
1


In [37]:
na_substituted = unique_structures[0]

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:


In [38]:
unique_structures_with_Mg = unique_structure_substitutions( na_substituted, 'Li', { 'Mg':1, 'Li':14 } )
len( unique_structures_with_Mg )


Out[38]:
5

In [39]:
[ s.number_of_equivalent_configurations for s in unique_structures_with_Mg ]


Out[39]:
[4, 2, 4, 4, 1]

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.


In [40]:
[ s.full_configuration_degeneracy for s in unique_structures_with_Mg ]


Out[40]:
[64, 32, 64, 64, 16]


In [41]:
# 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 ] ) )


Out[41]:
array([1., 2., 4., 5., 8.])

This double substitution can also be done in a single step:


In [42]:
unique_structures = unique_structure_substitutions( parent_structure, 'Li', { 'Mg':1, 'Na':1, 'Li':14 } )

In [43]:
len(unique_structures)


Out[43]:
5

In [44]:
np.array( sorted( [ s.get_distance( s.indices_from_symbol('Na')[0], 
                                    s.indices_from_symbol('Mg')[0] ) for s in unique_structures ] ) )**2


Out[44]:
array([1., 2., 4., 5., 8.])

In [45]:
[ s.number_of_equivalent_configurations for s in unique_structures ]


Out[45]:
[64, 32, 64, 64, 16]

Because both substitutions were performed in a single step, number_of_equivalent_configurations and full_configuration_degeneracy now contain the same data:


In [46]:
[ s.full_configuration_degeneracy for s in unique_structures ]


Out[46]:
[64, 32, 64, 64, 16]

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.


In [47]:
from bsym.interface.pymatgen import ( space_group_symbol_from_structure, 
                                      space_group_from_structure, 
                                      configuration_space_from_structure )

In [48]:
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 )

In [49]:
space_group_symbol_from_structure( structure )


Out[49]:
'Fm-3m'

In [50]:
space_group_from_structure( structure )


Out[50]:
SymmetryGroup
None	[1, 2, 3, 4]
None	[3, 4, 1, 2]
None	[1, 4, 3, 2]
None	[2, 3, 4, 1]
None	[4, 3, 2, 1]
None	[3, 2, 1, 4]
None	[2, 1, 4, 3]
None	[4, 1, 2, 3]
None	[4, 1, 3, 2]
None	[3, 2, 4, 1]
None	[2, 4, 1, 3]
None	[1, 3, 2, 4]
None	[1, 4, 2, 3]
None	[3, 1, 4, 2]
None	[2, 3, 1, 4]
None	[4, 2, 3, 1]
None	[3, 1, 2, 4]
None	[3, 4, 2, 1]
None	[2, 4, 3, 1]
None	[2, 1, 3, 4]
None	[1, 3, 4, 2]
None	[1, 2, 4, 3]
None	[4, 2, 1, 3]
None	[4, 3, 1, 2]

In [51]:
configuration_space_from_structure( structure )


Out[51]:
ConfigurationSpace
[1, 2, 3, 4]
None	[1, 2, 3, 4]
None	[3, 4, 1, 2]
None	[1, 4, 3, 2]
None	[2, 3, 4, 1]
None	[4, 3, 2, 1]
None	[3, 2, 1, 4]
None	[2, 1, 4, 3]
None	[4, 1, 2, 3]
None	[4, 1, 3, 2]
None	[3, 2, 4, 1]
None	[2, 4, 1, 3]
None	[1, 3, 2, 4]
None	[1, 4, 2, 3]
None	[3, 1, 4, 2]
None	[2, 3, 1, 4]
None	[4, 2, 3, 1]
None	[3, 1, 2, 4]
None	[3, 4, 2, 1]
None	[2, 4, 3, 1]
None	[2, 1, 3, 4]
None	[1, 3, 4, 2]
None	[1, 2, 4, 3]
None	[4, 2, 1, 3]
None	[4, 3, 1, 2]

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-ReO3 structured TiOF2 in a 2×2×2 supercell.


In [52]:
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' )




In [53]:
%load_ext version_information
%version_information bsym, numpy, jupyter, pymatgen, tqdm


Out[53]:
SoftwareVersion
Python3.7.0 64bit [Clang 10.0.0 (clang-1000.10.44.2)]
IPython7.0.1
OSDarwin 18.2.0 x86_64 i386 64bit
bsym1.1.1
numpy1.15.2
jupyter1.0.0
pymatgen2018.10.18
tqdm4.28.1
Sun Feb 10 12:08:49 2019 GMT