Using Material

Material is the data type for working with elemental compositions in NeXL.

julia> using NeXLCore

There are various mechanisms to construct a Material.

If you know the chemical formula, these mechanisms parse the formula.

julia> m = mat"Ca5(PO4)3F" # Fluorapatite
Ca5(PO4)3F[O=0.3807,F=0.0377,P=0.1843,Ca=0.3974]

julia> m = mat"Ca₅(PO₄)₃F" # Equivalent
Ca₅(PO₄)₃F[O=0.3807,F=0.0377,P=0.1843,Ca=0.3974]

julia> m = parse(Material, "Ca5(PO4)3F", name = "Fluorapatite", density = 3.2)  # Equivalent with density of 3.2 g/cm³
Fluorapatite[O=0.3807,F=0.0377,P=0.1843,Ca=0.3974,3.20 g/cm³]

julia> m = atomicfraction("Fluorapatite", n"Ca"=>5, n"P"=>3, n"O"=>12, n"F"=>1, density=3.2)
Fluorapatite[O=0.3807,F=0.0377,P=0.1843,Ca=0.3974,3.20 g/cm³]

You can construct pure elements with nominal densities.

julia> pure(n"Fe")
Pure Fe[Fe=1.0000,7.87 g/cm³]

julia> pure(n"Ca")
Pure Ca[Ca=1.0000,1.55 g/cm³]

These mechanisms necessarily produce an analytical total of unity (all the mass fractions sum to one.)

Other methods are not so constrained.

julia> m = material("Fluorapatite", n"Ca"=>0.1843, n"P"=>0.3974, n"O"=>0.3807, n"F"=>0.0377, density=3.2)
Fluorapatite[O=0.3807,F=0.0377,P=0.3974,Ca=0.1843,3.20 g/cm³]

julia> analyticaltotal(m)
1.0001

It is possible with many of these methods to specify custom atomic weights.

julia> u1=parse(Material, "U3O8", atomicweights = Dict(n"U"=>235.0))
U3O8[O=0.1537,U=0.8463]

julia> u2=parse(Material, "U3O8", atomicweights = Dict(n"U"=>238.0))
U3O8[O=0.1520,U=0.8480]

It is possible to inspect a Material's atomic weight (either custom like U or default like O).

julia> a(n"U", u1)
235.0

julia> a(n"U", u2)
238.0

julia> a(n"O", u1) == a(n"O", u2)
true

julia> a(n"O", u1) == a(n"O")
true

The parse function can also do elemental mass-fraction math using the + and * operators.

julia> mat"0.6*Al+0.4*O"
0⋅6⋅Al+0⋅4⋅O[O=0.4000,Al=0.6000]

We can sum materials within the parse function...

julia> mat"0.6*Fe2O3+0.4*FeO2"
0⋅6⋅Fe2O3+0⋅4⋅FeO2[O=0.3260,Fe=0.6740]

We can even use a lookup function to map names of materials to compositions. Using this mechanism, it is possible to look up materials in a database by name or using some other custom mechanism.

julia> function mylibrary(name)
           m = get(Dict("K411"=>NeXLCore.srm470_k411, "K412"=>NeXLCore.srm470_k412), name, missing)
           ismissing(m) ? missing : massfraction(m)
       end
mylibrary (generic function with 1 method)

julia> m=parse(Material,"0.59*K411+0.39*K412", lookup=mylibrary)
0⋅59⋅K411+0⋅39⋅K412[O=0.4167,Mg=0.0977,Al=0.0191,Si=0.2324,Ca=0.1077,Fe=0.0964]

Accessing the composition as mass-fraction is easy.

julia> m[n"Fe"]
0.0964 ± 0.0011

julia> m[26]
0.0964 ± 0.0011

julia> m[92] # Elements that are not present return zero
0.0e+00 ± 0.0e+00

julia> nonneg(m, n"Fe")  # Sets negative mass fractions to zero
0.09637164504859125

julia> nonneg(material("Cruft", n"Fe"=>-0.001, n"Al"=>0.999),n"Fe")
0.0

