Source code for neutronbraggedge.braggedge

"""Bragg edge calculation module for crystalline materials."""

import os
from typing import Any, Literal, TypedDict

from .braggedges_handler.braggedge_calculator import BraggEdgeCalculator
from .material_handler.retrieve_material_metadata import RetrieveMaterialMetadata
from .models import BraggEdgeEntry, BraggEdgeResult
from .utilities import Utilities

CrystalStructure = Literal["BCC", "FCC"]


[docs] class NewMaterialDict(TypedDict): """Type definition for new material dictionary. Attributes ---------- name : str Material name/symbol. lattice : float Lattice constant in Angstroms. crystal_structure : CrystalStructure Crystal structure type ("BCC" or "FCC"). """ name: str lattice: float crystal_structure: CrystalStructure
[docs] class BraggEdge: """Calculate Bragg edges for crystalline materials. This class retrieves material metadata and calculates Bragg edge positions for neutron diffraction analysis. Attributes ---------- material : list[str] List of material names. hkl : dict[str, list[list[int]]] Miller indices for each material. bragg_edges : dict[str, list[float]] Bragg edge wavelengths in Angstroms for each material. d_spacing : dict[str, list[float]] Interplanar spacing in Angstroms for each material. metadata : dict[str, Any] Material metadata including lattice and crystal structure. Examples -------- Calculate Bragg edges for Iron: >>> from neutronbraggedge.braggedge import BraggEdge >>> handler = BraggEdge(material='Fe', number_of_bragg_edges=4) >>> print(handler.bragg_edges['Fe']) [4.0538, 2.8664, 2.3404, 2.0269] Use a custom material: >>> custom = [{'name': 'MyMat', 'lattice': 3.5, 'crystal_structure': 'FCC'}] >>> handler = BraggEdge(new_material=custom) Get Pydantic model result: >>> result = handler.to_result('Fe') >>> print(result.model_dump_json()) """ hkl: dict[str, list[list[int]]] | None = None metadata: dict[str, Any] | None = None bragg_edges: dict[str, list[float]] | None = None d_spacing: dict[str, list[float]] | None = None material: list[str] number_of_bragg_edges: int use_local_metadata_table: bool lattice: dict[str, float] crystal_structure: dict[str, CrystalStructure] _calculator: dict[str, BraggEdgeCalculator]
[docs] def __init__( self, material: str | list[str] | None = None, new_material: list[NewMaterialDict] | None = None, number_of_bragg_edges: int = 10, use_local_metadata_table: bool = True, ) -> None: """Initialize BraggEdge calculator. Parameters ---------- material : str or list[str], optional Material name(s) such as 'Ni', 'Fe'. Either `material` or `new_material` must be provided. new_material : list[NewMaterialDict], optional List of custom material definitions with 'name', 'lattice', and 'crystal_structure' keys. number_of_bragg_edges : int, default 10 Number of Bragg edges to calculate. use_local_metadata_table : bool, default True If True, use local metadata table. If False, fetch from Wikipedia. Raises ------ ValueError If neither `material` nor `new_material` is provided, or if `new_material` has an invalid format. """ if material is None: if new_material is None: raise ValueError("No material or new_material defined!") else: # parse dictionary list_material: list[str] = [] try: for _element in new_material: _name = _element["name"] list_material.append(_name) _lattice_constant = _element["lattice"] _crystal_structure = _element["crystal_structure"] except: raise ValueError("Check the format of the new element array!") material = list_material if type(material) is not list: material = [material] self.material = material self.number_of_bragg_edges = number_of_bragg_edges self.use_local_metadata_table = use_local_metadata_table self._retrieve_metadata(new_material=new_material) self._calculate_hkl() self._calculate_braggedges()
[docs] def to_result(self, material: str) -> BraggEdgeResult: """Convert calculation results to a Pydantic model. Parameters ---------- material : str Material name to get results for. Returns ------- BraggEdgeResult Pydantic model containing all calculation results. Raises ------ KeyError If the specified material was not calculated. Examples -------- >>> handler = BraggEdge(material='Fe', number_of_bragg_edges=4) >>> result = handler.to_result('Fe') >>> print(result.lattice) 2.8664 >>> print(result.model_dump_json(indent=2)) """ if material not in self.material: raise KeyError(f"Material '{material}' not found. Available: {self.material}") entries = [] _hkl = self.hkl[material] _d_spacing = self.d_spacing[material] _bragg_edges = self.bragg_edges[material] for i in range(len(_d_spacing)): entries.append( BraggEdgeEntry( h=_hkl[i][0], k=_hkl[i][1], l=_hkl[i][2], d_spacing=_d_spacing[i], bragg_edge=_bragg_edges[i], ) ) return BraggEdgeResult( material=material, lattice=self.metadata["lattice"][material], crystal_structure=self.metadata["crystal_structure"][material], use_local_table=self.use_local_metadata_table, entries=entries, )
[docs] def get_experimental_lattice_parameter( self, experimental_bragg_edge_values: list[float] | None = None, experimental_bragg_edge_error: list[float] | None = None, ) -> None: """Calculate experimental lattice parameter from Bragg edge values. Note: This method is not yet implemented. Use the Lattice class directly for experimental lattice parameter calculations. Parameters ---------- experimental_bragg_edge_values : list[float], optional Array of experimental Bragg edge values. experimental_bragg_edge_error : list[float], optional Array of errors for the Bragg edge values. Raises ------ ValueError If `experimental_bragg_edge_values` is not provided, or if `experimental_bragg_edge_error` length doesn't match. NotImplementedError This method is not yet implemented. """ if experimental_bragg_edge_values is None: raise ValueError("Please provide an array of bragg edge values") if experimental_bragg_edge_error is not None: if len(experimental_bragg_edge_error) != len(experimental_bragg_edge_values): raise ValueError("Make sure exp. bragg edge value and error have the same size!") raise NotImplementedError( "get_experimental_lattice_parameter is not yet implemented. " "Use the Lattice class directly for experimental lattice calculations." )
def _retrieve_metadata(self, new_material: list[NewMaterialDict] | None = None) -> None: """Retrieve lattice and crystal structure metadata for materials.""" _lattice: dict[str, float] = {} _crystal_structure: dict[str, CrystalStructure] = {} if new_material is None: # retrieve infos from ascii table for _material in self.material: _handler = RetrieveMaterialMetadata(material=_material, use_local_table=self.use_local_metadata_table) _lattice[_material] = _handler.lattice _crystal_structure[_material] = _handler.crystal_structure else: # local infos for _element in new_material: _material = _element["name"] _local_lattice = _element["lattice"] _local_crystal_structure = _element["crystal_structure"] _lattice[_material] = _local_lattice _crystal_structure[_material] = _local_crystal_structure self.lattice = _lattice self.crystal_structure = _crystal_structure self.metadata = {"lattice": self.lattice, "crystal_structure": self.crystal_structure} def _calculate_hkl(self) -> None: """Calculate Miller indices up to number_of_bragg_edges.""" calculator: dict[str, BraggEdgeCalculator] = {} _hkl: dict[str, list[list[int]]] = {} for _material in self.material: _structure_name = self.metadata["crystal_structure"][_material] _lattice = self.metadata["lattice"][_material] _calculator = BraggEdgeCalculator( structure_name=_structure_name, lattice=_lattice, number_of_set=self.number_of_bragg_edges ) _calculator.calculate_hkl() calculator[_material] = _calculator _hkl[_material] = _calculator.hkl self._calculator = calculator self.hkl = _hkl def _calculate_braggedges(self) -> None: """Calculate Bragg edge wavelengths and d-spacing values.""" _d_spacing: dict[str, list[float]] = {} _bragg_edges: dict[str, list[float]] = {} for _material in self.material: _calculator = self._calculator[_material] _calculator.calculate_bragg_edges() _d_spacing[_material] = _calculator.d_spacing _bragg_edges[_material] = _calculator.bragg_edges self.d_spacing = _d_spacing self.bragg_edges = _bragg_edges def __repr__(self) -> str: """Display calculation results via logger.""" nbr_ticks = 45 for _material in self.material: print("=" * nbr_ticks) print(f"Material: {_material}") print(f"Lattice : {self.metadata['lattice'][_material]:.4f}\u212b") print(f"Crystal Structure: {self.metadata['crystal_structure'][_material]}") print(f"Using local metadata Table: {self.use_local_metadata_table}") print("=" * nbr_ticks) print(" h | k | l |\t d (\u212b) |\t BraggEdge") print("-" * nbr_ticks) _hkl = self.hkl[_material] _bragg_edges = self.bragg_edges[_material] _d_spacing = self.d_spacing[_material] for index in range(len(_d_spacing)): h, k, l = _hkl[index] print(f" {h} | {k} | {l} |\t {_d_spacing[index]:.5f} |\t {_bragg_edges[index]:.5f}") print("=" * nbr_ticks) return ""
[docs] def export(self, filename: str | None = None, file_type: str = "csv") -> None: """Export calculation results to a file. Parameters ---------- filename : str, optional Output file path. file_type : str, default "csv" Output format. Only "csv" is currently supported. Raises ------ OSError If no filename is provided. NotImplementedError If an unsupported file type is requested. """ if filename is None: raise OSError for _material in self.material: _filename = self._format_filename(filename, _material) _metadata = self._format_metadata(_material) _data = self._format_data(_material) if file_type == "csv": Utilities.save_csv(filename=_filename, data=_data, metadata=_metadata) else: raise NotImplementedError
def _format_filename(self, filename: str, material: str) -> str: """Format output filename with material suffix.""" _filename, _extension = os.path.splitext(filename) new_filename = os.path.join(_filename + "_" + material + _extension) return new_filename def _format_metadata(self, _material: str) -> list[str]: """Format metadata for file header.""" _metadata: list[str] = [] _metadata.append("Material: %s" % _material) _metadata.append("Lattice : %.4fAngstroms" % self.metadata["lattice"][_material]) _metadata.append("Crystal Structure: %s" % self.metadata["crystal_structure"][_material]) _metadata.append("Using local metadata Table: %s" % self.use_local_metadata_table) _metadata.append("") _metadata.append("h, k, l, d(Angstroms), BraggEdge") return _metadata def _format_data(self, _material: str) -> list[list[int | float]]: """Format calculation data for file output.""" _data: list[list[int | float]] = [] _hkl = self.hkl[_material] _bragg_edges = self.bragg_edges[_material] _d_spacing = self.d_spacing[_material] for index in range(len(_d_spacing)): _data.append([_hkl[index][0], _hkl[index][1], _hkl[index][2], _d_spacing[index], _bragg_edges[index]]) return _data