def tst_use_in_stills_parameterisation_for_crystal(crystal_param=0): # test use of analytical expression in stills prediction parameterisation from scitbx import matrix from math import pi, sqrt, atan2 import random print print "Test use of analytical expressions in stills prediction " + "parameterisation for crystal parameters" # crystal model from dxtbx.model.crystal import crystal_model from dials.algorithms.refinement.parameterisation.crystal_parameters import ( CrystalOrientationParameterisation, CrystalUnitCellParameterisation, ) crystal = crystal_model((20, 0, 0), (0, 30, 0), (0, 0, 40), space_group_symbol="P 1") # random reorientation e = matrix.col((random.random(), random.random(), random.random())).normalize() angle = random.random() * 180 crystal.rotate_around_origin(e, angle) wl = 1.1 s0 = matrix.col((0, 0, 1 / wl)) s0u = s0.normalize() # these are stills, but need a rotation axis for the Reeke algorithm axis = matrix.col((1, 0, 0)) # crystal parameterisations xlop = CrystalOrientationParameterisation(crystal) xlucp = CrystalUnitCellParameterisation(crystal) # Find some reflections close to the Ewald sphere from dials.algorithms.spot_prediction.reeke import reeke_model U = crystal.get_U() B = crystal.get_B() UB = U * B dmin = 4 hkl = reeke_model(UB, UB, axis, s0, dmin, margin=1).generate_indices() # choose first reflection for now, calc quantities relating to q h = matrix.col(hkl[0]) q = UB * h q0 = q.normalize() q_scalar = q.length() qq = q_scalar * q_scalar # calculate the axis of closest rotation e1 = q0.cross(s0u).normalize() # calculate c0, a vector orthogonal to s0u and e1 c0 = s0u.cross(e1).normalize() # calculate q1 q1 = q0.cross(e1).normalize() # calculate DeltaPsi a = 0.5 * qq * wl b = sqrt(qq - a * a) r = -1.0 * a * s0u + b * c0 DeltaPsi = -1.0 * atan2(r.dot(q1), r.dot(q0)) # Checks on the reflection prediction from libtbx.test_utils import approx_equal # 1. check r is on the Ewald sphere s1 = s0 + r assert approx_equal(s1.length(), s0.length()) # 2. check DeltaPsi is correct tst = q.rotate_around_origin(e1, DeltaPsi) assert approx_equal(tst, r) # choose the derivative with respect to a particular parameter. if crystal_param < 3: dU_dp = xlop.get_ds_dp()[crystal_param] dq = dU_dp * B * h else: dB_dp = xlucp.get_ds_dp()[crystal_param - 3] dq = U * dB_dp * h # NKS method of calculating d[q0]/dp q_dot_dq = q.dot(dq) dqq = 2.0 * q_dot_dq dq_scalar = dqq / q_scalar dq0_dp = (q_scalar * dq - (q_dot_dq * q0)) / qq # orthogonal to q0, as expected. print "NKS [q0].(d[q0]/dp) = {0} (should be 0.0)".format(q0.dot(dq0_dp)) # intuitive method of calculating d[q0]/dp, based on the fact that # it must be orthogonal to q0, i.e. in the plane containing q1 and e1 scaled = dq / q.length() dq0_dp = scaled.dot(q1) * q1 + scaled.dot(e1) * e1 # orthogonal to q0, as expected. print "DGW [q0].(d[q0]/dp) = {0} (should be 0.0)".format(q0.dot(dq0_dp)) # So it doesn't matter which method I use to calculate d[q0]/dp, as # both methods give the same results # use the fact that -e1 == q0.cross(q1) to redefine the derivative d[e1]/dp # from Sauter et al. (2014) (A.22) de1_dp = -1.0 * dq0_dp.cross(q1) # this *is* orthogonal to e1, as expected. print "[e1].(d[e1]/dp) = {0} (should be 0.0)".format(e1.dot(de1_dp)) # calculate (d[r]/d[e1])(d[e1]/dp) analytically from scitbx.array_family import flex from dials_refinement_helpers_ext import dRq_de dr_de1 = matrix.sqr(dRq_de(flex.double([DeltaPsi]), flex.vec3_double([e1]), flex.vec3_double([q]))[0]) print "Analytical calculation for (d[r]/d[e1])(d[e1]/dp):" print dr_de1 * de1_dp # now calculate using finite differences. dp = 1.0e-8 del_e1 = de1_dp * dp e1f = e1 + del_e1 * 0.5 rfwd = q.rotate_around_origin(e1f, DeltaPsi) e1r = e1 - del_e1 * 0.5 rrev = q.rotate_around_origin(e1r, DeltaPsi) print "Finite difference estimate for (d[r]/d[e1])(d[e1]/dp):" print (rfwd - rrev) * (1 / dp) print "These are essentially the same :-)"
def test(): import random import textwrap from cctbx.uctbx import unit_cell from libtbx.test_utils import approx_equal def random_direction_close_to(vector): return vector.rotate_around_origin( matrix.col((random.random(), random.random(), random.random())).normalize(), random.gauss(0, 1.0), deg=True, ) # make a random P1 crystal and parameterise it a = random.uniform(10, 50) * random_direction_close_to( matrix.col((1, 0, 0))) b = random.uniform(10, 50) * random_direction_close_to( matrix.col((0, 1, 0))) c = random.uniform(10, 50) * random_direction_close_to( matrix.col((0, 0, 1))) xl = Crystal(a, b, c, space_group_symbol="P 1") xl_op = CrystalOrientationParameterisation(xl) xl_ucp = CrystalUnitCellParameterisation(xl) null_mat = matrix.sqr((0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) # compare analytical and finite difference derivatives an_ds_dp = xl_op.get_ds_dp() fd_ds_dp = get_fd_gradients(xl_op, [1.0e-6 * pi / 180] * 3) for e, f in zip(an_ds_dp, fd_ds_dp): assert approx_equal((e - f), null_mat, eps=1.0e-6) an_ds_dp = xl_ucp.get_ds_dp() fd_ds_dp = get_fd_gradients(xl_ucp, [1.0e-7] * xl_ucp.num_free()) for e, f in zip(an_ds_dp, fd_ds_dp): assert approx_equal((e - f), null_mat, eps=1.0e-6) # random initial orientations with a random parameter shift at each attempts = 100 for i in range(attempts): # make a random P1 crystal and parameterise it a = random.uniform(10, 50) * random_direction_close_to( matrix.col((1, 0, 0))) b = random.uniform(10, 50) * random_direction_close_to( matrix.col((0, 1, 0))) c = random.uniform(10, 50) * random_direction_close_to( matrix.col((0, 0, 1))) xl = Crystal(a, b, c, space_group_symbol="P 1") xl_op = CrystalOrientationParameterisation(xl) xl_uc = CrystalUnitCellParameterisation(xl) # apply a random parameter shift to the orientation p_vals = xl_op.get_param_vals() p_vals = random_param_shift( p_vals, [1000 * pi / 9, 1000 * pi / 9, 1000 * pi / 9]) xl_op.set_param_vals(p_vals) # compare analytical and finite difference derivatives xl_op_an_ds_dp = xl_op.get_ds_dp() xl_op_fd_ds_dp = get_fd_gradients(xl_op, [1.0e-5 * pi / 180] * 3) # apply a random parameter shift to the unit cell. We have to # do this in a way that is respectful to metrical constraints, # so don't modify the parameters directly; modify the cell # constants and extract the new parameters cell_params = xl.get_unit_cell().parameters() cell_params = random_param_shift(cell_params, [1.0] * 6) new_uc = unit_cell(cell_params) newB = matrix.sqr(new_uc.fractionalization_matrix()).transpose() S = symmetrize_reduce_enlarge(xl.get_space_group()) S.set_orientation(orientation=newB) X = S.forward_independent_parameters() xl_uc.set_param_vals(X) xl_uc_an_ds_dp = xl_ucp.get_ds_dp() # now doing finite differences about each parameter in turn xl_uc_fd_ds_dp = get_fd_gradients(xl_ucp, [1.0e-7] * xl_ucp.num_free()) for j in range(3): assert approx_equal((xl_op_fd_ds_dp[j] - xl_op_an_ds_dp[j]), null_mat, eps=1.0e-6), textwrap.dedent("""\ Failure in try {i} failure for parameter number {j} of the orientation parameterisation with fd_ds_dp = {fd} and an_ds_dp = {an} so that difference fd_ds_dp - an_ds_dp = {diff} """).format( i=i, j=j, fd=xl_op_fd_ds_dp[j], an=xl_op_an_ds_dp[j], diff=xl_op_fd_ds_dp[j] - xl_op_an_ds_dp[j], ) for j in range(xl_ucp.num_free()): assert approx_equal((xl_uc_fd_ds_dp[j] - xl_uc_an_ds_dp[j]), null_mat, eps=1.0e-6), textwrap.dedent("""\ Failure in try {i} failure for parameter number {j} of the unit cell parameterisation with fd_ds_dp = {fd} and an_ds_dp = {an} so that difference fd_ds_dp - an_ds_dp = {diff} """).format( i=i, j=j, fd=xl_uc_fd_ds_dp[j], an=xl_uc_an_ds_dp[j], diff=xl_uc_fd_ds_dp[j] - xl_uc_an_ds_dp[j], )
class ModelState(object): """ A class to keep track of the model state """ def __init__( self, experiment, mosaicity_parameterisation, fix_mosaic_spread=False, fix_wavelength_spread=False, fix_unit_cell=False, fix_orientation=False, ): """ Initialise the model state """ # Save the crystal model self.experiment = experiment self.crystal = experiment.crystal # The U and P parameterisation self.U_parameterisation = CrystalOrientationParameterisation( self.crystal) self.B_parameterisation = CrystalUnitCellParameterisation(self.crystal) # The M, L and W parameterisations self.M_parameterisation = mosaicity_parameterisation # Set the flags to fix parameters self._is_mosaic_spread_fixed = fix_mosaic_spread self._is_wavelength_spread_fixed = fix_wavelength_spread self._is_unit_cell_fixed = fix_unit_cell self._is_orientation_fixed = fix_orientation def is_orientation_fixed(self): """ Return whether the orientation is fixed """ return self._is_orientation_fixed def is_unit_cell_fixed(self): """ Return whether the unit cell is fixed """ return self._is_unit_cell_fixed def is_mosaic_spread_fixed(self): """ Return whether the mosaic spread is fixed """ return self._is_mosaic_spread_fixed def is_mosaic_spread_angular(self): """ Return whether the mosaic spread is angular """ return self.M_parameterisation.is_angular() def is_wavelength_spread_fixed(self): """ Return whether the wavelength spread is fixed """ return self._is_wavelength_spread_fixed def get_unit_cell(self): """ Get the crystal unit cell """ return self.crystal.get_unit_cell() def get_U(self): """ Get the crystal U matrix """ return matrix.sqr(self.crystal.get_U()) def get_B(self): """ Get the crystal B matrix """ return matrix.sqr(self.crystal.get_B()) def get_A(self): """ Get the crystal A matrix """ return matrix.sqr(self.crystal.get_A()) def get_M(self): """ Get the Sigma M matrix """ return self.M_parameterisation.sigma() def get_U_params(self): """ Get the U parameters """ return flex.double(self.U_parameterisation.get_param_vals()) def get_B_params(self): """ Get the B parameters """ return flex.double(self.B_parameterisation.get_param_vals()) def get_M_params(self): """ Get the M parameters """ return self.M_parameterisation.parameters() def set_U_params(self, params): """ Set the U parameters """ return self.U_parameterisation.set_param_vals(params) def set_B_params(self, params): """ Set the B parameters """ return self.B_parameterisation.set_param_vals(params) def set_M_params(self, params): """ Set the M parameters """ return self.M_parameterisation.set_parameters(params) def num_U_params(self): """ Get the number of U parameters """ return len(self.get_U_params()) def num_B_params(self): """ Get the number of B parameters """ return len(self.get_B_params()) def num_M_params(self): """ Get the number of M parameters """ return len(self.get_M_params()) def get_dU_dp(self): """ Get the first derivatives of U w.r.t its parameters """ return flex.mat3_double(self.U_parameterisation.get_ds_dp()) def get_dB_dp(self): """ Get the first derivatives of B w.r.t its parameters """ return flex.mat3_double(self.B_parameterisation.get_ds_dp()) def get_dM_dp(self): """ Get the first derivatives of M w.r.t its parameters """ return self.M_parameterisation.first_derivatives() def get_active_parameters(self): """ Get the active parameters in order: U, B, M, L, W """ active_params = flex.double() if not self._is_orientation_fixed: active_params.extend(self.get_U_params()) if not self._is_unit_cell_fixed: active_params.extend(self.get_B_params()) if not self._is_mosaic_spread_fixed: active_params.extend(self.get_M_params()) if not self._is_wavelength_spread_fixed: active_params.extend(self.get_L_params()) assert len(active_params) > 0 return active_params def set_active_parameters(self, params): """ Set the active parameters in order: U, B, M, L, W """ if not self._is_orientation_fixed: temp = params[:self.num_U_params()] params = params[self.num_U_params():] self.set_U_params(temp) if not self._is_unit_cell_fixed: temp = params[:self.num_B_params()] params = params[self.num_B_params():] self.set_B_params(temp) if not self._is_mosaic_spread_fixed: temp = params[:self.num_M_params()] params = params[self.num_M_params():] self.set_M_params(temp) if not self._is_wavelength_spread_fixed: temp = params[:self.num_L_params()] params = params[self.num_L_params():] self.set_L_params(temp) def get_labels(self): """ Get the parameter labels """ labels = [] if not self._is_orientation_fixed: for i in range(len(self.get_U_params())): labels.append("Crystal_U_%d" % i) if not self._is_unit_cell_fixed: for i in range(len(self.get_B_params())): labels.append("Crystal_B_%d" % i) if not self._is_mosaic_spread_fixed: for i in range(len(self.get_M_params())): labels.append("Mosaicity_%d" % i) if not self._is_wavelength_spread_fixed: labels.append("Wavelength_Spread") assert len(labels) > 0 return labels
def tst_use_in_stills_parameterisation_for_crystal(crystal_param=0): # test use of analytical expression in stills prediction parameterisation from scitbx import matrix from math import pi, sqrt, atan2 import random print() print("Test use of analytical expressions in stills prediction " + "parameterisation for crystal parameters") # crystal model from dxtbx.model.crystal import crystal_model from dials.algorithms.refinement.parameterisation.crystal_parameters import ( CrystalOrientationParameterisation, CrystalUnitCellParameterisation, ) crystal = crystal_model((20, 0, 0), (0, 30, 0), (0, 0, 40), space_group_symbol="P 1") # random reorientation e = matrix.col( (random.random(), random.random(), random.random())).normalize() angle = random.random() * 180 crystal.rotate_around_origin(e, angle) wl = 1.1 s0 = matrix.col((0, 0, 1 / wl)) s0u = s0.normalize() # these are stills, but need a rotation axis for the Reeke algorithm axis = matrix.col((1, 0, 0)) # crystal parameterisations xlop = CrystalOrientationParameterisation(crystal) xlucp = CrystalUnitCellParameterisation(crystal) # Find some reflections close to the Ewald sphere from dials.algorithms.spot_prediction.reeke import reeke_model U = crystal.get_U() B = crystal.get_B() UB = U * B dmin = 4 hkl = reeke_model(UB, UB, axis, s0, dmin, margin=1).generate_indices() # choose first reflection for now, calc quantities relating to q h = matrix.col(hkl[0]) q = UB * h q0 = q.normalize() q_scalar = q.length() qq = q_scalar * q_scalar # calculate the axis of closest rotation e1 = q0.cross(s0u).normalize() # calculate c0, a vector orthogonal to s0u and e1 c0 = s0u.cross(e1).normalize() # calculate q1 q1 = q0.cross(e1).normalize() # calculate DeltaPsi a = 0.5 * qq * wl b = sqrt(qq - a * a) r = -1.0 * a * s0u + b * c0 DeltaPsi = -1.0 * atan2(r.dot(q1), r.dot(q0)) # Checks on the reflection prediction from libtbx.test_utils import approx_equal # 1. check r is on the Ewald sphere s1 = s0 + r assert approx_equal(s1.length(), s0.length()) # 2. check DeltaPsi is correct tst = q.rotate_around_origin(e1, DeltaPsi) assert approx_equal(tst, r) # choose the derivative with respect to a particular parameter. if crystal_param < 3: dU_dp = xlop.get_ds_dp()[crystal_param] dq = dU_dp * B * h else: dB_dp = xlucp.get_ds_dp()[crystal_param - 3] dq = U * dB_dp * h # NKS method of calculating d[q0]/dp q_dot_dq = q.dot(dq) dqq = 2.0 * q_dot_dq dq_scalar = dqq / q_scalar dq0_dp = (q_scalar * dq - (q_dot_dq * q0)) / qq # orthogonal to q0, as expected. print("NKS [q0].(d[q0]/dp) = {0} (should be 0.0)".format(q0.dot(dq0_dp))) # intuitive method of calculating d[q0]/dp, based on the fact that # it must be orthogonal to q0, i.e. in the plane containing q1 and e1 scaled = dq / q.length() dq0_dp = scaled.dot(q1) * q1 + scaled.dot(e1) * e1 # orthogonal to q0, as expected. print("DGW [q0].(d[q0]/dp) = {0} (should be 0.0)".format(q0.dot(dq0_dp))) # So it doesn't matter which method I use to calculate d[q0]/dp, as # both methods give the same results # use the fact that -e1 == q0.cross(q1) to redefine the derivative d[e1]/dp # from Sauter et al. (2014) (A.22) de1_dp = -1.0 * dq0_dp.cross(q1) # this *is* orthogonal to e1, as expected. print("[e1].(d[e1]/dp) = {0} (should be 0.0)".format(e1.dot(de1_dp))) # calculate (d[r]/d[e1])(d[e1]/dp) analytically from scitbx.array_family import flex from dials_refinement_helpers_ext import dRq_de dr_de1 = matrix.sqr( dRq_de(flex.double([DeltaPsi]), flex.vec3_double([e1]), flex.vec3_double([q]))[0]) print("Analytical calculation for (d[r]/d[e1])(d[e1]/dp):") print(dr_de1 * de1_dp) # now calculate using finite differences. dp = 1.0e-8 del_e1 = de1_dp * dp e1f = e1 + del_e1 * 0.5 rfwd = q.rotate_around_origin(e1f, DeltaPsi) e1r = e1 - del_e1 * 0.5 rrev = q.rotate_around_origin(e1r, DeltaPsi) print("Finite difference estimate for (d[r]/d[e1])(d[e1]/dp):") print((rfwd - rrev) * (1 / dp)) print("These are essentially the same :-)")