julia> [ el=>m[el] for el in keys(m) ]
6-element Vector{Pair{Element, UncertainValue}}:
 Element(Aluminium) => 0.0191 ± 0.0004
   Element(Calcium) => 0.1077 ± 0.0010
      Element(Iron) => 0.0964 ± 0.0011
 Element(Magnesium) => 0.0977 ± 0.0009
    Element(Oxygen) => 0.4167 ± 0.0011
   Element(Silicon) => 0.2324 ± 0.0007

julia> massfraction(m)
Dict{Element, AbstractFloat} with 6 entries:
  Element(Aluminium) => 0.0191 ± 0.0004
  Element(Calcium)   => 0.1077 ± 0.0010
  Element(Iron)      => 0.0964 ± 0.0011
  Element(Magnesium) => 0.0977 ± 0.0009
  Element(Oxygen)    => 0.4167 ± 0.0011
  Element(Silicon)   => 0.2324 ± 0.0007

There are various ways to produce the compositional data in a normalized form.

julia> normalizedmassfraction(m) # as a Dict(Element, T)
Dict{Element, UncertainValue} with 6 entries:
  Element(Aluminium) => 0.0197 ± 0.0004
  Element(Calcium)   => 0.1111 ± 0.0009
  Element(Iron)      => 0.0993 ± 0.0010
  Element(Magnesium) => 0.1007 ± 0.0008
  Element(Oxygen)    => 0.4296 ± 0.0004
  Element(Silicon)   => 0.2396 ± 0.0005

julia> normalized(m,n"Fe") # as a number
0.0993 ± 0.0011

julia> asnormalized(m) # as a Material
N[0⋅59⋅K411+0⋅39⋅K412,1.0][O=0.4296,Mg=0.1007,Al=0.0197,Si=0.2396,Ca=0.1111,Fe=0.0993]

The equivalent in atomic-fraction is

julia> atomicfraction(m)
Dict{Element, UncertainValue} with 6 entries:
  Element(Aluminium) => 0.0163 ± 0.0004
  Element(Calcium)   => 0.0618 ± 0.0006
  Element(Iron)      => 0.0397 ± 0.0005
  Element(Magnesium) => 0.0924 ± 0.0008
  Element(Oxygen)    => 0.5993 ± 0.0016
  Element(Silicon)   => 0.1904 ± 0.0005

Defining and extracting default or custom material properties is easy

julia> m = parse(Material, "NaAlSi3O8", density=2.6, name="Albite")
Albite[O=0.4881,Na=0.0877,Al=0.1029,Si=0.3213,2.60 g/cm³]

julia> m[:MyProperty]=12.23
12.23

julia> m[:MyOtherProperty]="This or that"
"This or that"

julia> m[:Density]
2.6

julia> m[:MyProperty]
12.23

julia> m[:MyOtherProperty]
"This or that"

How many atoms of an element or all elements per gram of material?

julia> atoms_per_g(m, n"Al")
2.2966133865065546e21

julia> atoms_per_g(n"Al")
2.231948613447806e22

Combining the density with the composition we get

julia> m[:Density]=3.0 # g/cm³
3.0

julia> atoms_per_cm³(m, n"Al")
6.889840159519664e21

julia> atoms_per_cm³(m)
8.956792207375563e22

You will notice that when appropriate the mass-fractions and atomic-fractions can has associated uncertainties. Typically, the mass-fractions in a Material are represented by a Float64. However, it is possible to use UncertainValue from NeXLUncertainties.

julia> material("Stuff",n"Al" => uv(0.0163,0.0004), n"Ca" => uv(0.0618,0.0006), n"Fe"=>uv(0.0397,0.0005), n"Mg"=>uv(0.0924,0.0008),n"O"=>uv(0.5993,0.0016))
Stuff[O=0.5993,Mg=0.0924,Al=0.0163,Ca=0.0618,Fe=0.0397]

julia> parse(Material, "(0.0163±0.0004)*Al+(0.0618±0.0006)*Ca+(0.0397±0.0005)*Fe+(0.0924±0.0008)*Mg+(0.5993±0.0016)*O")
(0⋅0163±0⋅0004)⋅Al+(0⋅0618±0⋅0006)⋅Ca+(0⋅0397±0⋅0005)⋅Fe+(0⋅0924±0⋅0008)⋅Mg+(0⋅5993±0⋅0016)⋅O[O=0.5993,Mg=0.0924,Al=0.0163,Ca=0.0618,Fe=0.0397]

