Skip to content

Commit

Permalink
Changed configuration objects into pydantic objects and added proper …
Browse files Browse the repository at this point in the history
…documentation for the configuration parameters
  • Loading branch information
CPrescher committed Jul 23, 2024
1 parent 833c2e4 commit aaf7b5b
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 53 deletions.
4 changes: 4 additions & 0 deletions docs/apidoc/glassure.configuration.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,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 models are then further split into submodels for better readability and maintainability.

.. automodule:: glassure.configuration
:members:
:undoc-members:
Expand Down
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'sphinx.ext.doctest',
'sphinx.ext.mathjax',
'nbsphinx',
'sphinxcontrib.autodoc_pydantic',
]

source_suffix = ['.rst', '.md']
Expand All @@ -35,3 +36,5 @@

html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

nbsphinx_allow_errors = True
155 changes: 113 additions & 42 deletions glassure/configuration.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,124 @@
# -*- coding: utf-8 -*/:

from typing import Optional, Literal
from pydantic import BaseModel
from pydantic import BaseModel, Field
from dataclasses import dataclass, field

from .utility import Composition, convert_density_to_atoms_per_cubic_angstrom
from .pattern import Pattern
from .methods import FourierTransformMethod, NormalizationMethod, ExtrapolationMethod


@dataclass
class SampleConfig:
class SampleConfig(BaseModel):
composition: Composition = field(default_factory=dict)
density: Optional[float] = None
atomic_density: Optional[float] = None
density: Optional[float] = field(
default=None,
)
atomic_density: Optional[float] = field(
default=None,
)

def __post_init__(self):
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
)


@dataclass
class FitNormalization:
TYPE: Literal["fit"] = "fit"
q_cutoff: float = 3.0
method: str = "squared"
multiple_scattering: bool = False
incoherent_scattering: bool = True
container_scattering: Optional[Composition] = None
class FitNormalization(BaseModel):
TYPE: Literal["fit"] = Field(default="fit", description="Normalization type")
q_cutoff: float = Field(
default=3.0,
description="Cutoff q in 1/A for the normalization. Only above this value the normalization is performed.",
)
method: str = Field(
default="squared",
description='How to scale the values in respect to q during fitting. "linear" or "squared" are possible.',
)
multiple_scattering: bool = Field(
default=False,
description="Whether to consider multiple scattering - if true, the multiple scattering is approximated by a constant value.",
)
incoherent_scattering: bool = Field(
default=True,
description="Whether to subtract the incoherent scattering during the normalization.",
)
container_scattering: Optional[Composition] = Field(
default=None,
description="""Composition of the container material in the experiment. Only the incoherent scattering of the
container is considered. The container scattering is subtracted from the total scattering and the amount is
fitted by just muliplying it with a constant value.""",
)


@dataclass
class IntNormalization:
TYPE: Literal["integral"] = "integral"
attenuation_factor: float = 0.001
incoherent_scattering: bool = True
class IntNormalization(BaseModel):
TYPE: Literal["integral"] = Field(
default="integral", description="Normalization type"
)
attenuation_factor: float = Field(
default=0.001, description="Attenuation factor for the normalization"
)
incoherent_scattering: bool = Field(
default=True,
description="Whether to subtract the incoherent scattering during the normalization",
)


@dataclass
class OptimizeConfig:
r_cutoff: float = 1.4
iterations: int = 5
use_modification_fcn: bool = False
class OptimizeConfig(BaseModel):
r_cutoff: float = Field(
default=1.4,
description="Cutoff r for the Kaplow optimization scheme. Should be below the first peak in g(r).",
)
iterations: int = Field(
default=5, description="Number of iterations for the Kaplow optimization."
)
use_modification_fcn: bool = Field(
default=False,
description="Whether to use the Lorch modification function during the optimization procedure. "
+ "This can be different from the transform configuration.",
)


@dataclass
class ExtrapolationConfig:
method: ExtrapolationMethod = ExtrapolationMethod.STEP
s0: Optional[float] = field(default=None)
overlap: float = 0.2
replace: bool = False
class ExtrapolationConfig(BaseModel):
method: ExtrapolationMethod = Field(
default=ExtrapolationMethod.STEP,
description="Method for the extrapolation of the structure factor S(q) from q_min to zero.",
)
s0: Optional[float] = Field(
default=None,
description="Target value at S(0) for the extrapolation to. If is None, the theorethical value is used.",
)
overlap: float = Field(
default=0.2,
description="Overlap in q-space [1/A] for the extrapolation. E.g. the fitting range.",
)
replace: bool = Field(
default=False,
description="Whether to replace the original S(q) data in the overlap region with the extrapolated values.",
)


