Introduction to atomman: Atoms class

Lucas M. Hale, lucas.hale@nist.gov, Materials Science and Engineering Division, NIST.

Disclaimers

1. Introduction

The Atoms class collects per-atom properties. The basic behaviors of the class are:

  • The number of atoms is immutable after initializing.

  • The only default per-atom properties are an integer atomic type ‘atype’ and 3D position vector ‘pos’.

  • Any other per-atom property can be freely assigned of any shape or type.

  • When creating a new per-atom property, values must be given for all atoms, and the types must be consistent.

Note: The underlying structure of Atoms changed with version 1.2 to be more memory efficient and easier to work with. The methods and attributes were designed with the old versions in mind, but there is no guarantee of complete backwards compatibility.

Library Imports

[1]:
# Standard Python libraries
import datetime

# http://www.numpy.org/
import numpy as np

# https://github.com/usnistgov/atomman
import atomman as am

# Show atomman version
print('atomman version =', am.__version__)

# Show date of Notebook execution
print('Notebook executed on', datetime.date.today())
atomman version = 1.3.2
Notebook executed on 2020-04-15

2. Basics

2.1 Initialization

Parameters

  • natoms (int, optional) The number of atoms to associate with the Atoms instance. This is constant once the Atoms object is initialized. If not given, will be inferred from the length of atype and/or pos.

  • atype (int or list of ints, optional) The per-atom integer atomic types. If not given, atype is set to 1 for all atoms.

  • pos (numpy.ndarray, optional) The per-atom 3D atomic position vector. If not given, pos is set to [0,0,0] for all atoms.

  • prop (dict, optional) Dictionary containing all per-atom properties to set, alternate to passing the per-atom properties in as function parameters. Included for backwards compatibility.

  • **kwargs (any) Other keyword parameters can be given for defining extra per-atom properties.

[2]:
# Define 10 atom system with random positions
# Notes: natoms inferred from first dimension of pos. Same atype assigned to all atoms
atoms = am.Atoms(atype = 1, pos = 4 * np.random.rand(10, 3))
print(atoms)
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369
      1       1   2.002   2.700   1.037
      2       1   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       1   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870

2.2 Pre-defined attributes

The class has a few pre-defined attributes:

  • natoms is the number of atoms. len(Atoms) returns the same thing.

  • atypes gives a list of all unique atype values.

  • natypes gives the number of unique atype values.

Update version 1.3.0: The way atypes and natypes are handled has changed. Backwards compatibility will be affected if previous Atoms had atype values of 0 or non-sequential atype values. The new method restricts atype values to be ≥ 1 and atypes returns all sequential integers from 1 through natypes=max(atype).

[3]:
print('atoms.natoms -> ', atoms.natoms)
print('len(atoms) ->   ', len(atoms))
print('atoms.atypes -> ', atoms.atypes)
print('atoms.natypes ->', atoms.natypes)
atoms.natoms ->  10
len(atoms) ->    10
atoms.atypes ->  (1,)
atoms.natypes -> 1

3. Per-atom properties

The per-atom properties of an Atoms instance can be interacted with in one of three ways:

  • as attributes of the Atoms instance, i.e. atoms.myprop.

  • as items in the Atoms.view dictionary.

  • by calling the Atoms.prop() (Section 6) or Atoms.prop_atype() (Section 7) methods.

3.1 List assigned per-atom properties

A list of all assigned per-atom properties can be retrieved using either:

  • atoms.prop()

  • atoms.view.keys()

[4]:
print('atoms.prop() ->     ', atoms.prop())
print('atoms.view.keys() ->', atoms.view.keys())
atoms.prop() ->      ['atype', 'pos']
atoms.view.keys() -> odict_keys(['atype', 'pos'])

3.2 Accessing per-atom properties

[5]:
print('atoms.atype ->            ', atoms.atype)
print("atoms.view['atype'] ->    ", atoms.view['atype'])
atoms.atype ->             [1 1 1 1 1 1 1 1 1 1]
atoms.view['atype'] ->     [1 1 1 1 1 1 1 1 1 1]

