\(\renewcommand\AA{\text{Å}}\)

Adding properties

The periodic table is extensible. Third party packages can add attributes to the table, and they will appear in all of the elements.

In order to add a new property to the table, you need to define a python package which contains the required information, and can attach the information to the periodic table so that it is available on demand. This is done with the function init(table) in your table extension.

This example adds the attribute discoverer to each element. First create the file discoverer/core.py:

"""
Partial table of element discoverers.

From http://en.wikipedia.org/wiki/Discoveries_of_the_chemical_elements.
"""

import periodictable.core

def init(table, reload=False):
    if 'discoverer' in table.properties and not reload: return
    table.properties.append('discoverer')

    # Set the default, if any
    periodictable.core.Element.discoverer = "Unknown"

    # Not numeric, so no discoverer_units

    # Load the data
    for name,person in data.items():
        el = table.name(name)
        el.discoverer = person

data = dict(
 arsenic="Jabir ibn Hayyan",
 antimony="Jabir ibn Hayyan",
 bismuth="Jabir ibn Hayyan",
 phosphorus="H. Brand",
 cobalt="G. Brandt",
 platinum="A. de Ulloa",
 nickel="A.F. Cronstedt",
 magnesium="J. Black",
)

Now that we have defined the init(table) function, we need a way to call it. The simplest solution is to load it directly when your package is imported. In the current example, this could be done by adding the following line to the end of the file:

init(periodictable.core.elements)

This would be fine for the current example because the table size is small and load time is fast. For large tables, you may wish to delay loading the table data until it is needed. To do this, we use the delayed_load function in our package init file discoverer/__init__.py:

import periodictable.core

# Delayed loading of the element discoverer information
def _load_discoverer():
    """
    The name of the person or group who discovered the element.
    """
    from . import core
    core.init(periodictable.core.default_table())
periodictable.core.delayed_load(['discoverer'], _load_discoverer)

The first argument to delayed_load is the list of all attributes that will be defined when the module is loaded. The second argument is the loading function, whose docstring will appear as the attribute description for each attribute in the first list.

Check that it works:

>>> import discoverer
>>> import periodictable
>>> print(periodictable.magnesium.discoverer)
J. Black

Isotope and ion specific data is also supported. In this case we need a data table that contains information for each isotope of each element. The following example uses a dictionary of elements, with a dictionary of isotopes for each. It adds the shells attribute to Fe[56] and Fe[58].

Define shelltable/core.py:

"""
Example of isotope specific extensions to the periodic table.
"""
from periodictable.core import Isotope

def init(table, reload=False):
    if 'shells' in table.properties and not reload: return
    table.properties.append('shells')

    # Set the default.  This is required, even if it is only
    # setting it to None.  If the attribute is missing then
    # the isotope data reverts to the element to supply the
    # value, which is almost certainly not what you want.
    Isotope.shells = None

    # Load the data
    for symbol,eldata in data.items():
        el = table.symbol(symbol)
        for iso,isodata in eldata.items():
            el[iso].shells = isodata

# Define the data
data = dict(
    Fe = {56: "56-Fe shell info",
          58: "58-Fe shell info",
         },
    )

Again, we are going to initialize the table with delayed loading. In this case it is very important that we set the isotope=True keyword in the delayed_load call. If we don’t, then the magic we use to return the correct value after loading the new table information fails. Since unknown attributes are delegated to the underlying element, the value for the natural abundance will be returned instead. On subsequent calls the isotope specific value will be returned.

This is demonstrated in shelltable/__init__.py:

import periodictable.core

# Delayed loading of the element discoverer information
def _load():
    """
    The name of the person or group who discovered the element.
    """
    from . import core
    core.init(periodictable.core.default_table())
periodictable.core.delayed_load(['shells'], _load,
                              isotope=True, element=False)

Check that it works:

>>> import shelltable
>>> import periodictable
>>> print(periodictable.Fe[56].shells)
56-Fe shell info
>>> print(periodictable.Ni[58].shells)
None

Ion specific data is more complicated, particularly because of the interaction with isotopes. For example, Ni[58].ion[3] should have the same neutron scattering factors as Ni[58] (the neutron is only sensitive to the nucleus), but different scattering factors from Ni.ion[3]. X-rays are sensitive to the electronic structure of the atom and not the nucleus, so Ni[58].ion[3].xray.f0(Q) and Ni.ion[3].xray.f0(Q) are the same but different from Ni.xray.f0(Q).

Current support for ion dependent properties is awkward. The X-ray table periodictable.xsf creates a specialized structure for each ion as it is requested. The magnetic form factor table periodictable.magnetic_ff does not try to support ion.magnetic_ff directly, but instead the user must request ion.magnetic_ff[ion.charge]. Support for ion mass, which is isotope mass adjusted for the number of electrons is built into the Ion class. There are not yet any examples of extension tables that depend on both isotope and ion.

Be sure to use the ion=True keyword for delayed_load when the table extension contains ion specific information.