Skip to content

Commit

Permalink
Redesign of final configuration before processing calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
CPrescher committed Jul 29, 2024
1 parent 5a87808 commit c82f479
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 166 deletions.
87 changes: 71 additions & 16 deletions docs/api.ipynb

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion docs/apidoc/glassure.configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ glassure.configuration module
=============================

This module contains all configurations classes necessary for the glassure package.
The main entrypoint is the :class:`glassure.configuration.Input` pydantic model containing all necessary information for the analysis.
The main entrypoints are the :class:`glassure.configuration.CalculationConfig` and :class: `glassure.configuration.DataModel`
pydantic models containing all necessary information for the analysis. These two configurations can then be used analysis
parameters for the *process* function in the :mod:`glassure.calc` module to perform the analysis. The output of the calculation
will be a :class:`glassure.configuration.Result` object containing the results of the analysis.
The models are then further split into submodels for better readability and maintainability.

.. automodule:: glassure.configuration
Expand Down
74 changes: 58 additions & 16 deletions glassure/calc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import numpy as np
from pydantic import ValidationError

from .configuration import Input, Result, FitNormalization, IntNormalization
from .configuration import (
Result,
FitNormalization,
IntNormalization,
SampleConfig,
DataConfig,
CalculationConfig,
Composition,
)
from .pattern import Pattern
from .methods import ExtrapolationMethod
from .normalization import normalize, normalize_fit
Expand All @@ -19,22 +27,56 @@
)


def process_input(input: Input) -> Pattern:
def create_process_configs(
data: Pattern,
composition: Composition,
density: float,
bkg: Pattern = None,
bkg_scaling: float = 1,
) -> tuple[DataConfig, CalculationConfig]:
"""
Helper function to create a starting glassure input configuration.
Automatically sets the q_min and q_max values to the first and last
x-value of the data pattern - thus, the whole pattern gets transformed,
when using this configuration.
These two inputs can then be used with the *process* function in the calc module
to calculate the structure factor S(q), the pair distribution function F(r) and
the pair correlation function g(r).
:param data: The data pattern.
:param composition: The composition of the sample.
:param density: The density of the sample in g/cm^3.
:param bkg: The background pattern. None if no background is present.
:param bkg_scaling: The scaling factor for the background pattern.
:return: DataConfig, CalculationConfig
"""
sample_config = SampleConfig(composition=composition, density=density)
calculation_config = CalculationConfig(sample=sample_config)
calculation_config.transform.q_min = data.x[0]
calculation_config.transform.q_max = data.x[-1]

data_config = DataConfig(data=data, bkg=bkg, bkg_scaling=bkg_scaling)
return data_config, calculation_config


def process(data_config: DataConfig, calculation_config: CalculationConfig) -> Pattern:
"""
Process the input configuration and return the result.
"""
validate_input(input)
validate_input(data_config, calculation_config)

# create some shortcuts
config = input.config
config = calculation_config
transform = config.transform
composition = config.sample.composition

# subtract background
if input.bkg is not None:
sample = input.data - input.bkg * input.bkg_scaling
if data_config.bkg is not None:
sample = data_config.data - data_config.bkg * data_config.bkg_scaling
else:
sample = input.data
sample = data_config.data

# limit the pattern
sample = sample.limit(transform.q_min, transform.q_max)
Expand Down Expand Up @@ -92,7 +134,7 @@ def process_input(input: Input) -> Pattern:

n, norm = normalize(
sample_pattern=sample,
atomic_density=input.config.sample.atomic_density,
atomic_density=config.sample.atomic_density,
f_squared_mean=f_squared_mean,
f_mean_squared=f_mean_squared,
incoherent_scattering=norm_inc,
Expand All @@ -112,7 +154,7 @@ def process_input(input: Input) -> Pattern:
s0 = config.transform.extrapolation.s0
else:
s0 = calculate_s0(composition)

extrapolation = transform.extrapolation
match extrapolation.method:
case ExtrapolationMethod.STEP:
Expand Down Expand Up @@ -168,11 +210,11 @@ def process_input(input: Input) -> Pattern:

gr = calculate_gr(
fr,
atomic_density=input.config.sample.atomic_density,
atomic_density=config.sample.atomic_density,
)

res = Result(
input=input,
calculation_config=config,
sq=sq,
fr=fr,
gr=gr,
Expand All @@ -181,17 +223,17 @@ def process_input(input: Input) -> Pattern:
return res


def validate_input(input: Input):
def validate_input(data_config: DataConfig, calculation_config: CalculationConfig):
"""
Validate the input configuration.
"""
if input.data is None or not isinstance(input.data, Pattern):
if data_config.data is None or not isinstance(data_config.data, Pattern):
raise ValueError("Input data must be a Pattern object.")
if input.bkg is not None and not isinstance(input.bkg, Pattern):
if data_config.bkg is not None and not isinstance(data_config.bkg, Pattern):
raise ValueError("Background data must be a Pattern object.")

if not input.config.sample.composition: # empty composition dict
if not calculation_config.sample.composition: # empty composition dict
raise ValueError("Composition must be set.")

if not input.config.sample.atomic_density:
if not calculation_config.sample.atomic_density:
raise ValueError("Atomic density must be set.")
91 changes: 43 additions & 48 deletions glassure/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ class TransformConfig(BaseModel):
description="Step size for the r values in Angstrom for the calculated pair distribution function g(r).",
)

normalization: FitNormalization | IntNormalization = field(
default_factory=FitNormalization
normalization: FitNormalization | IntNormalization = Field(
default_factory=FitNormalization,
description="Normalization configuration model. Possible values are :class:`FitNormalization` or "
+ ":class:`IntNormalization`.",
)

extrapolation: ExtrapolationConfig = field(default_factory=ExtrapolationConfig)
Expand All @@ -133,9 +135,27 @@ class TransformConfig(BaseModel):
fourier_transform_method: FourierTransformMethod = FourierTransformMethod.FFT


class Config(BaseModel):
"""Main configuration model for the glassure data processing. Does not contain any data, but only the information
how to process the dataset."""
class CalculationConfig(BaseModel):
"""Main calculation configuration model for the glassure data processing.
Does not contain any data, but only the information how to process the dataset.
To reuse the calculation config for a different calculation with some parameters changed, it is advised to use the
model_copy(deep=True) method of the config.
This will create a deep copy of the configuration object and not
overwrite parameters of the original one. (see https://docs.pydantic.dev/latest/concepts/serialization/#model_copy
for more information).
For example:
```
config = CalculationConfig()
config.sample.composition = {"Si": Si, "O": 2}
config_copy = config.model_copy(deep=True)
config_copy.sample.composition = {"Ge": 1, "O": 2}
```
"""

sample: SampleConfig = Field(
default_factory=SampleConfig,
Expand All @@ -152,50 +172,25 @@ class Config(BaseModel):
)


class Input(BaseModel):
"""Main input configuration for the glassure data processing. contains data and configuration."""
class DataConfig(BaseModel):
"""Configuration for the collected data, containing the data pattern, the background pattern and the bkg scaling
parameter."""

data: Optional[Pattern] = None
bkg: Optional[Pattern] = None
bkg_scaling: float = 1.0
config: Config = Config()
data: Pattern = Field(description="The data pattern.")
bkg: Optional[Pattern] = Field(default=None, description="The background pattern.")
bkg_scaling: float = Field(
default=1.0, description="The scaling factor for the background pattern."
)


class Result(BaseModel):
input: Input
sq: Optional[Pattern] = None
fr: Optional[Pattern] = None
gr: Optional[Pattern] = None


def create_input(
data: Pattern,
composition: Composition,
density: float,
bkg: Pattern = None,
bkg_scaling: float = 1,
) -> Input:
"""
Helper function to create a starting glassure input configuration.
Automatically sets the q_min and q_max values to the first and last
x-value of the data pattern - thus, the whole pattern gets transformed,
when using this configuration.
:param data: The data pattern.
:param composition: The composition of the sample.
:param density: The density of the sample in g/cm^3.
:param bkg: The background pattern. None if no background is present.
:param bkg_scaling: The scaling factor for the background pattern.
:return: The input configuration.
"""
sample_config = SampleConfig(composition=composition, density=density)
input_config = Input(
data=data,
bkg=bkg,
bkg_scaling=bkg_scaling,
config=Config(sample=sample_config),
)
input_config.config.transform.q_min = data.x[0]
input_config.config.transform.q_max = data.x[-1]
return input_config
calculation_config: CalculationConfig = Field(
description="The configuration used for the calculation."
)
sq: Optional[Pattern] = Field(
default=None, description="The calculated structure factor S(q)."
)
fr: Optional[Pattern] = Field(
default=None, description="The calculated pair distribution function F(r)."
)
gr: Optional[Pattern] = Field(default=None, description="The calculated g(r).")
Loading

0 comments on commit c82f479

Please sign in to comment.