3.3 Setting values of existing per-atom properties

The same three options can be used for setting values to existing per-atom properties.

[6]:
print('setting: atoms.atype[2] = 2')
atoms.atype[2] = 2

print("setting: atoms.view['atype'][5] = 2")
atoms.view['atype'][5] = 2

print()
print('atoms.atype ->', atoms.atype)
setting: atoms.atype[2] = 2
setting: atoms.view['atype'][5] = 2

atoms.atype -> [1 1 2 1 1 2 1 1 1 1]

3.4 Assigning new per-atom properties

New per-atom properties can be assigned almost as easily as setting values of exising properties. The only limitations are that values must be given for all atoms and the data types and shapes must be consistent for all atoms.

Value setting rules:

  • Values being assigned must either have no length, a length of 1, or a length of natoms.

  • If the value has no length or a length of 1, the value will be assigned to all atoms.

  • If the value as a length of natoms, each item in value will be assigned to a different atom.

[7]:
# Assign stress as attribute (same value for all atoms)
# Note first dimension is 1!
atoms.stress = np.zeros([1, 3, 3])

print('atoms.prop() ->', atoms.prop())
print()

print('atoms.stress[0] ->')
print(atoms.stress[0])
atoms.prop() -> ['atype', 'pos', 'stress']

atoms.stress[0] ->
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

3.5 Viewing per-atom properties

Updated version 1.2.7

The string representation of atoms lists all assigned per-atom properties and shows id, atype and pos values.

[8]:
print(atoms)
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369
      1       1   2.002   2.700   1.037
      2       2   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       2   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870

Alternatively, the atoms object can be copied into a pandas.DataFrame with the Atoms.df() method. This is convenient for viewing and analyzing all per-atom property values at once.

[9]:
df = atoms.df()
df
[9]:
atype pos[0] pos[1] pos[2] stress[0][0] stress[0][1] stress[0][2] stress[1][0] stress[1][1] stress[1][2] stress[2][0] stress[2][1] stress[2][2]
0 1 3.180632 1.021751 1.368982 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 1 2.002387 2.700365 1.036841 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 2 1.986024 2.185413 2.284540 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 1 1.714732 3.876444 3.360637 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 1 1.718764 1.360792 1.461209 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
5 2 0.309280 1.733428 1.953142 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
6 1 2.216314 1.225062 0.249055 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
7 1 1.064939 3.396161 3.682580 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
8 1 2.262611 2.949183 3.729774 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
9 1 2.628185 1.292423 3.870324 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

Note that the values in the DataFrame are copies: calling Atoms.df() doubles the memory usage and changed values are not retained.

[10]:
df.loc[0, 'atype'] = 3
print(df.atype.values)
print(atoms.atype)
[3 1 2 1 1 2 1 1 1 1]
[1 1 2 1 1 2 1 1 1 1]

4. Atoms by index

The Atoms class also allows for the atoms to be get/set using numpy indexing. This is useful for manipulations in how the atoms are listed and all per-atom properties for a given atom are to be retained.

Note: If you want to access/manipulate per-atom properties of certain atoms, it is more efficient to access the properties first (as in Section 3) then apply the slice.

4.1 Getting by index

An atoms object can be sliced using numpy indexing. This returns a new Atoms instance containing only the selected atom(s). Useful for generating subsets.

[11]:
# Get only the atoms with x position greater than 3
upperatoms = atoms[atoms.pos[:, 0] > 3]
print(upperatoms)
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369

4.2 Setting by index

All per-atom values of a subset of Atoms can be set at once using indexing. The value being assigned must be an Atoms instance of compatible size and same per-atom properties as the Atoms instance it is being assigned to.

[12]:
# Copy first atom in atoms to last atom in upperatoms
upperatoms[-1] = atoms[0]
print(upperatoms)
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369
[13]:
# Swap atoms 0 and 1 in atoms.
print(atoms)
print()

atoms[[0, 1]] = atoms[[1, 0]]

