#!/usr/bin/env python # MatrixExpert.py # Copyright (C) 2006 CCLRC, Graeme Winter # # This code is distributed under the BSD license, a copy of which is # included in the root directory of this package. # # 24th July 2007 # # A small expert to handle orientation matrix calculations. # import os import sys import math from cctbx import sgtbx from cctbx import crystal from cctbx import uctbx from scitbx import matrix if not os.environ.has_key('XIA2CORE_ROOT'): raise RuntimeError, 'XIA2CORE_ROOT not defined' if not os.environ.has_key('XIA2_ROOT'): raise RuntimeError, 'XIA2_ROOT not defined' if not os.path.join(os.environ['XIA2CORE_ROOT'], 'Python') in sys.path: sys.path.append(os.path.join(os.environ['XIA2CORE_ROOT'], 'Python')) if not os.environ['XIA2_ROOT'] in sys.path: sys.path.append(os.environ['XIA2_ROOT']) from Experts.SymmetryExpert import symop_to_mat, mat_to_symop from Experts.LatticeExpert import l2s, s2l from Wrappers.CCP4.Othercell import Othercell from Wrappers.Phenix.LatticeSymmetry import LatticeSymmetry from lib.SymmetryLib import lattice_to_spacegroup from Handlers.Syminfo import Syminfo from lib.bits import auto_logfiler from Handlers.Streams import Debug from ReferenceFrame import mosflm_to_xia2, xia2_to_mosflm # jiffies to convert matrix format (messy) def mat2vec(mat): return [[mat[0], mat[3], mat[6]], [mat[1], mat[4], mat[7]], [mat[2], mat[5], mat[8]]] def vec2mat(vectors): return [vectors[0][0], vectors[1][0], vectors[2][0], vectors[0][1], vectors[1][1], vectors[2][1], vectors[0][2], vectors[1][2], vectors[2][2]] # generic mathematical calculations for 3-vectors # FIXME cite PRE as the source here for these rotns - N.B. these should be # replaced with CCTBX code. def rot_x(theta): '''Rotation matrix about Y of theta degrees.''' dtor = 180.0 / (4.0 * math.atan(1.0)) c = math.cos(theta / dtor) s = math.sin(theta / dtor) return [1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c] def rot_y(theta): '''Rotation matrix about Y of theta degrees.''' dtor = 180.0 / (4.0 * math.atan(1.0)) c = math.cos(theta / dtor) s = math.sin(theta / dtor) return [c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c] def rot_z(theta): '''Rotation matrix about Y of theta degrees.''' dtor = 180.0 / (4.0 * math.atan(1.0)) c = math.cos(theta / dtor) s = math.sin(theta / dtor) return [c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0] def b_matrix(a, b, c, alpha, beta, gamma): '''Generate a B matric from a unit cell. Cite: Pflugrath in Methods Enzymology 276.''' dtor = 180.0 / (4.0 * math.atan(1.0)) ca = math.cos(alpha / dtor) sa = math.sin(alpha / dtor) cb = math.cos(beta / dtor) sb = math.sin(beta / dtor) cg = math.cos(gamma / dtor) sg = math.sin(gamma / dtor) # invert the cell parameters # CITE: International Tables C Section 1.1 V = a * b * c * math.sqrt(1 - ca * ca - cb * cb - cg * cg + 2 * ca * cb * cg) a_ = b * c * sa / V b_ = a * c * sb / V c_ = a * b * sg / V # NOTE well - these angles are in radians alpha_ = math.acos((cb * cg - ca) / (sb * sg)) beta_ = math.acos((ca * cg - cb) / (sa * sg)) gamma_ = math.acos((ca * cb - cg) / (sa * sb)) ca_ = math.cos(alpha_) sa_ = math.sin(alpha_) cb_ = math.cos(beta_) sb_ = math.sin(beta_) cg_ = math.cos(gamma_) sg_ = math.sin(gamma_) # NEXT construct the B matrix - CITE Pflugrath in Methods E 276 return [a_, b_ * cg_, c_ * cb_, 0.0, b_ * sg_, - c_ * sb_ * ca_, 0.0, 0.0, c_ * sb_ * sa_] def dot(a, b): return sum([a[j] * b[j] for j in range(3)]) def cross(a, b): return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] def vecscl(vector, scale): return [vector[j] * scale for j in range(len(vector))] def matscl(matrix, scale): return [matrix[j] * scale for j in range(len(matrix))] def invert(matrix): vecs = mat2vec(matrix) scl = 1.0 / dot(vecs[0], cross(vecs[1], vecs[2])) return transpose( vec2mat([vecscl(cross(vecs[1], vecs[2]), scl), vecscl(cross(vecs[2], vecs[0]), scl), vecscl(cross(vecs[0], vecs[1]), scl)])) def transpose(matrix): return [matrix[0], matrix[3], matrix[6], matrix[1], matrix[4], matrix[7], matrix[2], matrix[5], matrix[8]] def det(matrix): vecs = mat2vec(matrix) return dot(vecs[0], cross(vecs[1], vecs[2])) def matmul(b, a): avec = mat2vec(transpose(a)) bvec = mat2vec(b) result = [] for i in range(3): for j in range(3): result.append(dot(avec[i], bvec[j])) return result def matvecmul(M, v): '''Multiply a vector v by a matrix M -> return M v.''' Mvec = mat2vec(transpose(M)) result = [] for i in range(3): result.append(dot(Mvec[i], v)) return result # things specific to mosflm matrix files... def parse_matrix(matrix_text): '''Parse a matrix returning cell, a and u matrix.''' # this will need to be able to cope with the times # when the matrix includes columns merging together # (which sucks) # therefore parse this manually... or just add # a space before all '-' tokens = map(float, matrix_text.replace('-', ' -').split()[:30]) cell = tokens[21:27] a = tokens[0:9] u = tokens[12:21] return cell, a, u def format_matrix(cell, a, u): matrix_format = ' %11.8f %11.8f %11.8f\n' + \ ' %11.8f %11.8f %11.8f\n' + \ ' %11.8f %11.8f %11.8f\n' cell_format = ' %11.4f %11.4f %11.4f %11.4f %11.4f %11.4f\n' misset = ' 0.000 0.000 0.000\n' return matrix_format % tuple(a) + misset + matrix_format % tuple(u) + \ cell_format % tuple(cell) + misset def transmogrify_matrix(lattice, matrix, target_lattice, wavelength, wd = None): '''Transmogrify a matrix for lattice X into a matrix for lattice Y. This should work find for Mosflm... Will also return the new unit cell.''' cell, a, u = parse_matrix(matrix) o = _Othercell() if wd: o.set_working_directory(wd) auto_logfiler(o) o.set_cell(cell) o.set_lattice(lattice) o.generate() new_cell = o.get_cell(target_lattice) op = symop_to_mat(o.get_reindex_op(target_lattice)) # HACK! again - bug # 3193 if 'lattice_symmetry' in o.get_executable(): op = transpose(op) # why is only one of the matrices inverted?! a = matmul(invert(op), a) u = matmul(op, u) # in here test that the given unit cell corresponds to the # one calculated from the A matrix. anew = matscl(a, 1.0 / wavelength) reala = transpose(invert(anew)) _a, _b, _c = mat2vec(reala) la = math.sqrt(dot(_a, _a)) lb = math.sqrt(dot(_b, _b)) lc = math.sqrt(dot(_c, _c)) if math.fabs(la - new_cell[0]) / new_cell[0] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength if math.fabs(lb - new_cell[1]) / new_cell[1] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength if math.fabs(lc - new_cell[2]) / new_cell[2] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength return format_matrix(new_cell, a, u) def _Othercell(): '''A factory to produce either a wrapper for LaticeSymmetry or OtherCell depending on what is available.''' o = None try: o = LatticeSymmetry() Debug.write('Using iotbx.lattice_symmetry') except: o = Othercell() Debug.write('Using othercell') return o def get_real_space_primitive_matrix(lattice, matrix, wd = None): '''Get the primitive real space vectors for the unit cell and lattice type. Note that the resulting matrix will need to be scaled by a factor equal to the wavelength in Angstroms.''' # parse the orientation matrix cell, a, u = parse_matrix(matrix) # generate other possibilities o = _Othercell() if wd: o.set_working_directory(wd) auto_logfiler(o) o.set_cell(cell) o.set_lattice(lattice) o.generate() # transform the possibly centred cell to the primitive setting new_cell = o.get_cell('aP') op = symop_to_mat(o.get_reindex_op('aP')) primitive_a = matmul(invert(op), a) # then convert to real space real_a = invert(primitive_a) return real_a[0:3], real_a[3:6], real_a[6:9] def get_reciprocal_space_primitive_matrix(lattice, matrix, wd = None): '''Get the primitive reciprocal space vectors for this matrix.''' # parse the orientation matrix cell, a, u = parse_matrix(matrix) # generate other possibilities o = _Othercell() if wd: o.set_working_directory(wd) auto_logfiler(o) o.set_cell(cell) o.set_lattice(lattice) o.generate() # transform the possibly centred cell to the primitive setting new_cell = o.get_cell('aP') op = symop_to_mat(o.get_reindex_op('aP')) primitive_a = matmul(invert(op), a) return mat2vec(primitive_a) def find_primitive_axes(lattice, matrix, wd = None): '''From an orientation matrix file, calculate the angles (phi) where the primitive cell axes a, b, c are in the plane of the detector (that is, orthogonal to the direct beam vector.''' a, b, c = get_real_space_primitive_matrix(lattice, matrix, wd) dtor = 180.0 / (4.0 * math.atan(1.0)) return (dtor * math.atan( - a[2] / a[0]), \ dtor * math.atan( - b[2] / b[0]), \ dtor * math.atan( - c[2] / c[0])) def find_primitive_reciprocal_axes(lattice, matrix, wd = None): '''From an orientation matrix file, calculate the angles (phi) where the primitive reciprical space cell axes a, b, c are in the plane of the detector (that is, orthogonal to the direct beam vector.''' a, b, c = get_reciprocal_space_primitive_matrix(lattice, matrix, wd) dtor = 180.0 / (4.0 * math.atan(1.0)) return (dtor * math.atan( - a[2] / a[0]), \ dtor * math.atan( - b[2] / b[0]), \ dtor * math.atan( - c[2] / c[0])) def mosflm_a_matrix_to_real_space(wavelength, lattice, matrix): '''Given a Mosflm A matrix and the associated spacegroup (think of this Bravais lattice (which will be converted to a spacegroup for the benefit of the CCTBX program lattice_symmetry) return the real space primative crystal lattice vectors in the xia2 reference frame. This reference frame corresponds to that defined for imgCIF.''' # convert the lattice to a spacegroup - not needed, see below # spacegroup_number = lattice_to_spacegroup(lattice) # spacegroup = Syminfo.spacegroup_name_to_number(spacegroup_number) # get the a, u, matrices and the unit cell cell, a, u = parse_matrix(matrix) # use iotbx.latice_symmetry to obtain the reindexing operator to # a primative triclinic lattice - this should not be specific to # using iotbx - othercell should be optionally supported too... ls = _Othercell() ls.set_cell(cell) ls.set_lattice(lattice) # ls.set_spacegroup(spacegroup) # cell, reindex = ls.generate_primative_reindex() ls.generate() cell = ls.get_cell('aP') reindex = ls.get_reindex_op('aP') reindex_matrix = symop_to_mat(reindex) # grim hack warning! - this is because for some reason (probably to # do with LS getting the inverse operation) othercell is returning # the transpose of the operator or something... FIXME bug # 3193 if 'othercell' in ls.get_executable(): reindex_matrix = transpose(reindex_matrix) # scale the a matrix a = matscl(a, 1.0 / wavelength) # convert to real space (invert) and apply this reindex operator to the a # matrix to get the primative real space triclinic cell axes real_a = matmul(reindex_matrix, transpose(invert(a))) # convert these to the xia2 reference frame a, b, c = mat2vec(real_a) ax = mosflm_to_xia2(a) bx = mosflm_to_xia2(b) cx = mosflm_to_xia2(c) # FIXME in here add check that the unit cell lengths are within 1% of # the correct value - if they are not, raise an exception as the wavelength # provided is probably wrong... la = math.sqrt(dot(ax, ax)) lb = math.sqrt(dot(bx, bx)) lc = math.sqrt(dot(cx, cx)) # print out values - the cell may have been reindexed. Debug.write('Reindexed cell lengths: %.3f %.3f %.3f' % \ (cell[0], cell[1], cell[2])) Debug.write('Calculated from matrix: %.3f %.3f %.3f' % \ (la, lb, lc)) if math.fabs(la - cell[0]) / cell[0] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength if math.fabs(lb - cell[1]) / cell[1] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength if math.fabs(lc - cell[2]) / cell[2] > 0.01: raise RuntimeError, 'cell check failed (wavelength != %f)' % \ wavelength # return these vectors return ax, bx, cx def reindex_sym_related(A, A_ref): '''Calculate a reindexing matrix to move the indices referred to in A to the reference frame in A_ref: both are orientation matrices from Mosflm.''' Amat = matrix.sqr(parse_matrix(A)[1]) Amat_ref = matrix.sqr(parse_matrix(A_ref)[1]) R = Amat_ref.inverse() * Amat Debug.write('%5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f' % \ R.elems) reindex = mat_to_symop(R) return reindex def mosflm_matrix_centred_to_primitive(lattice, mosflm_a_matrix): '''Convert a mosflm orientation matrix from a centred setting to a primitive one (i.e. same lattice, but without the centering operations, which therefore corresponds to a different basis).''' space_group_number = l2s(lattice) spacegroup = sgtbx.space_group_symbols(space_group_number).hall() sg = sgtbx.space_group(spacegroup) rtod = 180.0 / math.pi if not (sg.n_ltr() - 1): return mosflm_a_matrix cell, amat, umat = parse_matrix(mosflm_a_matrix) # first derive the wavelength mi = matrix.sqr(amat) m = mi.inverse() A = matrix.col(m.elems[0:3]) B = matrix.col(m.elems[3:6]) C = matrix.col(m.elems[6:9]) a = math.sqrt(A.dot()) b = math.sqrt(B.dot()) c = math.sqrt(C.dot()) alpha = rtod * B.angle(C) beta = rtod * C.angle(A) gamma = rtod * A.angle(B) wavelength = ((cell[0] / a) + (cell[1] / b) + (cell[2] / c)) / 3.0 # then use this to rescale the A matrix mi = matrix.sqr([a / wavelength for a in amat]) m = mi.inverse() sgp = sg.build_derived_group(True, False) lattice_p = s2l(sgp.type().number()) symm = crystal.symmetry(unit_cell = cell, space_group = sgp) rdx = symm.change_of_basis_op_to_best_cell() symm_new = symm.change_basis(rdx) # now apply this to the reciprocal-space orientation matrix mi cb_op = rdx R = cb_op.c_inv().r().as_rational().as_float().transpose().inverse() mi_r = mi * R # now re-derive the cell constants, just to be sure m_r = mi_r.inverse() Ar = matrix.col(m_r.elems[0:3]) Br = matrix.col(m_r.elems[3:6]) Cr = matrix.col(m_r.elems[6:9]) a = math.sqrt(Ar.dot()) b = math.sqrt(Br.dot()) c = math.sqrt(Cr.dot()) alpha = rtod * Br.angle(Cr) beta = rtod * Cr.angle(Ar) gamma = rtod * Ar.angle(Br) cell = uctbx.unit_cell((a, b, c, alpha, beta, gamma)) amat = [wavelength * e for e in mi_r.elems] bmat = matrix.sqr(cell.fractionalization_matrix()) umat = mi_r * bmat.inverse() new_matrix = ['%s\n' % r for r in \ format_matrix((a, b, c, alpha, beta, gamma), amat, umat.elems).split('\n')] return new_matrix if __name__ == '__main__': lattice = sys.argv[1] mosflm_a_matrix = open(sys.argv[2]).read() result = mosflm_matrix_centred_to_primitive(lattice, mosflm_a_matrix) for r in result: print r[:-1]