diff --git a/python/tests/test_xray.py b/python/tests/test_xray.py index 079506b..98e4806 100644 --- a/python/tests/test_xray.py +++ b/python/tests/test_xray.py @@ -17,7 +17,9 @@ ionchamber_fluxes, XrayDB) -from xraydb.xray import chantler_data +from xraydb.xray import (chantler_data, formula_to_mass_fracs, + _validate_mass_fracs, mass_fracs_to_molar_fracs, + transmission_sample) def test_atomic_data(): assert atomic_number('zn') == 30 @@ -634,3 +636,72 @@ def test_ionchamber_fluxes(): assert_allclose(ic4.transmitted/ic4.incident, 0.4928, rtol=0.01) assert_allclose(ic4.incident, 2.7615e10, rtol=0.01) + + +def test_formula_to_mass_fracs(): + mf1 = formula_to_mass_fracs('Fe2O3') + desired1 = {'Fe': 0.69943, 'O': 0.30056} + assert_allclose(mf1['Fe'], desired1['Fe'], rtol=0.001) + assert_allclose(mf1['O'], desired1['O'], rtol=0.001) + assert_allclose(sum(mf1.values()), 1.0, rtol=0.001) + + mf2 = formula_to_mass_fracs({'Fe2O3':1, 'FeO':2}) + desired2 = {'Fe': 0.7363, 'O': 0.2637} + assert_allclose(mf2['Fe'], desired2['Fe'], rtol=0.001) + assert_allclose(mf2['O'], desired2['O'], rtol=0.001) + assert_allclose(sum(mf2.values()), 1.0, rtol=0.001) + + +def test_validate_mass_fracs(): + mf = _validate_mass_fracs({'Fe2O3': 0.75, 'FeO': 0.25}) + desired = {'Fe': 0.7189, 'O': 0.2811} + assert_allclose(mf['Fe'], desired['Fe'], rtol=0.001) + assert_allclose(mf['O'], desired['O'], rtol=0.001) + assert_allclose(sum(mf.values()), 1.0, rtol=0.001) + + +def test_mass_fracs_to_molar_fracs(): + """Circular test for consistency formula->mass fracs + and vice versa.""" + formula = {'Fe':1, 'FeO': 3} + mass_fracs = formula_to_mass_fracs(formula) + molar_fracs = mass_fracs_to_molar_fracs(mass_fracs) + assert_allclose(molar_fracs['Fe'], 4 / 7, rtol=0.001) + assert_allclose(molar_fracs['O'], 3 / 7, rtol=0.001) + + +def test_transmission_sample(): + # Validate against Hephaestus result, see PR #16 + result1 = transmission_sample( + sample='Fe2O3', + energy=7162, + absorp_total=1, + area=1, + density=5.24 + ) + assert_allclose(result1.mass_total_mg, 3.506, rtol=0.001) + assert_allclose(result1.absorption_length_um, 6.7, rtol=0.01) + + # Validate against Hephaestus result, see PR #16 + result2 = transmission_sample( + sample='Cu', + energy=9029, + absorp_total=1, + area=1, + density=8.94 + ) + assert_allclose(result2.mass_total_mg, 3.640, rtol=0.001) + assert_allclose(result2.absorption_length_um, 4.1, rtol=0.01) + + # Validate against XAFSmass result, see PR #16 + result3 = transmission_sample( + sample={'Fe': 0.05, 'SiO2': -1}, + energy=7162, + absorp_total=2.6, + area=1.33, + density=2.65 + ) + assert_allclose(result3.mass_components_mg['Fe'], 2.58, rtol=0.03) + assert_allclose(result3.absorbance_steps['Fe'], 0.706, rtol=0.06) + assert_allclose(result3.mass_total_mg, 51.7, rtol=0.02) + assert_allclose(result3.thickness_mm, 0.1466, rtol=0.02) diff --git a/python/xraydb/xray.py b/python/xraydb/xray.py index 686f3ac..138f776 100644 --- a/python/xraydb/xray.py +++ b/python/xraydb/xray.py @@ -22,6 +22,7 @@ TransmissionSample = namedtuple('TransmissionSample', ('energy_eV', 'absorp_total', 'mass_fractions', + 'molar_fractions', 'absorbance_steps', 'area_cm2', 'mass_total_mg', @@ -1197,6 +1198,8 @@ def transmission_sample(sample, energy, absorp_total=2.6, area=1, `mass_fractions` mass fractions of elements + `molar_fractions` molar fractions of elements + `absorbance_steps` absorbance steps of each element in the sample `area (cm^2)` area, if specified @@ -1229,10 +1232,14 @@ def transmission_sample(sample, energy, absorp_total=2.6, area=1, 'Fe': 0.05, 'Si': 0.4440648769202603, 'O': 0.5059351230797396}, + molar_fractions={ + 'Fe': 0.018525564495838743, + 'Si': 0.3271581451680538, + 'O': 0.6543162903361075}, absorbance_steps={ - 'Fe': 0.6692395733146204, - 'Si': 1.297403071978392e-06, - 'O': 3.386553723091669e-07}, + 'Fe': 0.6692395963328747, + 'Si': 1.6477111496690233e-06, + 'O': 4.3017044962086656e-07}, area_cm2=1.33, mass_total_mg=51.05953690489308, mass_components_mg={ @@ -1241,8 +1248,7 @@ def transmission_sample(sample, energy, absorp_total=2.6, area=1, 'O': 25.832813088371587}, density=None, thickness_mm=None, - absorption_length_um=None - ) + absorption_length_um=None) >>> transmission_sample( sample='Fe2O3', @@ -1257,9 +1263,12 @@ def transmission_sample(sample, energy, absorp_total=2.6, area=1, mass_fractions={ 'Fe': 0.6994307614270416, 'O': 0.3005692385729583}, + molar_fractions={ + 'Fe': 0.4, + 'O': 0.6}, absorbance_steps={ - 'Fe': 2.2227981005407176, - 'O': 4.7769571901536886e-08}, + 'Fe': 2.2227981769930585, + 'O': 6.067837661326302e-08}, area_cm2=1.33, mass_total_mg=12.123291571370844, mass_components_mg={ @@ -1316,6 +1325,7 @@ def transmission_sample(sample, energy, absorp_total=2.6, area=1, energy_eV=energy, absorp_total=absorp_total, mass_fractions=sample, + molar_fractions=mass_fracs_to_molar_fracs(sample), absorbance_steps=absorbance_steps, area_cm2=area, mass_total_mg=mass_total, @@ -1361,8 +1371,9 @@ def formula_to_mass_fracs(formula): return mass_fracs -def mass_fracs_to_formula(mass_fracs): - """Calculate molecular formula from a given mass fractions of elements. +def mass_fracs_to_molar_fracs(mass_fracs): + """Calculate molar fractions from a given mass fractions of elements. + Result is normalized to one. Args: mass_fracs (dict): mass fractions of elements @@ -1371,22 +1382,18 @@ def mass_fracs_to_formula(mass_fracs): dict with fields of each element and values of their coefficients Example: - >>> mass_fracs_to_formula({'Fe': 0.7, 'SiO2': -1}) + >>> mass_fracs_to_molar_fracs({'Fe': 0.7, 'SiO2': -1}) { - 'Fe': 0.012534694242994, - 'Si': 0.004993092888171364, - 'O': 0.009986185776342726 + 'Fe': 0.4555755828186302, + 'Si': 0.18147480572712324, + 'O': 0.3629496114542464 } """ mass_fracs = _validate_mass_fracs(mass_fracs) - masses = {} - for el, _ in mass_fracs.items(): - parsed = chemparse(el) - masses[el] = sum([atomic_mass(el) * c for el, c in parsed.items()]) - - coeffs = {k: mass_fracs[k] / masses[k] for k in mass_fracs.keys()} - - return coeffs + molar_fracs = {el: fr / atomic_mass(el) for el, fr in mass_fracs.items()} + total = sum(molar_fracs.values()) + molar_fracs = {el: fr / total for el, fr in molar_fracs.items()} + return molar_fracs def _validate_mass_fracs(mass_fracs): @@ -1401,14 +1408,19 @@ def _validate_mass_fracs(mass_fracs): assert len(unknown) == 1, 'Multiple unknown weight percentages' mass_fracs[unknown[0]] = 1 - sum({k:v for k, v in mass_fracs.items() if k != unknown[0]}.values()) else: - compare = abs(sum([v for v in mass_fracs.values()]) - 1) < 1e-4 + compare = abs(sum(mass_fracs.values()) - 1) < 1e-4 if not compare: raise RuntimeError("Mass fractions do not add up to one.") simplified_mass_fracs = {} for comp, frac in mass_fracs.items(): - parsed = chemparse(comp) - parsed_sum = sum([atomic_mass(el) * c for el, c in parsed.items()]) - for el, c in parsed.items(): - simplified_mass_fracs[el] = atomic_mass(el) * c / parsed_sum * frac + elements = chemparse(comp) + element_masses = {el: atomic_mass(el) * c for el, c in elements.items()} + for el, c in elements.items(): + contribution = element_masses[el] / sum(element_masses.values()) * frac + if el not in simplified_mass_fracs: + simplified_mass_fracs[el] = contribution + else: + simplified_mass_fracs[el] += contribution + assert abs(sum(simplified_mass_fracs.values()) - 1) < 1e-4, "Validation failed, calculated mass fractions do not add up to one." return simplified_mass_fracs