#!/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]
