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