User guide: atoms and elements

The basis of structure manipulations is to manipulate atoms. Atom objects are in the category of Transformable objects, meaning that their coordinates can be transformed according to any affine transform.

To create an atom, simply provide its element and coordinates:

>>> from crystals import Atom
>>>
>>> copper = Atom(element = 'Cu', coords = [0,0,0])

Optional information can be given, such as magnetic moment and mean-squared displacement. For users of ase, another possibility is to instantiate an Atom from an ase.Atom using the Atom.from_ase() constructor.

Atom instances are hashable; they can be used as dict keys or stored in a set.

Since we are most concerned with atoms in crystals, the coordinates here are assumed to be fractional. If the atom was created as part of a structure, the real-space position with respect to its parent (Crystal or Lattice) can be accessed using the Atom.coords_cartesian() method:

>>> from crystals import Crystal
>>> graphite = Crystal.from_database('C')
>>>
>>> carbon = sorted(graphite)[-1]
>>> carbon.coords_fractional
array([0.66667, 0.33334, 0.75   ])
>>> carbon.coords_cartesian
array([1.42259818e+00, 1.23200000e-05, 5.03325000e+00])

Atomic distances

The fractional/cartesian distance between two atoms sitting on the same lattice is possible:

>>> from crystals import distance_fractional, distance_cartesian
>>> graphite = Crystal.from_database('C')
>>>
>>> carbon1, carbon2, *_ = tuple(sorted(graphite))
>>> carbon1
< Atom C  @ (0.00, 0.00, 0.25) >
>>> carbon2
< Atom C  @ (0.00, 0.00, 0.75) >
>>> distance_fractional(carbon1, carbon2)
0.5
>>> distance_cartesian(carbon1, carbon2)    # in Angstroms
3.3555000000000006

If atoms are not sitting on the same lattice, calculating the distance should not be defined. In this case, an exception is raised:

>>> gold = Crystal.from_database('Au')
>>> silver = Crystal.from_database('Ag')
>>>
>>> gold1, *_ = tuple(sorted(gold))
>>> silver1, *_ = tuple(sorted(silver))
>>>
>>> distance_cartesian(gold1, silver1) 
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "(...omitted...)\crystals\atom.py",  in distance_cartesian
    "Cartesian distance is undefined if atoms are sitting on different lattices."
RuntimeError: Distance is undefined if atoms are sitting on different lattices.

The Element class

If all you want is access to elemental information, like atomic weights, you can instantiate an Element instead of an Atom:

>>> from crystals import Element
>>> Element("H")
< Hydrogen >
>>> Element("Hydrogen")
< Hydrogen >

You can specify elements by their atomic number as well:

>>> Element(29)
< Copper >
>>> Element(29).symbol
'Cu'
>>> Element(29).name
'Copper'

Element instances give access to atomic properties:

>>> Element("Cu").mass # Atomic mass in [u]
63.546
>>> Element("Cu").atomic_number
29

Since Atom is a subclass of Element, all of the above examples also work for Atom:

>>> from crystals import Atom
>>> Atom("Cu", coords = [0,0,0]).mass
63.546

Handling the atomic orbital structure of atoms

For certain applications, access to the electronic structure of orbitals is important. To that end, Atom instances carry this information in instances of ElectronicStructure.

You can create an electronic structure by hand:

>>> from crystals import ElectronicStructure
>>> ElectronicStructure({"1s": 2, "2s": 2, "2p": 2})
< ElectronicStructure: 1s²2s²2p² >

It is much more ergonomic to start from the ground state of an element. For example:

>>> ElectronicStructure.ground_state("C")
< ElectronicStructure: 1s²2s²2p² >

Once you have a starting point, the electronic structure can be modified for your application:

>>> structure = ElectronicStructure.ground_state("C")
>>> structure["2p"] -= 1
>>> structure["3d"] += 1
>>> structure
< ElectronicStructure: 1s²2s²2p¹3d¹ >

You can always check which orbital is the outermost orbital:

>>> structure = ElectronicStructure.ground_state("Ar")
>>> structure.outer_shell
<Orbital.three_p: '3p'>

Note that you cannot create impossible electronic structures, however:

>>> structure = ElectronicStructure.ground_state("C")
>>> structure["2s"] = 3 
ValueError: There cannot be 3 electrons in orbital 2s

Finally, you can modify atomic electronic structures on a particular atom. By default, the electronic structure of atoms is set to the ground state. Let’s move an electron up from the “2p” to “3d” orbital in one atom of graphite:

>>> graphite = Crystal.from_database('C')
>>> graphite
< Crystal object with following unit cell:
    Atom C  @ (0.00, 0.00, 0.25)
    Atom C  @ (0.00, 0.00, 0.75)
    Atom C  @ (0.33, 0.67, 0.25)
    Atom C  @ (0.67, 0.33, 0.75)
Lattice parameters:
    a=2.464Å, b=2.464Å, c=6.711Å
    α=90.000°, β=90.000°, γ=120.000°
Chemical composition:
     C: 100.000% >
>>> atom, *_ = sorted(graphite)
>>> atom.electronic_structure["2p"] -= 1
>>> atom.electronic_structure["3d"] += 1
>>> graphite
< Crystal object with following unit cell:
    Atom C  @ (0.00, 0.00, 0.25) | [1s²2s²2p¹3d¹]
    Atom C  @ (0.00, 0.00, 0.75)
    Atom C  @ (0.33, 0.67, 0.25)
    Atom C  @ (0.67, 0.33, 0.75)
Lattice parameters:
    a=2.464Å, b=2.464Å, c=6.711Å
    α=90.000°, β=90.000°, γ=120.000°
Chemical composition:
     C: 100.000% >

Note that atoms with ground-state electronic structure don’t show it explicitly. You could entirely replace the electronic structure of an atom:

>>> graphite = Crystal.from_database('C')
>>> atom, *_ = sorted(graphite)
>>> atom.electronic_structure = ElectronicStructure.ground_state("Ti") # This is just an example.
>>> graphite
< Crystal object with following unit cell:
    Atom C  @ (0.00, 0.00, 0.25) | [1s²2s²2p⁶3s²3p⁶4s²3d²]
    Atom C  @ (0.00, 0.00, 0.75)
    Atom C  @ (0.33, 0.67, 0.25)
    Atom C  @ (0.67, 0.33, 0.75)
Lattice parameters:
    a=2.464Å, b=2.464Å, c=6.711Å
    α=90.000°, β=90.000°, γ=120.000°
Chemical composition:
     C: 100.000% >