print(atoms)
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369
      1       1   2.002   2.700   1.037
      2       2   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       2   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870

per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   2.002   2.700   1.037
      1       1   3.181   1.022   1.369
      2       2   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       2   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870

5. Atoms.extend()

Added version 1.2.8

The Atoms.extend method creates a new Atoms object by copying the current Atoms object and adding new atoms to it. The new atoms can either be copied from another existing Atoms object, or a specified number of ‘empty’ atoms can be added.

Parameters - value (atomman.Atoms or int) An int value will result in the atoms object being extended by that number of atoms, with all per-atom properties having default values (atype = 1, everything else = 0). For an Atoms value, the current atoms list will be extended by the correct number of atoms and all per-atom properties in value will be copied over. Any properties defined in one Atoms object and not the other will be set to default values.

Returns - (atomman.Atoms) A new Atoms object containing all atoms and properties of the current object plus the additional atoms.

Passing Atoms.extend() an integer will add that many empty atoms to the end of the Atoms list.

[14]:
print(atoms.extend(2))
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   2.002   2.700   1.037
      1       1   3.181   1.022   1.369
      2       2   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       2   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870
     10       1   0.000   0.000   0.000
     11       1   0.000   0.000   0.000

Passing Atoms.extend() an Atoms object will combine the two Atoms lists.

[15]:
# Add copies of atoms 0,1,2,3 to the end
print(atoms.extend(atoms[:4]))
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   2.002   2.700   1.037
      1       1   3.181   1.022   1.369
      2       2   1.986   2.185   2.285
      3       1   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       2   0.309   1.733   1.953
      6       1   2.216   1.225   0.249
      7       1   1.065   3.396   3.683
      8       1   2.263   2.949   3.730
      9       1   2.628   1.292   3.870
     10       1   2.002   2.700   1.037
     11       1   3.181   1.022   1.369
     12       2   1.986   2.185   2.285
     13       1   1.715   3.876   3.361

6. Atoms.prop()

The Atoms.prop() method offers a “safe” means of getting and setting values. It is designed with three things in mind:

  1. All get/set actions copy values instead of references.

  2. For consistency with the System.atoms_prop() method.

  3. For backwards compatibility with older atomman versions.

Parameters:

  • key (str, optional) Per-atom property name.

  • index (int, list, slice, optional) Index of atoms.

  • value (any, optional) Property values to assign.

  • a_id (int, optional) Alternate name for index. Left in for backwards compatibility.

With no arguments, prop() returns the list of assigned per-atom properties.

[16]:
print(atoms.prop())
['atype', 'pos', 'stress']

If the value parameter is not given, prop() will return a copy of the value associated with the key, index combination.

[17]:
# key by itself returns the property value
print("atoms.prop('atype') ->", atoms.prop('atype'))
print()

# index by itself returns an Atoms slice
print('atoms.prop(index=slice(1,5)) ->')
print(atoms.prop(index=slice(1,5)))
print()

# key and index returns property value(s) of specific atoms
print("atoms.prop(key='pos', index=0) ->")
print(atoms.prop(key='pos', index=0))
atoms.prop('atype') -> [1 1 2 1 1 2 1 1 1 1]

atoms.prop(index=slice(1,5)) ->
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   3.181   1.022   1.369
      1       2   1.986   2.185   2.285
      2       1   1.715   3.876   3.361
      3       1   1.719   1.361   1.461

atoms.prop(key='pos', index=0) ->
[2.0023867  2.70036484 1.03684118]

Values can be set to Atoms using the value parameter. Any values set are copied to the Atoms instance as opposed to assigned by reference.

[18]:
# Set all values of a given property
print("calling: atoms.prop(key='atype', value=7)")
atoms.prop(key='atype', value=[7])
print("atoms.prop('atype') ->", atoms.prop('atype'))
print()

# Set the value of a specific atom's property
print("calling: atoms.prop(key='atype', index=4, value=1)")
atoms.prop(key='atype', index=4, value=1)
print("atoms.prop('atype') ->", atoms.prop('atype'))
print()
calling: atoms.prop(key='atype', value=7)
atoms.prop('atype') -> [7 7 7 7 7 7 7 7 7 7]