@dataclass
class TransformConfig:
q_min: float = 0.0
q_max: float = 10.0
class TransformConfig(BaseModel):
q_min: float = Field(
default=0.0,
description="Minimum q in 1/Angstrom from the data. Below it will be extended to zero.",
)
q_max: float = Field(
default=10.0, description="Maximum q in 1/Angstrom from the data."
)

r_min: float = 0.0
r_max: float = 10.0
r_step: float = 0.01
r_min: float = Field(
default=0.0,
description="Minimum r in Angstrom for the calculated pair distribution function g(r).",
)
r_max: float = Field(
default=10.0,
description="Maximum r in Angstrom for the calculated pair distribution function g(r).",
)
r_step: float = Field(
default=0.01,
description="Step size for the r values in Angstrom for the calculated pair distribution function g(r).",
)

normalization: FitNormalization | IntNormalization = field(
default_factory=FitNormalization
Expand All @@ -76,14 +133,28 @@ class TransformConfig:
fourier_transform_method: FourierTransformMethod = FourierTransformMethod.FFT


@dataclass
class Config:
sample: SampleConfig = field(default_factory=SampleConfig)
transform: TransformConfig = field(default_factory=TransformConfig)
optimize: Optional[OptimizeConfig] = None
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."""

sample: SampleConfig = Field(
default_factory=SampleConfig,
description="Sample configuration model, containing the composition and density of the material.",
)
transform: TransformConfig = Field(
default_factory=TransformConfig,
description="""Transform configuration model, containing the normalization, transform and
extrapolation settings.""",
)
optimize: Optional[OptimizeConfig] = Field(
default=None,
description="Optimization configuration model. If None, no optimization is performed",
)


class Input(BaseModel):
"""Main input configuration for the glassure data processing. contains data and configuration."""

data: Optional[Pattern] = None
bkg: Optional[Pattern] = None
bkg_scaling: float = 1.0
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ scipy = [
lmfit = "^1.2.0"
pandas = "^2.1.0"
pydantic = "^2.7.1"
autodoc-pydantic = "^2.2.0"

[tool.poetry.group.dev.dependencies]
mock = "^5.0.1"
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os

unittest_data_path = os.path.join(os.path.dirname(__file__), 'data')
unittest_data_path = os.path.join(os.path.dirname(__file__), "data")


def data_path(filename):
Expand Down
7 changes: 3 additions & 4 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from pytest import approx
import json
from dataclasses import asdict

import numpy as np

Expand Down Expand Up @@ -68,8 +66,9 @@ def test_fit_normalization_config():

def test_calculation_config():
c = Config()
c_dict = asdict(c)
test = json.dumps(c_dict)
c_json = c.model_dump_json()
assert type(c.model_dump()) == dict
assert type(c_json) == str


def test_input_config():
Expand Down
12 changes: 6 additions & 6 deletions tests/test_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,26 @@ def test_from_dict():
assert pattern1.name == pattern2.name


class TestModel(BaseModel):
class DummyModel(BaseModel):
x: PydanticNpArray


def test_pydantic_nparray_with_array_input():
input_array = np.linspace(0, 10, 1000)
t = TestModel(x=input_array)
t = DummyModel(x=input_array)
json = t.model_dump()
t = TestModel(**json)
t = DummyModel(**json)
assert np.array_equal(t.x, input_array)

def test_pydantic_nparray_with_list_input():
input_array = np.array([1, 2, 3])
t = TestModel(x=input_array.tolist())
t = DummyModel(x=input_array.tolist())
assert np.array_equal(t.x, input_array)
json = t.model_dump()
t = TestModel(**json)
t = DummyModel(**json)
assert np.array_equal(t.x, input_array)

def test_pydantic_nparray_from_json():
input = {"x": [1, 2, 3]}
t = TestModel(**input)
t = DummyModel(**input)
assert np.array_equal(t.x, np.array([1, 2, 3]))

0 comments on commit aaf7b5b

Please sign in to comment.