Source code for dials.algorithms.indexing.basis_vector_search.combinations
from __future__ import annotations
import logging
import math
from cctbx.sgtbx.bravais_types import bravais_lattice
from cctbx.uctbx.reduction_base import iteration_limit_exceeded
from dxtbx.model import Crystal
from scitbx.array_family import flex
from dials.algorithms.indexing import DialsIndexError
from dials.algorithms.indexing.compare_orientation_matrices import (
difference_rotation_matrix_axis_angle,
)
from dials.algorithms.indexing.symmetry import find_matching_symmetry
logger = logging.getLogger(__name__)
[docs]
def candidate_orientation_matrices(basis_vectors, max_combinations=None):
# select unique combinations of input vectors to test
# the order of combinations is such that combinations comprising vectors
# nearer the beginning of the input list will appear before combinations
# comprising vectors towards the end of the list
n = len(basis_vectors)
# hardcoded limit on number of vectors, fixes issue #72
# https://github.com/dials/dials/issues/72
n = min(n, 100)
basis_vectors = basis_vectors[:n]
combinations = flex.vec3_int(flex.nested_loop((n, n, n)))
combinations = combinations.select(
flex.sort_permutation(combinations.as_vec3_double().norms())
)
# select only those combinations where j > i and k > j
i, j, k = combinations.as_vec3_double().parts()
sel = flex.bool(len(combinations), True)
sel &= j > i
sel &= k > j
combinations = combinations.select(sel)
if max_combinations is not None and max_combinations < len(combinations):
combinations = combinations[:max_combinations]
half_pi = 0.5 * math.pi
min_angle = 20 / 180 * math.pi # 20 degrees, arbitrary cutoff
for i, j, k in combinations:
a = basis_vectors[i]
b = basis_vectors[j]
angle = a.angle(b)
if angle < min_angle or (math.pi - angle) < min_angle:
continue
a_cross_b = a.cross(b)
gamma = a.angle(b)
if gamma < half_pi:
# all angles obtuse if possible please
b = -b
a_cross_b = -a_cross_b
c = basis_vectors[k]
if abs(half_pi - a_cross_b.angle(c)) < min_angle:
continue
alpha = b.angle(c, deg=True)
if alpha < half_pi:
c = -c
if a_cross_b.dot(c) < 0:
# we want right-handed basis set, therefore invert all vectors
a = -a
b = -b
c = -c
model = Crystal(a, b, c, space_group_symbol="P 1")
uc = model.get_unit_cell()
try:
cb_op_to_niggli = uc.change_of_basis_op_to_niggli_cell()
except iteration_limit_exceeded as e:
raise DialsIndexError(e)
model = model.change_basis(cb_op_to_niggli)
uc = model.get_unit_cell()
params = uc.parameters()
if uc.volume() > (params[0] * params[1] * params[2] / 100):
# unit cell volume cutoff from labelit 2004 paper
yield model
[docs]
def filter_known_symmetry(
crystal_models,
target_symmetry,
relative_length_tolerance=0.1,
absolute_angle_tolerance=5,
max_delta=5,
):
"""Filter crystal models for known symmetry.
Args:
crystal_models (list): A list of :class:`dxtbx.model.Crystal` objects.
target_symmetry (cctbx.crystal.symmetry): The target symmetry for filtering.
relative_length_tolerance (float): Relative tolerance for unit cell lengths in
unit cell comparison (default value is 0.1).
absolute_angle_tolerance (float): Angular tolerance (in degrees) in unit cell
comparison (default value is 5).
max_delta (float): Maximum allowed Le Page delta used in searching for basis
vector combinations that are consistent with the given symmetry (default
value is 5).
"""
n_matched = 0
cb_op_ref_to_primitive = target_symmetry.change_of_basis_op_to_primitive_setting()
if target_symmetry.unit_cell() is not None:
target_symmetry_primitive = target_symmetry.change_basis(cb_op_ref_to_primitive)
else:
target_symmetry_primitive = target_symmetry.customized_copy(
space_group_info=target_symmetry.space_group_info().change_basis(
cb_op_ref_to_primitive
)
)
target_bravais_str = str(
bravais_lattice(
group=target_symmetry_primitive.space_group_info()
.reference_setting()
.group()
)
)
for model in crystal_models:
uc = model.get_unit_cell()
best_subgroup = find_matching_symmetry(
uc, None, max_delta=max_delta, target_bravais_str=target_bravais_str
)
if best_subgroup is not None:
if target_symmetry.unit_cell() is not None and not (
best_subgroup["best_subsym"]
.unit_cell()
.is_similar_to(
target_symmetry.as_reference_setting().best_cell().unit_cell(),
relative_length_tolerance=relative_length_tolerance,
absolute_angle_tolerance=absolute_angle_tolerance,
)
):
logger.debug(
"Rejecting crystal model inconsistent with input symmetry:\n"
f" Unit cell: {str(model.get_unit_cell())}"
)
continue
n_matched += 1
yield model
if not n_matched:
logger.warning(
"No crystal models remaining after comparing with known symmetry"
)
[docs]
def filter_similar_orientations(
crystal_models, other_crystal_models, minimum_angular_separation=5
):
for cryst in crystal_models:
orientation_too_similar = False
for cryst_a in other_crystal_models:
R_ab, axis, angle, cb_op_ab = difference_rotation_matrix_axis_angle(
cryst_a, cryst
)
if abs(angle) < minimum_angular_separation: # degrees
orientation_too_similar = True
break
if orientation_too_similar:
logger.debug("skipping crystal: too similar to other crystals")
continue
yield cryst