calling: atoms.prop(key='atype', index=4, value=1)
atoms.prop('atype') -> [7 7 7 7 1 7 7 7 7 7]

[19]:
# Copy atom 0 to atom 9
print("calling: atoms.prop(index=9, value=atoms.prop(index=0))")
atoms.prop(index=9, value=atoms.prop(index=0))
print(atoms)
calling: atoms.prop(index=9, value=atoms.prop(index=0))
per-atom properties = ['atype', 'pos', 'stress']
     id   atype  pos[0]  pos[1]  pos[2]
      0       7   2.002   2.700   1.037
      1       7   3.181   1.022   1.369
      2       7   1.986   2.185   2.285
      3       7   1.715   3.876   3.361
      4       1   1.719   1.361   1.461
      5       7   0.309   1.733   1.953
      6       7   2.216   1.225   0.249
      7       7   1.065   3.396   3.683
      8       7   2.263   2.949   3.730
      9       7   2.002   2.700   1.037

Demonstrate safe copy of prop() using only value parameter.

[20]:
# Generate atoms1 with 3 atoms (all with atype=1, pos=[0,0,0])
atoms1 = am.Atoms(natoms=3)
print('atoms1 ->')
print(atoms1)
atoms1 ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   0.000   0.000   0.000
      1       1   0.000   0.000   0.000
      2       1   0.000   0.000   0.000
[21]:
# Directly setting atoms2 = atoms1 makes them point to the same reference
print('setting atoms2 = atoms1')
atoms2 = atoms1

# Changing atoms2 changes atoms1
print('setting: atoms2.atype = 2')
atoms2.atype = 2

print('atoms1 ->')
print(atoms1)
setting atoms2 = atoms1
setting: atoms2.atype = 2
atoms1 ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       2   0.000   0.000   0.000
      1       2   0.000   0.000   0.000
      2       2   0.000   0.000   0.000
[22]:
# Seting atoms3 to atoms1 using prop() copies values *not* reference.
print('setting: atoms3 = am.Atoms(natoms=3)')
atoms3 = am.Atoms(natoms=3)
print('calling: atoms3.prop(value=atoms1)')
atoms3.prop(value=atoms1)
print('atoms3 ->')
print(atoms3)
# Changing atoms3 does not change atoms1
print('setting: atoms3.atype = 3')
atoms3.atype = 3

print('atoms1 ->')
print(atoms1)
print('atoms3 ->')
print(atoms3)
setting: atoms3 = am.Atoms(natoms=3)
calling: atoms3.prop(value=atoms1)
atoms3 ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       2   0.000   0.000   0.000
      1       2   0.000   0.000   0.000
      2       2   0.000   0.000   0.000
setting: atoms3.atype = 3
atoms1 ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       2   0.000   0.000   0.000
      1       2   0.000   0.000   0.000
      2       2   0.000   0.000   0.000
atoms3 ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       3   0.000   0.000   0.000
      1       3   0.000   0.000   0.000
      2       3   0.000   0.000   0.000

7. Atoms.prop_atype()

Added version 1.3.0

prop_atype() is a convenience method allowing for property values to be assigned to the atoms based on the atom’s corresponding atype values.

Parameters

  • key (str) Per-atom property name.

  • value (list, any) Property value(s) to assign. If atype is not given, this should be an object of length Atoms.natypes. Otherwise, should be a single per-atom value.

  • atype (int, optional) A specific atype to assign value to.

[23]:
# Generate atoms with 10 atoms and two atom types
atoms = am.Atoms(atype=[1,1,1,2,2,2,1,1,1,2])
print('atoms ->')
print(atoms)
atoms ->
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   0.000   0.000   0.000
      1       1   0.000   0.000   0.000
      2       1   0.000   0.000   0.000
      3       2   0.000   0.000   0.000
      4       2   0.000   0.000   0.000
      5       2   0.000   0.000   0.000
      6       1   0.000   0.000   0.000
      7       1   0.000   0.000   0.000
      8       1   0.000   0.000   0.000
      9       2   0.000   0.000   0.000
