Skip to content

compute mu #278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions src/diffpy/utils/tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib.metadata
import json
import warnings
from copy import copy
from pathlib import Path

Expand Down Expand Up @@ -210,29 +211,45 @@ def get_package_info(package_names, metadata=None):
return metadata


def compute_mu_using_xraydb(sample_composition, energy, density=None, packing_fraction=1):
def compute_mu_using_xraydb(sample_composition, energy, sample_mass_density=None, packing_fraction=None):
"""Compute the attenuation coefficient (mu) using the XrayDB database.

Computes mu based on the sample composition and energy.
User can provide a measured density or an estimated packing fraction.
Specifying the density is recommended, though not required for some pure or standard materials.
User should provide a sample mass density or a packing fraction.
If neither density nor packing fraction is specified, or if both are specified, a ValueError will be raised.
Reference: https://xraypy.github.io/XrayDB/python.html#xraydb.material_mu.

Parameters
----------
sample_composition : str
The chemical formula or the name of the material.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the name, just chemical formula.

energy : float
The energy in keV.
density : float, optional, Default is None
The energy of the incident x-rays in keV.
sample_mass_density : float, optional, Default is None
The mass density of the packed powder/sample in gr/cm^3.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

g/cm*3, not gr/cm^3

packing_fraction : float, optional, Default is 1
packing_fraction : float, optional, Default is None
The fraction of sample in the capillary (between 0 and 1).

Returns
-------
mu : float
The attenuation coefficient mu in mm^{-1}.
"""
mu = material_mu(sample_composition, energy * 1000, density=density, kind="total") * packing_fraction / 10
if (sample_mass_density is None and packing_fraction is None) or (
sample_mass_density is not None and packing_fraction is not None
):
raise ValueError(
"You must specify either sample_mass_density or packing_fraction, but not both. "
"Please rerun specifying only one."
)
if sample_mass_density is not None:
mu = material_mu(sample_composition, energy * 1000, density=sample_mass_density, kind="total") / 10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it more readable, don't hardcode numbers but rather variables with obvious names.

else:
warnings.warn(
"Warning: Density is set to None if a packing fraction is specified, "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't need the word warning here. Warnings.warn handles that.

"which may cause errors for some materials. "
"We recommend specifying sample mass density for now. "
"Auto-density calculation is coming soon."
Copy link
Contributor Author

@yucongalicechen yucongalicechen Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a warning message for now. I set density to None if user specifies packing fraction. We can probably replace this with density=get_density_from_cloud(sample_composition). There're some instructions here on how to estimate the density from given chemical formula: https://11bm.xray.aps.anl.gov/absorb/absorb.php. I can try to look into this more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's easy if we know the structure, but I am pretty sure MP has material density as an attribute so a simple API call will give it. We will have to add users adding their API key to the global config.....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Density is just number of atoms/unit cell volume which we know from composition and any CIF file we find, then converted to mass density with the formula, then multiplied by packing fraction

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code could be pretty reusable so I expect it will end up in diffpy.utils in the end (but in a future release)

)
mu = material_mu(sample_composition, energy * 1000, density=None, kind="total") * packing_fraction / 10
return mu
82 changes: 42 additions & 40 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,59 +171,61 @@ def test_get_package_info(monkeypatch, inputs, expected):
"inputs, expected_mu",
[
# Test whether the function returns the correct mu
( # C1: No density or packing fraction (only for known material), expect to get mu from database
( # C1: Composition, energy, and mass density provided, expect to get mu based on mass density
# 1. Fully dense mass density
{"sample_composition": "quartz", "energy": 10, "sample_mass_density": 2.65},
5.0368,
),
( # 2. Measured mass density
{
"sample_composition": "H2O",
"energy": 10,
"sample_composition": "ZrO2",
"energy": 17.445,
"sample_mass_density": 1.009,
},
0.5330,
1.2522,
),
( # C2: Packing fraction (=0.5) provided only (only for known material)
( # C2: Composition, energy, and packing fraction provided, expect to get mu based on packing fraction
# Reuse pattern from C1.1 here
{
"sample_composition": "H2O",
"sample_composition": "quartz",
"energy": 10,
"packing_fraction": 0.5,
},
0.2665,
2.5184,
),
( # C3: Density provided only, expect to compute mu based on it
# 1. Known material
],
)
def test_compute_mu_using_xraydb(inputs, expected_mu):
actual_mu = compute_mu_using_xraydb(**inputs)
assert actual_mu == pytest.approx(expected_mu, rel=1e-6, abs=1e-4)


@pytest.mark.parametrize(
"inputs",
[
# Test when the function raises ValueError
# C1: Both mass density and packing fraction are provided
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To compactify we want to write this as follows....also, put what you expect, e.g.,

        # Test when the function has invalid inputs
        ( # C1: Both mass density and packing fraction are provided, expect ValueError exception
            {
                "sample_composition": "quartz",
                "energy": 10,
                "sample_mass_density": 2.65,
                "packing_fraction": 1,
            }
        ),

(
{
"sample_composition": "H2O",
"sample_composition": "quartz",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"quartz" is not a omposition, so we don't want this in the tests. It doesn't matter for the test, but let's not confuse readers. Please also fix this below. You can use SiO2 instead.

Also, please can you move the # C2 down a line inside the paren as I tried to illustrate before. thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha. Will fix that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops there're conflicts - willl fix it

"energy": 10,
"density": 0.987,
},
0.5330,
"sample_mass_density": 2.65,
"packing_fraction": 1,
}
),
( # 2. Unknown material
{
"sample_composition": "ZrO2",
"energy": 17,
"density": 1.009,
},
1.252,
),
( # C4: Both density and packing fraction are provided, expect to compute mu based on both
# 1. Known material
# C2: None of mass density or packing fraction are provided
(
{
"sample_composition": "H2O",
"sample_composition": "quartz",
"energy": 10,
"density": 0.997,
"packing_fraction": 0.5,
},
0.2665,
),
( # 2. Unknown material
{
"sample_composition": "ZrO2",
"energy": 17,
"density": 1.009,
"packing_fraction": 0.5,
},
0.626,
}
),
],
)
def test_compute_mu_using_xraydb(inputs, expected_mu):
actual_mu = compute_mu_using_xraydb(**inputs)
assert actual_mu == pytest.approx(expected_mu, rel=0.01, abs=0.1)
def test_compute_mu_using_xraydb_bad(inputs):
with pytest.raises(
ValueError,
match="You must specify either sample_mass_density or packing_fraction, but not both. "
"Please rerun specifying only one.",
):
compute_mu_using_xraydb(**inputs)
Loading