# This code is Copyright (C) 2024 The Cambridge Crystallographic Data Centre
# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC.
# This code may not be used, reproduced, translated, modified, disassembled or
# copied, except in accordance with a valid licence agreement with CCDC and may
# not be disclosed or redistributed in any form, either in whole or in part, to
# any third party. All copies of this code made in accordance with a valid
# licence agreement as referred to above must contain this copyright notice.
#
# No representations, warranties, or liabilities are expressed or implied in the
# supply of this code by CCDC, its servants or agents, except where such
# exclusion or limitation is prohibited, void or unenforceable under governing
# law.
#
'''
pxrd_match_optimiser.py - an API for optimising crystal
structures to match powder X-ray diffraction (PXRD) patterns.
'''
###########################################################################
from ccdc import crystal
from ccdc import descriptors
from ccdc.utilities import bidirectional_dict, _private_importer
with _private_importer() as pi:
pi.import_ccdc_module('MathsLib')
pi.import_ccdc_module('PackingSimilarityLib')
pi.import_ccdc_module('PXRDMatcherLib')
pi.import_ccdc_module('BFDHMorphologyLib')
[docs]class PXRDMatchOptimiser:
'''
A class for optimising crystal structures to match powder
X-ray diffraction (PXRD) patterns.
'''
[docs] class Settings:
'''Settings for the PXRD match optimiser.'''
def __init__(self) -> None:
self._settings = PXRDMatcherLib.PXRDMatchMinimiserSettings()
@property
def optimise_cell(self) -> bool:
'''Whether to optimise the unit cell.
The defaults value is True.
'''
return self._settings.optimise_cell()
@optimise_cell.setter
def optimise_cell(self, value: bool) -> None:
self._settings.set_optimise_cell(value)
@property
def optimisation_accuracy(self) -> float:
'''
The optimisation accuracy.
Optimisation will terminate when the magnitude of the gradient of the cost function
goes below this fraction of the optimised variable's magnitude.
The default value is 1e-6.
'''
return self._settings.optimisation_accuracy()
@optimisation_accuracy.setter
def optimisation_accuracy(self, value: float) -> None:
self._settings.set_optimisation_accuracy(value)
@property
def width(self) -> float:
'''The width (in degrees) of the base of the triangle weight function used in pattern matching.
The default value is 2.0 degrees. Values within the range 0 to 90 degrees are accepted.
'''
return self._settings.width()
@width.setter
def width(self, value: float) -> None:
self._settings.set_width(value)
@property
def use_ESDs(self) -> bool:
'''Whether to use ESDs in pattern matching.
The default value is False.
'''
return self._settings.use_esds() == PackingSimilarityLib.PXRDPattern.USE_ESDS
@use_ESDs.setter
def use_ESDs(self, value: bool) -> None:
self._settings.set_use_esds(PackingSimilarityLib.PXRDPattern.USE_ESDS if value else PackingSimilarityLib.PXRDPattern.DO_NOT_USE_ESDS)
_optimise_molecule_dict = bidirectional_dict({
"None": PXRDMatcherLib.PXRDMatchMinimiserSettings.MoleculeOptimisationKeepFractionalCentroids,
"Position": PXRDMatcherLib.PXRDMatchMinimiserSettings.MoleculeOptimisationTranslationRotation,
"Geometry": PXRDMatcherLib.PXRDMatchMinimiserSettings.MoleculeOptimisationTranslationRotationTorsion
})
@property
def optimise_molecule(self) -> str:
'''How much to optimise the crystal's molecules.
The options are:
* 'None' - The molecules will retain their internal geometry and position relative to the cell.
* 'Position' - The molecules will retain their internal geometry but their position and orientation within the cell are optimised.
* 'Geometry' - The molecules' internal torsions, position and orientation within the cell are optimised.
The default value is 'None'.
'''
return PXRDMatchOptimiser.Settings._optimise_molecule_dict.inverse_lookup(self._settings.molecule_optimisation())
@optimise_molecule.setter
def optimise_molecule(self, value: str) -> None:
self._settings.set_molecule_optimisation(PXRDMatchOptimiser.Settings._optimise_molecule_dict.prefix_lookup(value))
@property
def verbose(self) -> bool:
'''Whether to output verbose information.
The default value is False.
'''
return self._settings.verbose()
@verbose.setter
def verbose(self, value: bool) -> None:
self._settings.set_verbose(value)
@property
def simulator_settings(self):
'''The settings for the PXRD simulator.
:return: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern.Settings`
'''
return descriptors.CrystalDescriptors.PowderPattern.Settings(_settings=self._settings.simulator_settings())
@property
def reduced_unit_cell(self) -> bool:
'''Whether to use Niggli reduced unit cells to perform the optimisation. Default is false
'''
return self._settings.reduced_unit_cell()
@reduced_unit_cell.setter
def reduced_unit_cell(self, value: bool) -> None:
self._settings.set_reduced_unit_cell(value)
[docs] @staticmethod
def predict_bfdh_preferred_orientation(crystal):
'''
Predict the PXRD preferred orientation using BFDH morphology.
:param crystal: a :class:`ccdc.crystal.Crystal` object
:return: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation` or None
The returned preferred orientation has an optimisable March-Dollase r parameter.
'''
bfdh = BFDHMorphologyLib.BFDHMorphologyCalculator()
morph = bfdh.calculate_morphology(crystal._crystal.cell())
po_predictor = PackingSimilarityLib.MorphologyPreferredOrientationPredictor(morph)
po = po_predictor.predict(crystal._crystal)
md = PackingSimilarityLib.perferred_orientation_function_as_march_dollase(po)
return md if md is None else descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation(_function=md)
[docs] @staticmethod
def predict_free_direction_preferred_orientation(crystal, pxrd_pattern):
'''
Predict the PXRD preferred orientation using free variables for direction and March Dollase r.
:param crystal: a :class:`ccdc.crystal.Crystal` object
:param pxrd_pattern: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern` object
:return: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation`
The returned prediction is not immediately useful as it needs to be optimised.
After optimisation, the preferred orientation should be a better match to the PXRD pattern.
'''
po_predictor = PackingSimilarityLib.FreeVariablesPreferredOrientationPredictor(pxrd_pattern._pattern)
po = po_predictor.predict(crystal._crystal)
fd = PackingSimilarityLib.perferred_orientation_function_as_free_direction_march_dollase(po)
return descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation(_function=fd)
[docs] @staticmethod
def search_preferred_orientation(crystal, pxrd_pattern):
'''
Predict the PXRD preferred orientation by brute force search to best match a PXRD pattern.
:param crystal: a :class:`ccdc.crystal.Crystal` object
:param pxrd_pattern: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern` object
:return: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation`
The returned preferred orientation has an optimisable March-Dollase r parameter.
'''
po_predictor = PXRDMatcherLib.SearchPreferredOrientationPredictor(pxrd_pattern._pattern)
po = po_predictor.predict(crystal._crystal)
md = PackingSimilarityLib.perferred_orientation_function_as_march_dollase(po)
return md if md is None else descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation(_function=md)
preferred_orientation_prediction_modes = ['BFDH', 'FREE', 'SEARCH']
[docs] @staticmethod
def predict_preferred_orientation(crystal, pxrd_pattern, mode='BFDH'):
'''
Predict the preferred orientation using the specified mode.
:param mode: one of
* 'BFDH' - use BFDH morphology to predict a preferred orientation with optimisable March-Dollase r parameter.
* 'FREE' - a fully opimisable preferred orientation, the actual values are only meaningful after optimisation.
* 'SEARCH' - a brute force search to find a preferred orientation to match the PXRD pattern with optimisable March-Dollase r parameter.
:param crystal: a :class:`ccdc.crystal.Crystal` object
:param pxrd_pattern: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern` object
:return: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern.PreferredOrientation` or None
'''
if mode not in PXRDMatchOptimiser.Settings.preferred_orientation_prediction_modes:
raise ValueError(f"expected one of [{PXRDMatchOptimiser.Settings.preferred_orientation_prediction_modes}] - got {mode}")
if mode == 'BFDH':
return PXRDMatchOptimiser.Settings.predict_bfdh_preferred_orientation(crystal)
if mode == 'FREE':
return PXRDMatchOptimiser.Settings.predict_free_direction_preferred_orientation(crystal, pxrd_pattern)
if mode == 'SEARCH':
return PXRDMatchOptimiser.Settings.search_preferred_orientation(crystal, pxrd_pattern)
@property
def optimise_preferred_orientation(self) -> bool:
'''Whether to optimise preferred orientation.
The default value is False.
'''
return self._settings.optimise_preferred_orientation()
@optimise_preferred_orientation.setter
def optimise_preferred_orientation(self, value: bool) -> None:
self._settings.set_optimise_preferred_orientation(value)
[docs] def find_best_two_theta_shift(self, crystal, pxrd_pattern) -> float:
'''Find a two-theta shift that best matches the PXRD pattern.
:param crystal: a :class:`ccdc.crystal.Crystal` object
:param pxrd_pattern: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern` object
:return: the best two-theta shift in degrees to match the pattern
'''
minimiser = PXRDMatcherLib.PXRDMatchMinimiser(crystal._crystal, pxrd_pattern._pattern, self._settings)
return minimiser.find_best_two_theta_shift() * self.simulator_settings.two_theta_step
@property
def two_theta_shift(self) -> float:
'''The two-theta angle to shift the pattern, in degrees.
The default value is 0.
The value will be rounded to a multiple of the two-theta step size.
'''
return self._settings.two_theta_shift_angle().degrees()
@two_theta_shift.setter
def two_theta_shift(self, value: float) -> None:
self._settings.set_two_theta_shift_angle(MathsLib.Angle(value, MathsLib.Angle.DEGREES))
@property
def cell_length_tolerance(self) -> float:
'''The maximum amount by which the cell lengths can be modified, as a fraction of the starting value.
The default value is 0.05.
'''
return self._settings.cell_length_tolerance()
@cell_length_tolerance.setter
def cell_length_tolerance(self, value: float) -> None:
self._settings.set_cell_length_tolerance(value)
@property
def cell_angle_tolerance(self) -> float:
'''The maximum amount by which the cell angles can be modified, in degrees.
The default value is 5 degrees.
'''
return self._settings.cell_angle_tolerance().degrees()
@cell_angle_tolerance.setter
def cell_angle_tolerance(self, value: float) -> None:
self._settings.set_cell_angle_tolerance(MathsLib.Angle(value, MathsLib.Angle.DEGREES))
def __init__(self, crystal_structure: crystal.Crystal, pxrd_pattern: descriptors.CrystalDescriptors.PowderPattern, settings: Settings = None):
'''
Create a PXRDMatchOptimiser object.
:param crystal_structure: a :class:`ccdc.crystal.Crystal` object
:param pxrd_pattern: a :class:`ccdc.descriptors.CrystalDescriptors.PowderPattern` object
:param settings: a :class:`ccdc.csp.pxrd_match_optimiser.PXRDMatchOptimiser.Settings` object
'''
self._crystal_structure = crystal_structure
self._pxrd_pattern = pxrd_pattern
self._settings = settings if settings is not None else PXRDMatchOptimiser.Settings()
def __repr__(self):
return f'PXRDMatchOptimiser({self._crystal_structure}, {self._pxrd_pattern}, {self._settings})'
def __str__(self):
return f'PXRDMatchOptimiser for {self._crystal_structure.identifier}'
@property
def crystal_structure(self):
'''
The crystal structure to be optimised.
:return: a :class:`ccdc.crystal.Crystal` object
'''
return self._crystal_structure
@property
def pxrd_pattern(self):
'''
The PXRD pattern to match.
:return: a :class:`ccdc.descriptors.PXRD` object
'''
return self._pxrd_pattern
@property
def settings(self):
'''
The settings for the PXRD matching.
:return: a :class:`ccdc.descriptors.PXRDMatchOptimiserSettings` object
'''
return self._settings
[docs] def optimise(self):
'''
Create and return an optimised crystal structure that better matches the PXRD pattern.
:return: an optimised :class:`ccdc.crystal.Crystal` object
'''
cry = self._crystal_structure._crystal.clone()
minimiser = PXRDMatcherLib.PXRDMatchMinimiser(cry, self._pxrd_pattern._pattern, self._settings._settings)
minimiser.minimise()
return crystal.Crystal(cry, self._crystal_structure.identifier)