Materials

Download the full notebook.

02-materials

Materials

PyNE Material objects provide a way of representing, manipulating, and storing materials. A Material object is a collection of nuclides with various mass fractions (though methods for converting to/from atom fractions are present as well). Optionally, a Material object may have an associated mass. By keeping the mass and the composition separate, operations that only affect one attribute may be performed independent of the other. Most of the functionality of the Material class is implemented in a C++, so this interface is very fast and light-weight.

Materials may be initialized in a number of different ways. For example, initializing from dictionaries of compositions are shown below. First import the Material class:

In [1]:
from pyne.material import Material

Now create a low enriched uranium (leu) with a mass of 42:

In [2]:
leu = Material({'U238': 0.96, 'U235': 0.04}, 42)
leu
Out[2]:
pyne.material.Material({922350000: 0.04, 922380000: 0.96}, 42.0, -1.0, -1.0, {})

Create another Material, this one with more components. Notice that the mass is 9 x 1.0 = 9.0:

In [3]:
nucvec = {10010:  1.0, 80160:  1.0, 691690: 1.0, 922350: 1.0,
          922380: 1.0, 942390: 1.0, 942410: 1.0, 952420: 1.0,
          962440: 1.0}
mat = Material(nucvec)
print(mat)
Material:
mass = 9.0
density = -1.0
atoms per molecule = -1.0
-------------------------
H1     0.1111111111111111
O16    0.1111111111111111
Tm169  0.1111111111111111
U235   0.1111111111111111
U238   0.1111111111111111
Pu239  0.1111111111111111
Pu241  0.1111111111111111
Am242  0.1111111111111111
Cm244  0.1111111111111111

Materials may also be initialized from plain text or HDF5 files (see Material.from_text() and Material.from_hdf5()).


Normalization

Upon instantiation, the mass fraction that define a Material are normalized. However, you can always obtain the unnormalized mass vector through Material.mult_by_mass(). Normalization routines to normalize the mass Material.normalize() or the composition Material.norm_comp() are also available. Here we see that our 42 units of LEU consists of 1.68 units of U-235 and 40.32 units of U-238:

In [4]:
leu.mult_by_mass()
Out[4]:
{922350000: 1.68, 922380000: 40.32}

Recall that mat has a mass of 9. Here it is normalized to a mass of 1:

In [5]:
mat.normalize()
mat
Out[5]:
pyne.material.Material({10010000: 0.1111111111111111, 80160000: 0.1111111111111111, 691690000: 0.1111111111111111, 922350000: 0.1111111111111111, 922380000: 0.1111111111111111, 942390000: 0.1111111111111111, 942410000: 0.1111111111111111, 952420000: 0.1111111111111111, 962440000: 0.1111111111111111}, 1.0, -1.0, -1.0, {})
In [6]:
mat.mass
Out[6]:
1.0

Material Arithmetic

Various arithmetic operations between Materials and numeric types are also defined. Adding two Materials together will return a new Material whose values are the weighted union of the two original. Multiplying a Material by 2, however, will simply double the mass of the original Material.

In [7]:
other_mat = mat * 2
other_mat
Out[7]:
pyne.material.Material({10010000: 0.11111111111111108, 80160000: 0.11111111111111108, 691690000: 0.11111111111111108, 922350000: 0.11111111111111108, 922380000: 0.11111111111111108, 942390000: 0.11111111111111108, 942410000: 0.11111111111111108, 952420000: 0.11111111111111108, 962440000: 0.11111111111111108}, 2.0, -1.0, -1.0, {})
In [8]:
other_mat.mass
Out[8]:
2.0
In [9]:
weird_mat = leu + mat * 18
print(weird_mat)
Material:
mass = 60.0
density = -1.0
atoms per molecule = -1.0
-------------------------
H1     0.03333333333333332
O16    0.03333333333333332
Tm169  0.03333333333333332
U235   0.06133333333333332
U238   0.7053333333333334
Pu239  0.03333333333333332
Pu241  0.03333333333333332
Am242  0.03333333333333332
Cm244  0.03333333333333332

Note that there are also ways of mixing Materials by volume using known densities. See the pyne.MultiMaterial class for more information.


Raw Member Access

You may also change the attributes of a material directly without generating a new material instance.

In [10]:
other_mat.mass = 10
other_mat.comp = {10020000: 3, 922350000: 15.0}
print(other_mat)
Material:
mass = 10.0
density = -1.0
atoms per molecule = -1.0
-------------------------
H2     3.0
U235   15.0

Of course when you do this you have to be careful because the composition and mass may now be out of sync. This may always be fixed with normalization.

In [11]:
other_mat.norm_comp()
print(other_mat)
Material:
mass = 10.0
density = -1.0
atoms per molecule = -1.0
-------------------------
H2     0.16666666666666666
U235   0.8333333333333334

Indexing & Slicing

Additionally (and very powerfully!), you may index into either the material or the composition to get, set, or remove sub-materials. Generally speaking, you may only index into the composition by integer-key and only to retrieve the normalized value. Indexing into the material allows the full range of operations and returns the unnormalized mass weight. Moreover, indexing into the material may be performed with integer-keys, string-keys, slices, or sequences of nuclides.

