Crystal#

For the full technical reference see Crystal

Crystal is a child of the the Lattice and utilize Atom for the storage of atoms. We recommend you to read about Lattice and Atom first.

Crystal behaves like a list of atoms. See List-like behavior for the details.

Import#

>>> # Exact import
>>> from wulfric.crystal import Crystal
>>> # Recommended import
>>> from wulfric import Crystal

For the examples in this page we need additional import and some predefined variables:

>>> from wulfric import Lattice, Atom

Creation of the Crystal#

To create a crystal you would typically need to define the lattice and a set of atoms:

  • lattice - instance of the Lattice class.

  • atoms - list of the Atom instances, which defines the set of atoms in the unit cell.

>>> lattice = Lattice(3.0, 3.0, 3.0, 90.0, 90.0, 90.0)
>>> atoms = [Atom('Si', (0.0, 0.0, 0.0))]
>>> crystal = Crystal(lattice, atoms)

Parameters for lattice creation can be passed to the Crystal constructor as keyword arguments:

>>> crystal = Crystal(a=3.0, b=3.0, c=3.0, alpha=90.0, beta=90.0, gamma=90.0)

By default the Crystal is created with the cubic lattice (\(a = 1\)) and no atoms.

>>> from wulfric import Crystal
>>> crystal = Crystal()
>>> crystal.cell
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
>>> crystal.type()
'CUB'
>>> crystal.atoms
[]

Adding atoms#

Adding atoms to the crystal is straightforward:

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> for atom in crystal.atoms:
...    print(atom.fullname, atom.position)
...
Cr__1 [0. 0. 0.]
Cr__2 [0.5 0.5 0.5]

By default atom coordinates are interpreted as relative. In order to add atom with absolute coordinates set the relative keyword argument to False:

>>> crystal = Crystal(cell=[[3., 0., 0.], [0., 3., 0.], [0., 0., 3.]])
>>> crystal.add_atom(Atom('Cr', (0.0, 0.0, 0.0)), relative=False)
>>> crystal.add_atom(Atom('Cr', (1.5, 1.5, 1.5)), relative=False)
>>> for atom in crystal.atoms:
...    print(atom.fullname, atom.position)
...
Cr__1 [0. 0. 0.]
Cr__2 [0.5 0.5 0.5]

Note

Crystal.get_distance() and Crystal.get_vector() methods are the ones that returns absolute coordinates by default.

It is possible to add atoms by passing the Atom instance:

>>> crystal = Crystal()
>>> Cr = Atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom(Cr)

Removing atoms#

Removing atoms is also straightforward:

>>> crystal = Crystal()
>>> crystal.add_atom("Cr", position=(0.0, 0.0, 0.0))
>>> atom = Atom("Cr", (0.5, 0.5, 0.5))
>>> crystal.add_atom(atom)
>>> crystal.remove_atom("Cr__1")
>>> for atom in crystal.atoms:
...    print(atom.fullname, atom.position)
...
Cr__2 [0.5 0.5 0.5]
>>> crystal.remove_atom(atom)
>>> crystal.atoms
[]
>>> crystal.add_atom("Cr", position = (0.0, 0.0, 0.0))
>>> crystal.add_atom("Cr", position = (1.0, 0.0, 0.0))
>>> crystal.add_atom("Cr", position = (1.0, 1.0, 0.0))
>>> crystal.add_atom("Cr", position = (1.0, 1.0, 1.0))
>>> # Same as len(crystal.atoms)
>>> len(crystal)
4
>>> # You can remove all atoms with the same name at once
>>> crystal.remove_atom("Cr")
>>> len(crystal)
0

Note

The Crystal.remove_atom() method can take either the name (or fullname) or instance of the atom as an argument.

Moving atoms in the cell#

Two routines are predefined:

  • Crystal.shift_atoms() - shift atoms in a way that the point in the middle of the atom's coordinates extremums will be at the given gravity_point.

    >>> crystal = Crystal(cell=[[2, 0, 0], [0, 2, 0], [0, 0, 2]])
    >>> crystal.add_atom('Cr1', position=(0.0, 0.0, 0.0))
    >>> crystal.add_atom('Cr2', position=(0.5, 0.5, 1.0))
    >>> crystal.shift_atoms(gravity_point=(0.5, 0.5, 0.5))
    >>> for atom in crystal.atoms:
    ...    print(atom.name, atom.position)
    ...
    Cr1 [0.25 0.25 0.  ]
    Cr2 [0.75 0.75 1.  ]
    >>> crystal.shift_atoms(gravity_point=(1,1,1), relative=False)
    >>> for atom in crystal.atoms:
    ...    print(atom.name, atom.position)
    ...
    Cr1 [0.25 0.25 0.  ]
    Cr2 [0.75 0.75 1.  ]
    
  • Crystal.cure_negative() - enforce all atoms to have positive relative coordinates.

    >>> crystal = Crystal(cell=[[2, 0, 0], [0, 2, 0], [0, 0, 2]])
    >>> crystal.add_atom('Cr1', position=(-0.5, 0.5, 0.0))
    >>> crystal.add_atom('Cr2', position=(0.1, 0.5, 0.0))
    >>> crystal.cure_negative()
    >>> for atom in crystal.atoms:
    ...    print(atom.name, atom.position)
    ...
    Cr1 [0.  0.5 0. ]
    Cr2 [0.6 0.5 0. ]
    