[24]:
# Assign charges to the atoms based on atom type
atoms.prop_atype('charge', [-1, 1])
atoms.df()
[24]:
atype pos[0] pos[1] pos[2] charge
0 1 0.0 0.0 0.0 -1
1 1 0.0 0.0 0.0 -1
2 1 0.0 0.0 0.0 -1
3 2 0.0 0.0 0.0 1
4 2 0.0 0.0 0.0 1
5 2 0.0 0.0 0.0 1
6 1 0.0 0.0 0.0 -1
7 1 0.0 0.0 0.0 -1
8 1 0.0 0.0 0.0 -1
9 2 0.0 0.0 0.0 1
[25]:
# Change the charges for only atoms with atype of 2
atoms.prop_atype('charge', 0, atype=2)
atoms.df()
[25]:
atype pos[0] pos[1] pos[2] charge
0 1 0.0 0.0 0.0 -1
1 1 0.0 0.0 0.0 -1
2 1 0.0 0.0 0.0 -1
3 2 0.0 0.0 0.0 0
4 2 0.0 0.0 0.0 0
5 2 0.0 0.0 0.0 0
6 1 0.0 0.0 0.0 -1
7 1 0.0 0.0 0.0 -1
8 1 0.0 0.0 0.0 -1
9 2 0.0 0.0 0.0 0

8. Atoms.model()

Added version 1.2.7

A JSON/XML equivalent data model representation of the Atoms object can be generated using the model() method.

Parameters

  • prop_name (list, optional) The Atoms properties to include. If neither prop_name nor prop_unit are given, all system properties will be included.

  • unit (list, optional) Lists the units for each prop_name as stored in the table. For a value of None, no conversion will be performed for that property. If neither unit nor prop_units given, pos will be given in Angstroms and all other values will not be converted.

  • prop_unit (dict, optional) Dictionary where the keys are the property keys to include, and the values are units to use. If neither unit nor prop_units given, pos will be given in Angstroms and all other values will not be converted.

Returns

  • (DataModelDict.DataModelDict) A JSON/XML data model for the current Atoms object.

[26]:
model = atoms.model()
print(model.json())
print()
print(model.xml())
{"atoms": {"natoms": 10, "property": [{"name": "atype", "data": {"value": [1, 1, 1, 2, 2, 2, 1, 1, 1, 2]}}, {"name": "pos", "data": {"value": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "shape": [10, 3], "unit": "angstrom"}}, {"name": "charge", "data": {"value": [-1, -1, -1, 0, 0, 0, -1, -1, -1, 0]}}]}}

<?xml version="1.0" encoding="utf-8"?>
<atoms><natoms>10</natoms><property><name>atype</name><data><value>1</value><value>1</value><value>1</value><value>2</value><value>2</value><value>2</value><value>1</value><value>1</value><value>1</value><value>2</value></data></property><property><name>pos</name><data><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><value>0.0</value><shape>10</shape><shape>3</shape><unit>angstrom</unit></data></property><property><name>charge</name><data><value>-1</value><value>-1</value><value>-1</value><value>0</value><value>0</value><value>0</value><value>-1</value><value>-1</value><value>-1</value><value>0</value></data></property></atoms>

Any stored model information can then be reloaded in as a new Atoms object by passing the ‘model’ parameter to the class initializer.

[27]:
print(am.Atoms(model=model))
per-atom properties = ['atype', 'pos', 'charge']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   0.000   0.000   0.000
      1       1   0.000   0.000   0.000
      2       1   0.000   0.000   0.000
      3       2   0.000   0.000   0.000
      4       2   0.000   0.000   0.000
      5       2   0.000   0.000   0.000
      6       1   0.000   0.000   0.000
      7       1   0.000   0.000   0.000
      8       1   0.000   0.000   0.000
      9       2   0.000   0.000   0.000