Source code for ccdc.csp.crystal_optimiser

# This code is Copyright (C) 2025 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.
#
'''
crystal_optimiser.py -   an API for crystal structure optimisation.

The main class is :class:`ccdc.csp.crystal_optimiser.CrystalOptimiser`

Simplest operation is:

.. code-block:: python

    >>> from ccdc import io
    >>> from ccdc.csp.crystal_optimiser import CrystalOptimiser
    >>> csd = io.EntryReader('csd')
    >>> crystal = csd.entry('HXACAN').crystal
    >>> optimiser = CrystalOptimiser()
    >>> optimisation_results = optimiser.optimise(crystal)
    >>> optimised_crystal = optimisation_results.optimised_crystal
    >>> final_score = optimisation_results.final_score

CrystalOptimiser can also be used to directly score a crystal structure based on the current settings:

.. code-block:: python

    >>> score = optimiser.score(crystal)

'''
###########################################################################


import os

from ccdc.crystal import Crystal
from ccdc.utilities import Decorator

from ccdc.utilities import _private_importer
with _private_importer() as pi:
    pi.import_ccdc_module('CrystalOptimiserLib')


[docs] class CrystalOptimiser(object):
[docs] class Settings(object): '''Settings pertaining to the optimiser.''' def __init__(self, _settings=None): if _settings is None: _settings = CrystalOptimiserLib.CrystalOptimiserSettings() self._settings = _settings self._settings.set_optimiser_file_path(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templater')) @property def normalise_hydrogens(self): '''If hydrogens are to be normalised before optimisation.''' return self._settings.normalise_hydrogens() @normalise_hydrogens.setter @Decorator.value_true_or_false def normalise_hydrogens(self, value): self._settings.set_normalise_hydrogens(value) @property def force_field(self): '''The force field choices for optimisation, including: "CLP", "CSD-OPCS16", "Dreiding II", "Momany", "Gavezzotti".''' return self._settings.force_field() @force_field.setter def force_field(self, value): ff_types = ("CLP", "CSD-OPCS16", "Dreiding II", "Momany", "Gavezzotti") if value not in ff_types: raise ValueError("force field must be one of these values: %s - received %s" % (", ".join(ff_types), str(value))) self._settings.set_force_field(value) @property def optimise_cell(self): '''If unit cell parameters are allowed to change during optimisation.''' return self._settings.optimise_cell() @optimise_cell.setter @Decorator.value_true_or_false def optimise_cell(self, value): self._settings.set_optimise_cell(value) @property def cell_limit(self): '''The maximum % volume change allowed for the unit cell.''' return self._settings.cell_limit() @cell_limit.setter def cell_limit(self, value): if type(value) is not type(10) or value >= 100 or value <= 0: raise ValueError( "cell volume (%%) change limit must be an integer between (0, 100), received %s" % str(value)) self._settings.set_cell_limit(value) @property def optimise_molecule(self): '''The choices for molecule optimisation: "Position" or "Geometry".''' return self._settings.molecule_optimisation() @optimise_molecule.setter def optimise_molecule(self, value): molecule_optimisation_types = ["Position", "Geometry"] if value not in molecule_optimisation_types: raise ValueError( "Molecule optimisation must be one of these values: %s - received %s" % (", ".join(molecule_optimisation_types), str(value))) self._settings.set_molecule_optimisation(value) @property def convergence_tolerance(self): '''The convergence tolerance for optimisation.''' return self._settings.crystal_convergence() @convergence_tolerance.setter def convergence_tolerance(self, value): if type(value) is not type(0.5) or value >= 1.0 or value <= 0.0: raise ValueError( "Convergence tolerance must be a number between (0.0, 1.0), received %s" % str(value)) self._settings.set_crystal_convergence(value) @property def limiting_radius(self): '''The limiting radius for optimisation.''' return self._settings.limiting_radius() @limiting_radius.setter def limiting_radius(self, value): if type(value) is not type(0.5) or value >= 100.00 or value <= 0.0: raise ValueError( "Limiting Radius (Angstrom) must be a number between (0.0, 100.0), received %s" % str(value)) self._settings.set_limiting_radius(value) @property def working_dir(self): '''The directory to save the optimised structure file and log file.''' return self._settings.job_directory() @working_dir.setter def working_dir(self, dir): if dir != '' and not os.path.isdir(dir): raise ValueError( "working_dir must be an existing dir - received %s" % str(dir)) self._settings.set_job_directory(dir) @property def output_format(self): '''The file format for the output optimised crystal, one of 'cif' or 'mol2'.''' return self._settings.output_format() @output_format.setter def output_format(self, value): formats = ('cif', 'mol2') if value not in formats: raise ValueError("format must be one of %s, received %s" % (", ".join(formats), str(value))) self._settings.set_output_format(value)
[docs] class Results: '''Holds the results of a CrystalOptimiser calculation.''' def __init__(self, results, crystal, original_crystal, optimised_crystal, optimised_name, starting_score, final_score, n_steps): self._optimised_name = optimised_name self._original_crystal = original_crystal self._optimised_crystal = optimised_crystal self._starting_score = starting_score self._final_score = final_score self._n_steps = n_steps self._crystal = crystal self._results = results @property def optimised_name(self): '''The name assigned to the optimised structure.''' return self._optimised_name @property def original_crystal(self): '''The original crystal structure used for the optimisation.''' return self._original_crystal @property def optimised_crystal(self): '''The optimised crystal structure.''' return self._optimised_crystal @property def starting_score(self): '''The score of the starting crystal structure.''' return self._starting_score @property def final_score(self): '''The score of the optimised crystal structure.''' return self._final_score @property def n_steps(self): '''The number of steps taken during optimisation.''' return self._n_steps
def __init__(self, settings=None): if settings is None: settings = CrystalOptimiser.Settings() self.settings = settings self.optimiser = CrystalOptimiserLib.CrystalOptimiser()
[docs] def minimiser_settings(self, crystal): ''' Return the minimiser settings for the optimiser.''' self.optimiser.prepare_optimisation(crystal._csv) self.optimiser.create_optimiser() self.optimiser.prepare_optimiser_settings() return self.optimiser.minimiser_settings()
[docs] def optimise(self, crystal): ''' Optimise the crystal based on the current settings. :return: A :class:`ccdc.csp.crystal_optimiser.CrystalOptimiser.Results` instance. ''' self.settings._settings.set_input_name(crystal.identifier) self.optimiser.set_settings(self.settings._settings) results = self.optimiser.optimise_synchronous(crystal._csv) optimised_name = self.settings._settings.optimised_name() original_crystal = Crystal(self.optimiser.original_structure(), crystal.identifier) optimised_crystal = Crystal(results.crystal_structure(), optimised_name) n_steps = self.optimiser.step_count() starting_score = self.score(original_crystal) final_score = results.absolute_score() return self.Results(results, crystal, original_crystal, optimised_crystal, optimised_name, starting_score, final_score, n_steps)
[docs] def score(self, crystal): '''Score the crystal based on the current settings.''' self.optimiser.set_settings(self.settings._settings) score = self.optimiser.score(crystal._csv) return score