To summarize the Material we can convert it to a DataFrame.

julia> using DataFrames

julia> asa(DataFrame, m)
4×7 DataFrame
 Row │ Material  Element  Z      A        C(z)       Norm[C(z)]  A(z)
     │ String    String   Int64  Float64  Float64    Float64     Float64
─────┼─────────────────────────────────────────────────────────────────────
   1 │ Albite    O            8  15.999   0.488112    0.488112   0.615385
   2 │ Albite    Na          11  22.9898  0.0876742   0.0876742  0.0769231
   3 │ Albite    Al          13  26.9815  0.102897    0.102897   0.0769231
   4 │ Albite    Si          14  28.085   0.321316    0.321316   0.230769

Or we can summarize a Material[] in a DataFrame

julia> asa(DataFrame, [ NeXLCore.srm470_k411, NeXLCore.srm470_k412])
2×8 DataFrame
 Row │ Material      O          Mg         Al         Si         Ca         Fe         Total
     │ String        Abstract…  Abstract…  Abstract…  Abstract…  Abstract…  Abstract…  Abstract…
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ SRM-470 K411   0.423686  0.0884662  0.0         0.253818   0.110564  0.112166   9.887000e-01 ± 0.0e+00
   2 │ SRM-470 K412   0.427576  0.116568   0.0490621   0.211983   0.108991  0.0774201  9.916000e-01 ± 0.0e+00

We can also compare materials in a DataFrame

julia> compare(NeXLCore.srm470_k411,NeXLCore.srm470_k412)
6×11 DataFrame
 Row │ Material 1    Material 2    Elm     C₁(z)            C₂(z)      ΔC           ΔC/C         A₁(z)      A₂(z)      ΔA           ΔA/A
     │ String        String        String  Uncertai…        Float64    Float64      Float64      Float64    Float64    Float64      Float64
─────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ SRM-470 K411  SRM-470 K412  Ca      0.1090 ± 0.0014  0.110564   -0.00157234  -0.0142211   0.0604414  0.0628022  -0.00236083  -0.0375915
   2 │ SRM-470 K411  SRM-470 K412  Mg      0.1166 ± 0.0012  0.0884662   0.0281018    0.241076    0.106595   0.0828619   0.023733     0.222647
   3 │ SRM-470 K411  SRM-470 K412  O       0.4276 ± 0.0018  0.423686    0.0038898    0.00909733  0.593982   0.602871   -0.00888897  -0.0147444
   4 │ SRM-470 K411  SRM-470 K412  Al      0.0491 ± 0.0011  0.0         0.0490621    1.0         0.040414   0.0         0.040414     1.0
   5 │ SRM-470 K411  SRM-470 K412  Fe      0.0774 ± 0.0016  0.112166   -0.0347457   -0.309771    0.030812   0.0457243  -0.0149123   -0.326135
   6 │ SRM-470 K411  SRM-470 K412  Si      0.2120 ± 0.0009  0.253818   -0.0418356   -0.164825    0.167756   0.205741   -0.0379849   -0.184625

It is possible to do math using the + and * operators ith Material data items.

julia> m1, m2 = mat"FeO2", mat"Al2O3"
(FeO2[O=0.3643,Fe=0.6357], Al2O3[O=0.4707,Al=0.5293])

julia> m3 = 0.9*m1 + 0.1*m2
0.9⋅FeO2+0.1⋅Al2O3[O=0.3749,Al=0.0529,Fe=0.5722]

julia> isapprox(m3, mat"0.9*FeO2+0.1*Al2O3")
true

There are various different ways to compute the mean atomic number.

julia> z(m3)
16.602113041602944

julia> z(NeXLCore.NaiveZ, m3), z(NeXLCore.AtomicFraction, m3), z(NeXLCore.ElectronFraction, m3)
(18.563572946574325, 13.449627818584222, 18.26890156121255)

julia> z(NeXLCore.ElasticFraction, m3, 10.0e3), z(NeXLCore.Donovan2002, m3)
(19.82831737587615, 16.602113041602944)

Material data items are used throughout the NeXL libraries. For example:

julia> mac(m3, n"O K-L3")
2800.893061698401