Simulated X-ray powder diffraction patterns from nomad bulk systems
There is a growing request to be able to display the simulated XRD patterns from NOMAD systems. In Python, this could be done with several packages including ase
, pymatgen
or xrayutilities
. ase
felt a slower solution when given realistic values, and xrayutilities
would be an additional dependency that we are aiming to avoid.
For reference: in a random PC calculating this from a MOF structure (AgC38N4H28.cif) took several seconds and for a more normal symmetrized structure like Calcite.cif 0.2s.
There are several options for this, but upon conversations with @mscheidg and @himanel1, the ideal case would be to do it on the fly.
- One option would be to do this directly on JS, but I am not aware of anything existing that does this on the fly.
- Python and serve it to the GUI with an API endpoint. This could be done maybe in
nomad/app/v1/routers/systems.py
, usingpymatgen
or other packages... some very quick draft for calculating this from a structure file or atoms in any NOMAD path (not tested).
class DiffractionPattern(BaseModel):
two_theta: List[float]
q_values: List[float]
intensities: List[float]
d_spacing: List[float]
async def extract_atoms_from_path(entry_id: str, path: str, user: User) -> NOMADAtoms: # extracted from Lauri's system endpoint
for prefix in ['#/']:
if path.startswith(prefix):
path = path[len(prefix) :]
query_list: List[Union[str, int]] = []
paths = [x for x in path.split('/') if x != '']
i = 0
while i < len(paths):
query_list.append(paths[i])
try:
query_list.append(int(paths[i + 1]))
i += 1
except (IndexError, ValueError):
pass
i += 1
value = {'atoms': '*'}
if 'topology' in path:
value['atoms_ref'] = 'include-resolved'
value['indices'] = '*'
required = query_list_to_dict(query_list, value)
required['resolve-inplace'] = True
query = {'entry_id': entry_id}
try:
archive = answer_entry_archive_request(query, required=required, user=user)[
'data'
]['archive']
except Exception as e:
raise HTTPException(
status_code=404, detail='Archive data not found or access denied.'
)
try:
result_dict = deep_get(
archive, *[0 if isinstance(x, int) else x for x in query_list]
)
atoms_data = result_dict.get('atoms', result_dict.get('atoms_ref'))
if atoms_data is None:
raise HTTPException(
status_code=404, detail='Atoms data not found in the archive.'
)
return NOMADAtoms.m_from_dict(atoms_data)
except Exception as e:
raise HTTPException(
status_code=404, detail='Failed to extract atoms data from the archive.'
)
from fastapi import HTTPException, UploadFile, File, Query
from pydantic import BaseModel
from typing import List
import numpy as np
from pymatgen.io.ase import AseAtomsAdaptor
class DiffractionPattern(BaseModel):
two_theta: List[float]
q_values: List[float]
intensities: List[float]
d_spacing: List[float]
@router.post('/calculate_diffraction_pattern', response_model=DiffractionPattern)
async def calculate_diffraction_pattern_from_file(
structure_file: UploadFile = File(...),
wavelength: float = Query(
default=1.54056, # Default to Cu Kα radiation
description='The wavelength of the X-ray source in Ångstroms.',
),
):
try:
structure_data = await structure_file.read()
structure = Structure.from_str(
structure_data.decode('utf-8'), fmt=structure_file.filename.split('.')[-1]
)
xrd_calculator = XRDCalculator(wavelength=wavelength)
pattern = xrd_calculator.get_pattern(structure)
# Explicit conversion of numpy.float64 to Python float for JSON serialization
q_values = [
float(value)
for value in (4 * np.pi * np.sin(np.radians(pattern.x / 2)) / wavelength)
]
two_theta = [
float(value) for value in pattern.x
] # Convert each element to float
intensities = [
float(value) for value in pattern.y
] # Convert each element to float
d_spacing = [
float(value) for value in pattern.d_hkls
] # Convert each element to float
return DiffractionPattern(
two_theta=two_theta,
q_values=q_values,
intensities=intensities,
d_spacing=d_spacing,
)
except Exception as e:
raise HTTPException(
status_code=400, detail=f'Error calculating diffraction pattern: {str(e)}'
)
finally:
await structure_file.close()
@router.get(
'/calculate_diffraction_pattern_from_path', response_model=DiffractionPattern
)
async def calculate_diffraction_pattern_from_path(
entry_id: str,
path: str,
wavelength: float = Query(default=1.54056),
user: User = Depends(create_user_dependency(signature_token_auth_allowed=True)),
):
atoms = await extract_atoms_from_path(entry_id, path, user)
ase_atoms = ase_atoms_from_nomad_atoms(
atoms
)
structure = AseAtomsAdaptor.get_structure(ase_atoms)
xrd_calculator = XRDCalculator(wavelength=wavelength)
pattern = xrd_calculator.get_pattern(structure)
return DiffractionPattern(
two_theta=[float(value) for value in pattern.x.tolist()],
q_values=[
float(value)
for value in (4 * np.pi * np.sin(np.radians(pattern.x / 2)) / wavelength)
],
intensities=[float(value) for value in pattern.y.tolist()],
d_spacing=[float(value) for value in pattern.d_hkls.tolist()],
)