#!/usr/bin/env python
# Copyright 2014-2019 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author: Qiming Sun <osirpt.sun@gmail.com>
#
'''Wigner rotation D-matrix for real spherical harmonics'''
from math import sqrt
from functools import reduce
import numpy
from scipy.special import factorial
from pyscf.symm import sph
[docs]
def Dmatrix(l, alpha, beta, gamma, reorder_p=False):
'''Wigner rotation D-matrix
D_{mm'} = <lm|R(alpha,beta,gamma)|lm'>
alpha, beta, gamma are Euler angles (in z-y-z convention)
Kwargs:
reorder_p (bool): Whether to put the p functions in the (x,y,z) order.
'''
if l == 0:
return numpy.eye(1)
else:
d = dmatrix(l, beta, reorder_p=False)
ms = numpy.arange(-l, l+1)
D = numpy.einsum('i,ij,j->ij', numpy.exp(-1j*alpha*ms), d,
numpy.exp(-1j*gamma*ms))
D = _dmat_to_real(l, D, reorder_p=False)
if reorder_p and l == 1:
D = D[[2,0,1]][:,[2,0,1]]
return D
def _dmat_to_real(l, d, reorder_p=False):
''' Transform the input D-matrix to make it compatible with the real
spherical harmonic functions.
Kwargs:
reorder_p (bool): Whether to put the p functions in the (x,y,z) order.
'''
# The input D matrix works for pure spherical harmonics. The real
# representation should be U^\dagger * D * U, where U is the unitary
# matrix that transform the complex harmonics to the real harmonics.
u = sph.sph_pure2real(l, reorder_p)
return reduce(numpy.dot, (u.conj().T, d, u)).real
[docs]
def dmatrix(l, beta, reorder_p=False):
'''Wigner small-d matrix (in z-y-z convention)'''
c = numpy.cos(beta/2)
s = numpy.sin(beta/2)
if l == 0:
return numpy.eye(1)
elif l == 1:
mat = numpy.array(((c**2 , sqrt(2)*c*s , s**2 ),
(-sqrt(2)*c*s, c**2-s**2 , sqrt(2)*c*s),
(s**2 , -sqrt(2)*c*s, c**2 )))
if reorder_p:
mat = mat[[2,0,1]][:,[2,0,1]]
return mat
elif l == 2:
c3s = c**3*s
s3c = s**3*c
c2s2 = (c*s)**2
c4 = c**4
s4 = s**4
s631 = sqrt(6)*(c3s-s3c)
s622 = sqrt(6)*c2s2
c4s2 = c4-3*c2s2
c2s4 = 3*c2s2-s4
c4s4 = c4-4*c2s2+s4
return numpy.array((( c4 , 2*c3s, s622, 2*s3c, s4 ),
(-2*c3s , c4s2 , s631, c2s4 , 2*s3c),
( s622 ,-s631 , c4s4, s631 , s622 ),
(-2*s3c , c2s4 ,-s631, c4s2 , 2*c3s),
( s4 ,-2*s3c, s622,-2*c3s, c4 )))
else:
facs = factorial(numpy.arange(2*l+1))
cs = c**numpy.arange(2*l+1)
ss = s**numpy.arange(2*l+1)
mat = numpy.zeros((2*l+1,2*l+1))
for i,m1 in enumerate(range(-l, l+1)):
for j,m2 in enumerate(range(-l, l+1)):
#:fac = sqrt( factorial(l+m1)*factorial(l-m1) \
#: *factorial(l+m2)*factorial(l-m2))
#:for k in range(max(m2-m1,0), min(l+m2, l-m1)+1):
#: mat[i,j] += (-1)**(m1+m2+k) \
#: * c**(2*l+m2-m1-2*k) * s**(m1-m2+2*k) \
#: / (factorial(l+m2-k) * factorial(k) \
#: * factorial(m1-m2+k) * factorial(l-m1-k))
#:mat[i,j] *= fac
k = numpy.arange(max(m2-m1,0), min(l+m2, l-m1)+1)
tmp = (cs[2*l+m2-m1-2*k] * ss[m1-m2+2*k] /
(facs[l+m2-k] * facs[k] * facs[m1-m2+k] * facs[l-m1-k]))
mask = ((m1+m2+k) & 0b1).astype(bool)
mat[i,j] -= tmp[ mask].sum()
mat[i,j] += tmp[~mask].sum()
ms = numpy.arange(-l, l+1)
msfac = numpy.sqrt(facs[l+ms] * facs[l-ms])
mat *= numpy.einsum('i,j->ij', msfac, msfac)
return mat
[docs]
def get_euler_angles(c1, c2):
'''Find the three Euler angles (alpha, beta, gamma in z-y-z convention)
that rotates coordinates c1 to coordinates c2.
yp = numpy.einsum('j,kj->k', c1[1], geom.rotation_mat(c1[2], beta))
tmp = numpy.einsum('ij,kj->ik', c1 , geom.rotation_mat(c1[2], alpha))
tmp = numpy.einsum('ij,kj->ik', tmp, geom.rotation_mat(yp , beta ))
c2 = numpy.einsum('ij,kj->ik', tmp, geom.rotation_mat(c2[2], gamma))
(For backward compatibility) if c1 and c2 are two points in the real
space, the Euler angles define the rotation transforms the old coordinates
to the new coordinates (new_x, new_y, new_z) in which c1 is identical to c2.
tmp = numpy.einsum('j,kj->k', c1 , geom.rotation_mat((0,0,1), gamma))
tmp = numpy.einsum('j,kj->k', tmp, geom.rotation_mat((0,1,0), beta) )
c2 = numpy.einsum('j,kj->k', tmp, geom.rotation_mat((0,0,1), alpha))
'''
c1 = numpy.asarray(c1)
c2 = numpy.asarray(c2)
if c1.ndim == 2 and c2.ndim == 2:
zz = c1[2].dot(c2[2])
if abs(zz - 1.0) < 1e-12:
beta = numpy.arccos(1.0)
elif abs(zz + 1.0) < 1e-12:
beta = numpy.arccos(-1.0)
else:
beta = numpy.arccos(zz)
if abs(zz) < 1 - 1e-12:
yp = numpy.cross(c1[2], c2[2])
yp /= numpy.linalg.norm(yp)
else:
yp = c1[1]
yy = yp.dot(c1[1])
alpha = numpy.arccos(yy)
if numpy.cross(c1[1], yp).dot(c1[2]) < 0:
alpha = -alpha
tmp = yp.dot(c2[1])
if abs(tmp - 1.0) < 1e-12:
gamma = numpy.arccos(1.0)
elif abs(tmp + 1.0) < 1e-12:
gamma = numpy.arccos(-1.0)
else:
gamma = numpy.arccos(tmp)
if numpy.cross(yp, c2[1]).dot(c2[2]) < 0:
gamma = -gamma
else: # For backward compatibility, c1 and c2 are two points
norm1 = numpy.linalg.norm(c1)
norm2 = numpy.linalg.norm(c2)
assert (abs(norm1 - norm2) < 1e-12)
xy_norm = numpy.linalg.norm(c1[:2])
if xy_norm > 1e-12:
gamma = -numpy.arccos(c1[0] / xy_norm)
else:
gamma = 0
xy_norm = numpy.linalg.norm(c2[:2])
if xy_norm > 1e-12:
alpha = numpy.arccos(c2[0] / xy_norm)
else:
alpha = 0
beta = numpy.arccos(c2[2]/norm1) - numpy.arccos(c1[2]/norm2)
return alpha, beta, gamma