"""Lattice parameter calculation from experimental Bragg edges."""
import ast
import configparser
from typing import Any, Literal
import numpy as np
from numpy.typing import ArrayLike, NDArray
from ..braggedges_handler.braggedge_calculator import BraggEdgeCalculator
from ..config import config_file as config_config_file
from ..models import LatticeStatistics
CrystalStructure = Literal["BCC", "FCC"]
[docs]
class Lattice:
"""Calculate lattice parameters from experimental Bragg edge measurements.
Given experimental Bragg edge values and crystal structure, this class
calculates the lattice parameter for each reflection and provides
statistical analysis of the results.
Attributes
----------
material : str or None
Material name/symbol.
crystal_structure : CrystalStructure
Crystal structure type ("BCC" or "FCC").
bragg_edge_array : NDArray
Array of experimental Bragg edge values.
bragg_edge_error_array : NDArray
Array of errors for the Bragg edge values.
hkl : list[list[int]]
Miller indices for each Bragg edge.
lattice_array : list[float]
Calculated lattice parameters for each reflection.
lattice_error : list[float]
Errors in lattice parameters.
lattice_statistics : dict[str, Any]
Statistical summary (min, max, mean, median, std).
Examples
--------
>>> from neutronbraggedge.lattice_handler.lattice import Lattice
>>> bragg_edges = [4.05, 2.87, 2.34, 2.03]
>>> errors = [0.01, 0.01, 0.01, 0.01]
>>> lattice = Lattice(
... material='Fe',
... crystal_structure='BCC',
... bragg_edge_array=bragg_edges,
... bragg_edge_error_array=errors
... )
>>> print(lattice.lattice_statistics['mean'])
[2.866, 0.007]
"""
space: int = 75
material: str | None
use_local_metadata: bool
bragg_edge_array: NDArray[np.floating]
bragg_edge_error_array: NDArray[np.floating]
_crystal_structure: CrystalStructure
_list_structure: list[str]
hkl: list[list[int]]
hkl_bragg_edge: list[tuple[list[int], float, float]]
lattice_array: list[float]
lattice_error: list[float]
lattice_statistics: dict[str, Any]
[docs]
def __init__(
self,
material: str | None = None,
crystal_structure: CrystalStructure | None = None,
bragg_edge_array: ArrayLike | None = None,
bragg_edge_error_array: ArrayLike | None = None,
use_local_metadata_table: bool = True,
) -> None:
"""Initialize Lattice calculator.
Parameters
----------
material : str, optional
Material name/symbol for reference.
crystal_structure : {"BCC", "FCC"}
Crystal structure type.
bragg_edge_array : array-like
Experimental Bragg edge wavelengths in Angstroms.
bragg_edge_error_array : array-like, optional
Errors for the Bragg edge values. If None, zeros are used.
use_local_metadata_table : bool, default True
Whether to use local metadata table.
Raises
------
ValueError
If crystal_structure is not "BCC" or "FCC".
"""
self.material = material
self._crystal_structure = crystal_structure
self.crystal_structure = crystal_structure # only used to run test
self.use_local_metadata = use_local_metadata_table
self.bragg_edge_array = self._format_array(bragg_edge_array)
self.bragg_edge_error_array = self._format_array(bragg_edge_error_array)
# retrieve hkl
o_bragg_calculator = BraggEdgeCalculator(
structure_name=crystal_structure, lattice=None, number_of_set=len(bragg_edge_array)
)
o_bragg_calculator.calculate_hkl()
self.hkl = o_bragg_calculator.hkl
self._calculate()
@property
def crystal_structure(self) -> CrystalStructure:
"""Get the crystal structure type."""
return self._crystal_structure
@crystal_structure.setter
def crystal_structure(self, structure_name: CrystalStructure | None) -> None:
"""Set and validate the crystal structure type."""
_config_file = config_config_file
config_obj = configparser.ConfigParser()
config_obj.read(_config_file)
self._list_structure = ast.literal_eval(config_obj["DEFAULT"]["list_structure"])
if structure_name not in self._list_structure:
raise ValueError(f"Structure name should be in the list {self._list_structure}")
self._crystal_structure = structure_name
[docs]
def get_statistics(self) -> LatticeStatistics:
"""Get lattice statistics as a Pydantic model.
Returns
-------
LatticeStatistics
Pydantic model containing statistical summary of lattice parameters.
Examples
--------
>>> lattice = Lattice(material='Fe', crystal_structure='BCC',
... bragg_edge_array=[4.05, 2.87],
... bragg_edge_error_array=[0.01, 0.01])
>>> stats = lattice.get_statistics()
>>> print(stats.mean)
2.866
>>> print(stats.model_dump_json(indent=2))
"""
return LatticeStatistics(
min=float(self.lattice_statistics["min"]),
max=float(self.lattice_statistics["max"]),
median=float(self.lattice_statistics["median"]),
mean=float(self.lattice_statistics["mean"][0]),
mean_error=float(self.lattice_statistics["mean"][1]),
std=float(self.lattice_statistics["std"]),
)
def _format_array(self, bragg_edge_array: ArrayLike | None) -> NDArray[np.floating]:
"""Format input array, replacing None values with np.nan."""
_bragg_edge_array_formated: list[float] = []
if bragg_edge_array is None:
sz = len(self.bragg_edge_array)
_bragg_edge_array_formated_arr: NDArray[np.floating] = np.zeros(sz)
return _bragg_edge_array_formated_arr
for _value in bragg_edge_array:
if _value is None:
_value = np.nan
_bragg_edge_array_formated.append(_value)
_bragg_edge_array_formated_arr = np.array(_bragg_edge_array_formated)
return _bragg_edge_array_formated_arr
def _calculate(self) -> None:
"""Calculate lattice parameters step by step."""
self._match_bragg_edge_with_hkl()
self._calculate_lattice_array()
self._calculate_lattice_statistics()
def _match_bragg_edge_with_hkl(self) -> None:
"""Match each Bragg edge with its corresponding Miller indices."""
_bragg_edge_array = self.bragg_edge_array
_bragg_edge_array_error = self.bragg_edge_error_array
zipped = zip(self.hkl, _bragg_edge_array, _bragg_edge_array_error)
self.hkl_bragg_edge = list(zipped)
[docs]
def display_hkl_bragg_edge(self) -> bool:
"""Display the hkl-Bragg edge table.
Returns
-------
bool
Always returns True on successful display.
"""
print("hkl Bragg Edge Table")
print("=" * self.space)
print("hkl \t\t Bragg Edge Value\t Bragg Edge Error \t Lattice")
print("-" * self.space)
_lattice_array = self.lattice_array
for _index, _row in enumerate(self.hkl_bragg_edge):
_key = _row[0]
_value = _row[1]
_error = _row[2]
_lattice = _lattice_array[_index]
if np.isnan(_error):
print(f"{_key!r}\t {_value:.5f} \t\t\t {_error:.5f} \t\t\t {_lattice:.5f}")
else:
print(f"{_key!r}\t {_value:.5f} \t\t {_error:.5f} \t\t {_lattice:.5f}")
print("-" * self.space)
print("")
return True
def _calculate_lattice_array(self) -> None:
"""Calculate lattice parameter for each hkl reflection."""
_hkl_bragg_edge = self.hkl_bragg_edge
_lattice_array: list[float] = []
_lattice_error_array: list[float] = []
for _row in _hkl_bragg_edge:
_hkl = _row[0]
_bragg_edge = _row[1]
_bragg_error = _row[2]
[_lattice, _lattice_error] = self._calculate_lattice_coefficient(
hkl=_hkl, bragg_edge=_bragg_edge, bragg_error=_bragg_error
)
_lattice_array.append(_lattice)
_lattice_error_array.append(_lattice_error)
self.lattice_array = _lattice_array
self.lattice_error = _lattice_error_array
def _calculate_lattice_coefficient(
self,
hkl: list[int] | None = None,
bragg_edge: float | None = None,
bragg_error: float | None = None,
) -> list[float]:
"""Calculate lattice parameter from a single hkl and Bragg edge."""
_h, _k, _l = hkl
_term1 = np.sqrt(_h**2 + _k**2 + _l**2)
_term2 = bragg_edge / 2.0
_lattice = _term2 * _term1
_lattice_error = _term1 * bragg_error / 2.0
return [_lattice, _lattice_error]
def _calculate_lattice_statistics(self) -> None:
"""Calculate statistics of the lattice parameter array."""
_lattice_statistics: dict[str, Any] = {}
# min
_min = np.nanmin(self.lattice_array)
_lattice_statistics["min"] = _min
# max
_max = np.nanmax(self.lattice_array)
_lattice_statistics["max"] = _max
# median
_median = np.nanmedian(self.lattice_array)
_lattice_statistics["median"] = _median
# mean
_mean = np.nanmean(self.lattice_array)
_error = self._calculate_mean_error(self.lattice_error)
_lattice_statistics["mean"] = [_mean, _error]
# std
_std = np.nanstd(self.lattice_array)
_lattice_statistics["std"] = _std
self.lattice_statistics = _lattice_statistics
def _calculate_mean_error(self, lattice_error: list[float]) -> float:
"""Calculate propagated error for the mean lattice parameter."""
_mean_error = 0.0
_index = 0
_sum = 0.0
for _error in lattice_error:
if not np.isnan(_error):
_step1 = _error * _error
_sum += _step1
_index += 1
_mean_error = np.sqrt(_sum) / _index
return _mean_error
[docs]
def display_lattice_statistics(self) -> None:
"""Display lattice statistics via logger."""
_lattice_statistics = self.lattice_statistics
print("Lattice Statistics")
print("=" * self.space)
print(f"min: {_lattice_statistics['min']:.5f}")
print(f"max: {_lattice_statistics['max']:.5f}")
print(f"median: {_lattice_statistics['median']:.5f}")
print(f"mean: {_lattice_statistics['mean'][0]:.5f} +/- {_lattice_statistics['mean'][1]:.5f}")
print(f"std: {_lattice_statistics['std']:.5f}")
print("-" * self.space)
print("")
[docs]
def display_recap(self) -> None:
"""Display a summary of inputs and calculation results."""
print(" -- Recap --")
print("=" * self.space)
print(f"Material: {self.material!r}")
print(f"Crystal Structure: {self._crystal_structure!r}")
print("-" * self.space)
print("")
self.display_hkl_bragg_edge()
self.display_lattice_statistics()