Skip to content
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

"Added enrich function to material-db-material_db_tools" #36

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
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
39 changes: 39 additions & 0 deletions material-db-tools/material_db_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import Dict, Union, Optional
from pyne import material
from pyne.material import Material, MultiMaterial
from pyne.material_library import MaterialLibrary
from pyne import nucname
from decimal import Decimal, getcontext

def make_mat(nucvec, density, citation, molecular_mass = None):
mat = Material(nucvec, density = density, metadata = {'citation' : citation})
Expand Down Expand Up @@ -53,3 +56,39 @@ def mix_by_volume(material_library, vol_fracs, citation, density_factor=1):
)
return mat


def enrich(
material_composition: Dict[Union[int, str], float],
element_to_enrich: Union[int, str],
isotope_enrichments: Dict[Union[int, str], float],
) -> Dict[int, float]:
"""
Enrich a specific element in a material composition by replacing it with its isotopes.

Args:
material_composition (Dict[Union[int, str], float]): The original material composition.
Copy link
Member

Choose a reason for hiding this comment

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

Does this method only work for mass fractions? Or maybe it works for both as long as the fraction in material_composition are the same basis as the fractions in isotope_enrichments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right that they both need to be on the same basis for the current version of the enrich function. With the logic currently in the make_mat_from_atom you'd have to make your function that converts the mass fraction to an atomic fraction.

Copy link
Member

Choose a reason for hiding this comment

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

Might be good to add this to the docstring - that if the material composition is defined as atom fractions, the enrichment must be as well, and vice versa.

Keys can be nuclide IDs (int) or element symbols (str), and values are their fractions (float).
element_to_enrich (Union[int, str]): The ID or symbol of the element to be enriched.
isotope_enrichments (Dict[Union[int, str], float]): The isotopic composition for element_to_enrich.
Keys can be isotope IDs (int) or isotope symbols (str), and values are their enrichment fractions (float).

Returns:
Dict[int, float]: The updated material composition with the enriched element.
Keys are nuclide IDs (int) and values are their fractions (float).
"""
# Convert all keys to PyNE nuclide IDs
material_composition = {
nucname.id(k): Decimal(str(v)) for k, v in material_composition.items()
Copy link
Member

Choose a reason for hiding this comment

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

Decimal is new to me. I think I see some of the benefits, but it probably makes sense to introduce it with a complete overhaul - and PyNE's just going to convert things back to floats.

You also change this back to a float below anyay.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Converting material_composition and isotope_enrichments allows the enrichment function to take either letters or numbers for the isotopes and elements as input. With how Pyne likes to switch between letter and number names for materials, it makes the function a lot more flexible. In regards to returning it as a float, I had left it like that for the pytests. But I'm thinking of rewriting them to all work with decimal and/or using the pytest.approx. That should remove the issues of pytests failing from testing two floats together.

}
isotope_enrichments = {
nucname.id(k): Decimal(str(v)) for k, v in isotope_enrichments.items()
}
Comment on lines +83 to +85
Copy link
Member

Choose a reason for hiding this comment

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

Do you need this step? It seems that it's just converting the key from potentially being a valid string representation to being a canonical nucname ID. The string representation will work below, won't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this provides protection for the cases where the isotope_enrichment is provided with letters such as "Li6"

Copy link
Member

Choose a reason for hiding this comment

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

Given the way you use this data below, I don't think it matters what form is uses. You don't compare it to anything, but just use it to define a composition. PyNE doesn't care if you mix integers and strings for this.

element_to_enrich_id = nucname.id(element_to_enrich)

if element_to_enrich_id in material_composition:
Copy link
Member

Choose a reason for hiding this comment

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

Since you have to loop through the materials above to convert the keys to the nucname ID, perhaps this whole method could be inside that loop.

For each material:
     convert to ID
     if ID is the element_to_enrich_id
         replace with enrichment stuff
    else
        fill in composition

fract = material_composition.pop(element_to_enrich_id)
for nuclide, enrich_frac in isotope_enrichments.items():
material_composition[nuclide] = enrich_frac * fract
Copy link
Member

Choose a reason for hiding this comment

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

One of the lingering concerns I have is about assumptions regarding elemental mass fractions when we change enrichment. We should discuss. I think it may depend on how materials are manufactured and/or specified, and may be outside the scope of our work, but we should probably be clear about our assumptions.

Here is an example of what I mean: I could imagine a material that is specified as being 10% Li by mass, where that really represents an atom ratio that is determiend by the manufacturing process. If we change the Li enrichment substantially, the molar mass of Li changes, and the mass ratio for a given atom ratio changes.

In most cases, this will be a small effect, but we need to be sure we understand exactly what the impact is and how to document it for users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the example you provided, a user could specify their original material which is 10% Li by mass, then convert the material to atomic fraction. Then if they enriched either material using make_mat_from_atom that would allow them to easily maintain their atomic ratio while enriching the element. I ran a test based on your example using Li2O3Ti with 10% mass lithium and enriching it to 0 and 100%. The elemental mass fraction of lithium stayed at 10%. The atomic mass fraction of lithium changed by 3.5% from the 0% to 100% lithium 6 enrichment case, or roughly +-5% relative to the overall atomic mass. We could tell them that the enrichment function does not currently take into consideration changes in density that result from enrichment and that the density provided will be the final material density. The user-specified fractions for either case are what the material's final composition is.


# Convert back to float for consistency with the original function signature
return {k: float(v) for k, v in material_composition.items()}
Comment on lines +80 to +94
Copy link
Member

Choose a reason for hiding this comment

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

I think this will do the same thing with less converting things back and forth, fewer loops, and valid with PyNE's interface.

Suggested change
material_composition = {
nucname.id(k): Decimal(str(v)) for k, v in material_composition.items()
}
isotope_enrichments = {
nucname.id(k): Decimal(str(v)) for k, v in isotope_enrichments.items()
}
element_to_enrich_id = nucname.id(element_to_enrich)
if element_to_enrich_id in material_composition:
fract = material_composition.pop(element_to_enrich_id)
for nuclide, enrich_frac in isotope_enrichments.items():
material_composition[nuclide] = enrich_frac * fract
# Convert back to float for consistency with the original function signature
return {k: float(v) for k, v in material_composition.items()}
enriched_composition = {}
element_to_enrich_id = nucname.id(element_to_enrich)
for element, fract in material_composition.items():
if nucname.id(element) == element_to_enrich_id:
for nuclide, enrich_frac in isotope_enrichments.items():
enriched_composition[nuclide] = enrich_frac * fract
else:
enriched_composition[element] = fract
return enriched_composition