Source code for mckit_nuclides.abundance
"""Methods to change nuclide abundance in compositions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import polars as pl
from mckit_nuclides.elements import ELEMENTS_TABLE_PL
from mckit_nuclides.nuclides import NUCLIDES_TABLE_PL
if TYPE_CHECKING:
from collections.abc import Generator, Iterable
MOLAR_MASS_TABLE = (
ELEMENTS_TABLE_PL.select("atomic_number", "molar_mass")
.with_columns(pl.lit(0, dtype=pl.UInt16).alias("mass_number"))
.select("atomic_number", "mass_number", "molar_mass")
.vstack(NUCLIDES_TABLE_PL.select("atomic_number", "mass_number", "molar_mass"))
.sort("atomic_number", "mass_number")
)
"""The table contains molar masses for nuclides with specified and not specified mass numbers."""
[docs]
def convert_to_atomic_fraction(
composition: pl.DataFrame, fraction_column: str = "fraction"
) -> pl.DataFrame:
"""Change fractions by mass to fractions by atoms.
Args:
composition: DataFrame with columns atomic_number, mass_number
fraction_column: name of column presenting fraction
Returns:
DataFrame: df with modified column "fraction"
"""
composition_columns = composition.columns
converted = (
composition.cast(dtypes={"atomic_number": pl.UInt8, "mass_number": pl.UInt16})
.join(MOLAR_MASS_TABLE, on=["atomic_number", "mass_number"])
.with_columns((pl.col(fraction_column) / pl.col("molar_mass")).alias(fraction_column))
)
return normalize_column(converted, fraction_column).select(composition_columns)
[docs]
def normalize_column(table: pl.DataFrame, column: str = "fraction") -> pl.DataFrame:
"""Normalize the values in a column to have sum() == 1.0 over the column.
Args:
table: ... to normalize
column: ... over this column
Returns:
Result of normalization
"""
total_fractions = table.select(column).sum().item()
return table.with_columns(pl.col(column) / total_fractions)
[docs]
def expand_df_natural_presence(
composition: pl.DataFrame,
fraction_column: str = "fraction",
) -> pl.DataFrame:
"""Expand 'natural' presence in composition presented as a DataFrame.
Args:
composition: table with columns atomic_number, mass_number (may be 0), fraction
fraction_column: exact 'fraction' column name
Returns:
Expanded composition as a DataFrame.
"""
composition_columns = composition.columns
having_mass_numbers = composition.filter(pl.col("mass_number").ne(0))
not_having_mass_number = composition.filter(pl.col("mass_number").eq(0))
expanded = (
not_having_mass_number.join(
NUCLIDES_TABLE_PL.select("atomic_number", "mass_number", "isotopic_composition").filter(
pl.col("isotopic_composition").gt(0)
),
on="atomic_number",
)
.with_columns(
(pl.col(fraction_column) * pl.col("isotopic_composition")).alias(fraction_column),
mass_number=pl.col(
"mass_number_right"
), # replace 0 with mass_number from nuclides table
)
.select(composition_columns)
)
return (
having_mass_numbers.vstack(expanded)
.group_by("atomic_number", "mass_number")
.agg(pl.col(fraction_column).sum())
.sort("atomic_number", "mass_number")
)
[docs]
def expand_natural_presence(
zaf: Iterable[tuple[int, int, float]],
) -> Generator[tuple[int, int, float]]:
"""Convert sequence of nuclide-fraction specification with natural presence.
Substitute a sequence of nuclides when mass number is specified as 0.
This means natural presence.
Args:
zaf: sequence of atomic number, mass number and fraction
Yields:
atomic number, mass_number, and corrected atomic fraction
"""
for z, a, f in zaf:
if a != 0:
yield z, a, f
else:
isotopic_compositions = NUCLIDES_TABLE_PL.filter(
pl.col("atomic_number").eq(z) & pl.col("isotopic_composition").gt(0.0)
).select("mass_number", "isotopic_composition")
for _a, _ic in isotopic_compositions.iter_rows():
yield z, _a, f * _ic