Atom getters#

There is a number of properties involving atom of the crystal you have access to:

  • Crystal.atoms - list of the Atom instances in the crystal.

  • Crystal.get_atom - get atom by name or by fullname.

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> atom1 = crystal.get_atom('Cr')
>>> atom1.name
'Cr'
>>> atom1.position
array([0., 0., 0.])
>>> atom1.index
1
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> atom2 = crystal.get_atom('Cr')
Traceback (most recent call last):
...
ValueError: Multiple matches found for name = Cr, index = None
>>> atom2 = crystal.get_atom('Cr__2')
>>> atom2.index
2
>>> crystal = Crystal(cell=[[2, 0, 0], [0, 2, 0],[0, 0, 2]])
>>> crystal.add_atom('Cr', position = (0.0, 0.5, 0.0))
>>> crystal.get_atom_coordinates('Cr')
array([0. , 0.5, 0. ])
>>> crystal.Cr.position
array([0. , 0.5, 0. ])
>>> crystal.get_atom_coordinates('Cr', relative=False)
array([0., 1., 0.])
>>> crystal = Crystal(cell=[[2, 0, 0], [0, 2, 0], [0, 0, 2]])
>>> crystal.add_atom('Cr', position = (0.0, 0.5, 0.0))
>>> crystal.add_atom('Cr', position = (0.5, 0.0, 0.0))
>>> # Gives result in absolute coordinates
>>> crystal.get_vector('Cr__1', 'Cr__2')
array([ 1., -1.,  0.])
>>> crystal.get_vector('Cr__1', 'Cr__2', relative=True)
array([ 0.5, -0.5,  0. ])
>>> crystal = Crystal(cell=[[2, 0, 0], [0, 2, 0], [0, 0, 2]])
>>> crystal.add_atom('Cr', position=(0.0, 0.5, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.0, 0.0))
>>> round(crystal.get_distance('Cr__1', 'Cr__2'), 4)
1.4142

List-like behavior#

Crystal class supports the logic of a list of atoms. The following list-like methods are implemented:

  • len(crystal) - returns the number of atoms in the crystal

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> len(crystal)
2
  • Iteration over the atoms

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> for atom in crystal:
...    print(atom.fullname)
...
Cr__1
Cr__2
  • in syntax returns True if the atom is present in the Crystal

>>> crystal = Crystal()
>>> atom = Atom('Cr', (0.0, 0.0, 0.0))
>>> crystal.add_atom(atom)
>>> atom in crystal
True
>>> "Cr" in crystal
True
>>> 'Cr__1' in crystal
True
>>> 'Cr__3' in crystal
False
>>> "Br" in crystal
False

Attribute and item access#

For the access to the atoms objects two additional interfaces are implemented. They both rely on the Crystal.get_atom() method.

  • Access via attribute

It is identical to passing the name to the Crystal.get_atom() method with return_all=False.

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> atom = crystal.Cr__1
>>> atom.fullname
'Cr__1'
>>> atom = crystal.Cr__2
>>> atom.fullname
'Cr__2'
>>> crystal.Cr
Traceback (most recent call last):
...
AttributeError: 'Crystal' object has either none or more than one 'Cr' atoms.
  • Access via item

It is identical to passing the name to the Crystal.get_atom() method with return_all=True.

>>> crystal = Crystal()
>>> crystal.add_atom('Cr', position=(0.0, 0.0, 0.0))
>>> crystal.add_atom('Cr', position=(0.5, 0.5, 0.5))
>>> atoms = crystal["Cr__1"]
>>> atoms[0].fullname
'Cr__1'
>>> atoms = crystal["Cr__2"]
>>> atoms[0].fullname
'Cr__2'
>>> atoms = crystal["Cr"]
>>> for atom in atoms:
...    print(atom.fullname)
...
Cr__1
Cr__2

Note

Second method returns a list of atoms, while the first one returns a single atom.