Source code for ccdc.csp.pxrd_match_optimiser

# 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)