From 01373307201d358262df554b6bf9524ac87a13bb Mon Sep 17 00:00:00 2001 From: Clemens Prescher Date: Mon, 29 Jul 2024 15:27:37 +0200 Subject: [PATCH] updated density behavior of sample config --- glassure/configuration.py | 53 +++++++++++++++++++++++++------------ glassure/utility.py | 22 ++++++++++++++- tests/test_configuration.py | 15 ++++++----- tests/test_utility.py | 9 +++++++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/glassure/configuration.py b/glassure/configuration.py index c8e3587..919a4fc 100644 --- a/glassure/configuration.py +++ b/glassure/configuration.py @@ -1,27 +1,42 @@ # -*- coding: utf-8 -*/: from typing import Optional, Literal -from pydantic import BaseModel, Field -from dataclasses import dataclass, field +from pydantic import BaseModel, Field, computed_field -from .utility import Composition, convert_density_to_atoms_per_cubic_angstrom +from .utility import ( + Composition, + convert_density_to_atoms_per_cubic_angstrom, + convert_density_to_grams_per_cubic_centimeter, +) from .pattern import Pattern from .methods import FourierTransformMethod, NormalizationMethod, ExtrapolationMethod class SampleConfig(BaseModel): - composition: Composition = field(default_factory=dict) - density: Optional[float] = field( + composition: Composition = Field(default_factory=dict) + density: Optional[float] = Field( default=None, - ) - atomic_density: Optional[float] = field( - default=None, - ) - - def model_post_init(self, __context): - if self.density is not None: - self.atomic_density = convert_density_to_atoms_per_cubic_angstrom( - self.composition, self.density + description="Density in g/cm^3. Will be automatically updated when the atomic density is set", + ) + + @computed_field( + description="The atomic density in atoms per cubic Angstrom. Will be automatically updated when density is set." + ) + @property + def atomic_density(self) -> Optional[float]: + if self.composition == {}: # empty composition + return None + return convert_density_to_atoms_per_cubic_angstrom( + self.composition, self.density + ) + + @atomic_density.setter + def atomic_density(self, value: Optional[float]): + if self.composition == {}: + self.density = None + else: + self.density = convert_density_to_grams_per_cubic_centimeter( + self.composition, value ) @@ -127,7 +142,8 @@ class TransformConfig(BaseModel): ) extrapolation: ExtrapolationConfig = Field( - default_factory=ExtrapolationConfig, description="Extrapolation configuration model." + default_factory=ExtrapolationConfig, + description="Extrapolation configuration model.", ) use_modification_fcn: bool = Field( @@ -138,8 +154,11 @@ class TransformConfig(BaseModel): description="Whether to apply the Klein-Nishima correction to the Compton scattering of the sample and the" + "container (defined in normalization).", ) - wavelength: Optional[float] = Field(default=None, description="Wavelength in Angstrom. Needs to be set for the " - + "Klein-Nishima correction.") + wavelength: Optional[float] = Field( + default=None, + description="Wavelength in Angstrom. Needs to be set for the " + + "Klein-Nishima correction.", + ) fourier_transform_method: FourierTransformMethod = FourierTransformMethod.FFT diff --git a/glassure/utility.py b/glassure/utility.py index 0fa0125..d4d2e93 100644 --- a/glassure/utility.py +++ b/glassure/utility.py @@ -38,7 +38,7 @@ def parse_str_to_composition(formula: str) -> Composition: """ - Parses a chemical formula string into a dictionary with elements as keys and abundances as relative numbers. + Parses a chemical formula string into a dictionary with elements as keys and abundances as relative numbers. Typical examples are 'SiO2'-> {'Si': 1, 'O': 2} or 'Na2Si2O5' -> {'Na': 2, 'Si': 2, 'O': 5} :param formula: chemical formula string @@ -291,6 +291,26 @@ def convert_density_to_atoms_per_cubic_angstrom( return density / mean_z * 0.602214129 +def convert_density_to_grams_per_cubic_centimeter( + composition: Composition, atomic_density: float +) -> float: + """ + Converts densities given in atoms/A^3 into g/cm3 + + :param composition: dictionary with elements as key and abundances as relative numbers + :param atomic_density: density in atoms/A^3 + :return: density in g/cm^3 + """ + + # get_smallest abundance + norm_elemental_abundances = normalize_composition(composition) + mean_z = 0.0 + for element, concentration in norm_elemental_abundances.items(): + element = re.findall("[A-zA-Z]*", element)[0] + mean_z += concentration * scattering_factors.atomic_weights["AW"][element] + return atomic_density * mean_z / 0.602214129 + + def extrapolate_to_zero_step(pattern: Pattern, y0: float = 0) -> Pattern: """ Extrapolates a pattern to (0, y0) by setting everything below the q_min of the pattern to y0 (default=0) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 16fbb2c..16822ae 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -8,23 +8,24 @@ def test_sample_config(): - c = SampleConfig() - c_dict = vars(c) - assert c_dict == {"composition": {}, "density": None, "atomic_density": None} - c = SampleConfig(composition={"Si": 1, "O": 2}, density=2.2) assert c.atomic_density == approx(0.0662, abs=1e-4) - c_dict = vars(c) + c_dict = c.model_dump() assert c_dict == { "composition": {"Si": 1, "O": 2}, "density": 2.2, "atomic_density": approx(0.0662, abs=1e-4), } - c = SampleConfig(composition={"Si": 1, "O": 2}, atomic_density=0.0662) - assert c.density == None + +def test_sample_density_update(): + s = SampleConfig(composition={"Si": 1, "O": 2}, density=2.2) + atomic_density = s.atomic_density + s.density = 2.4 + + assert s.atomic_density != atomic_density def test_fit_normalization_config(): diff --git a/tests/test_utility.py b/tests/test_utility.py index 69190a1..33e3b76 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -5,6 +5,7 @@ from glassure.utility import ( normalize_composition, convert_density_to_atoms_per_cubic_angstrom, + convert_density_to_grams_per_cubic_centimeter, calculate_f_mean_squared, calculate_f_squared_mean, calculate_incoherent_scattering, @@ -38,6 +39,14 @@ def test_convert_density_to_atoms_per_cubic_angstrom(self): self.assertAlmostEqual(density_au, 0.0662, places=4) + + def test_convert_density_to_grams_per_cubic_centimeter(self): + composition = {"Si": 1, "O": 2} + density_au = convert_density_to_atoms_per_cubic_angstrom(composition, 2.2) + density = convert_density_to_grams_per_cubic_centimeter(composition, density_au) + self.assertAlmostEqual(density, 2.2, places=4) + + def test_calculate_f_mean_squared(self): q = np.linspace(0, 10) composition = {"Si": 1, "O": 2}