In [12]:
leu.comp[922350000]
Out[12]:
0.04
In [13]:
leu['U235']
Out[13]:
1.68
In [14]:
weird_mat['U':'Am']
Out[14]:
pyne.material.Material({922350000: 0.07359999999999998, 922380000: 0.8464, 942390000: 0.03999999999999998, 942410000: 0.03999999999999998}, 50.0, -1.0, -1.0, {})
In [15]:
other_mat[:920000000] = 42.0
print(other_mat)
Material:
mass = 50.333333333333336
density = -1.0
atoms per molecule = -1.0
-------------------------
H2     0.8344370860927152
U235   0.16556291390728478
In [16]:
del mat[962440, 'TM169', 'Zr90', 80160]
mat[:]
Out[16]:
pyne.material.Material({10010000: 0.16666666666666663, 922350000: 0.16666666666666663, 922380000: 0.16666666666666663, 942390000: 0.16666666666666663, 942410000: 0.16666666666666663, 952420000: 0.16666666666666663}, 0.6666666666666667, -1.0, -1.0, {})

Other methods also exist for obtaining commonly used sub-materials, such as gathering the Uranium or Plutonium vector.

Molecular Weights & Atom Fractions

You may also calculate the molecular weight of a material via the Material.molecular_weight method. This uses the pyne.data.atomic_mass function to look up the atomic mass values of the constituent nuclides.

In [17]:
leu.molecular_mass()
Out[17]:
237.9290363047951

Note that by default, materials are assumed to have one atom per molecule. This is a poor assumption for more complex materials. Take water for example. Without specifying the number of atoms per molecule, the molecular weight calculation will be off by a factor of 3. This can be remedied by passing the correct number to the method. If there is no other valid number of molecules stored on the material, this will set the appropriate attribute on the class.

In [18]:
h2o = Material({'H1': 0.11191487328808077, 'O16': 0.8880851267119192})
h2o.molecular_mass()
Out[18]:
6.003521561386799
In [19]:
h2o.molecular_mass(3.0)
h2o.atoms_per_molecule
Out[19]:
3.0

It is also useful to be able to convert the current mass-weighted material to an atom fraction mapping. This can be easily done via the Material.to_atom_frac() method. Continuing with the water example, if the number of atoms per molecule is properly set then the atom fraction returned is normalized to this amount. Alternatively, if the atoms per molecule are set to its default state on the class, then a truly fractional number of atoms is returned.

In [20]:
h2o.to_atom_frac()
Out[20]:
{10010000: 1.9999999999946356, 80160000: 1.0000000000053646}
In [21]:
h2o.atoms_per_molecule = -1.0
h2o.to_atom_frac()
Out[21]:
{10010000: 0.6666666666648785, 80160000: 0.3333333333351215}

Additionally, you may wish to convert an existing set of atom fractions to a new material stream. This can be done with the Material.from_atom_frac() method, which will clear out the current contents of the material's composition and replace it with the mass-weighted values. Note that when you initialize a material from atom fractions, the sum of all of the atom fractions will be stored as the atoms per molecule on this class. Additionally, if a mass is not already set on the material, the molecular weight will be used.

In [22]:
h2o_atoms = {10010000: 2.0, 'O16': 1.0}
h2o = Material()
h2o.from_atom_frac(h2o_atoms)

print(h2o.comp)
print(h2o.atoms_per_molecule)
print(h2o.mass)
print(h2o.molecular_mass())
{10010000: 0.11191487328888054, 80160000: 0.8880851267111195}
3.0
18.01056468408
18.01056468408

Moreover, other materials may also be used to specify a new material from atom fractions. This is a typical case for reactors where the fuel vector is convolved inside of another chemical form. Below is an example of obtaining the Uranium-Oxide material from Oxygen and low-enriched uranium.

In [23]:
uox = Material()
uox.from_atom_frac({leu: 1.0, 'O16': 2.0})
print(uox)
Material:
mass = 269.9188655439951
density = -1.0
atoms per molecule = 3.0
------------------------
O16    0.11851646299241672
U235   0.03525934148030333
U238   0.84622419552728

NOTE: Materials may be used as keys in a dictionary because they are hashable.

User-defined Metadata

Materials also have an metadata attribute which allows users to store arbitrary custom information about the material. This can include things like units, comments, provenance information, or anything else the user desires. This is implemented as an in-memory JSON object attached to the C++ class. Therefore, what may be stored in the metadata is subject to the same restrictions as JSON itself. The top-level of the metadata should be a dictionary, though this is not explicitly enforced.

In [24]:
leu = Material({922350: 0.05, 922380: 0.95}, 15, metadata={'units': 'kg'})
leu
Out[24]:
pyne.material.Material({922350000: 0.05, 922380000: 0.95}, 15.0, -1.0, -1.0, {"units":"kg"})
In [25]:
print(leu)
Material:
mass = 15.0
density = -1.0
atoms per molecule = -1.0
units = kg
-------------------------
U235   0.05
U238   0.95
In [26]:
leu.metadata
Out[26]:
{"units":"kg"}
In [27]:
m = leu.metadata
m['comments'] = ['Anthony made this material.']
leu.metadata['comments'].append('And then Katy made it better!')
m['id'] = 42
leu.metadata
Out[27]:
{"comments":["Anthony made this material.","And then Katy made it better!"],"id":42,"units":"kg"}
In [28]:
leu.metadata = {'units': 'solar mass'}
leu.metadata
Out[28]:
{"units":"solar mass"}
In [29]:
m
Out[29]:
{"units":"solar mass"}
In [30]:
leu.metadata['units'] = 'not solar masses'
leu.metadata['units']
Out[30]:
'not solar masses'

As you can see from the above, the attrs interface provides a view into the underlying JSON object. This can be manipulated directly or by renaming it to another variable. Additionally, metadata can be replaced with a new object of the appropriate type. Doing so invalidates any previous views into this container.