Source code for galpy.potential.RotateAndTiltWrapperPotential

###############################################################################
#   RotateAndTiltWrapperPotential.py: Wrapper to rotate and tilt the z-axis
#   of a potential
###############################################################################
import numpy
from .Potential import check_potential_inputs_not_arrays, \
    _evaluatePotentials, _evaluateRforces, _evaluatezforces, \
    _evaluatephiforces, evaluateDensities, evaluateR2derivs, \
    evaluatez2derivs, evaluatephi2derivs, evaluateRzderivs, \
    evaluateRphiderivs, evaluatephizderivs
from .WrapperPotential import WrapperPotential
from ..util import conversion
from ..util import _rotate_to_arbitrary_vector
from ..util import coords
# Only implement 3D wrapper
[docs]class RotateAndTiltWrapperPotential(WrapperPotential): """Potential wrapper that allows a potential to be rotated in 3D according to three orientation angles. These angles can either be specified using: * A rotation around the original z-axis (`galaxy_pa`) and the new direction of the z-axis (`zvec`) or * A rotation around the original z-axis (`galaxy_pa`), the `inclination`, and a rotation around the new z axis (`sky_pa`). The second option allows one to specify the inclination and sky position angle (measured from North) in the usual manner in extragalactic observations."""
[docs] def __init__(self,amp=1.,inclination=None,galaxy_pa=None,sky_pa=None, zvec=None,pot=None, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a RotateAndTiltWrapper Potential INPUT: amp= (1.) overall amplitude to apply to the potential pot= Potential instance or list thereof for the potential to rotate and tilt Orientation angles as galaxy_pa= rotation angle of the original potential around the original z axis (can be a Quantity) and either 1) zvec= 3D vector specifying the direction of the rotated z axis 2) inclination= usual inclination angle (with the line-of-sight being the z axis) sky_pa= rotation angle around the inclined z axis (usual sky position angle measured from North) OUTPUT: (none) HISTORY: 2021-03-29 - Started - Mackereth (UofT) 2021-04-18 - Added inclination, sky_pa, galaxy_pa setup - Bovy (UofT) """ WrapperPotential.__init__(self,amp=amp,pot=pot,ro=ro,vo=vo, _init=True) inclination= conversion.parse_angle(inclination) sky_pa= conversion.parse_angle(sky_pa) galaxy_pa= conversion.parse_angle(galaxy_pa) zvec, galaxy_pa= self._parse_inclination(inclination,sky_pa, zvec,galaxy_pa) self._setup_zvec_pa(zvec,galaxy_pa) self.hasC= True self.hasC_dxdv= True self.isNonAxi= True
def _parse_inclination(self,inclination,sky_pa,zvec,galaxy_pa): if inclination is None: return (zvec,galaxy_pa) if sky_pa is None: sky_pa= 0. zvec_rot= numpy.dot(\ numpy.array([[numpy.sin(sky_pa),numpy.cos(sky_pa),0.], [-numpy.cos(sky_pa),numpy.sin(sky_pa),0.], [0.,0.,1]]), numpy.array([[1.,0.,0.], [0.,-numpy.cos(inclination),-numpy.sin(inclination)], [0.,-numpy.sin(inclination),numpy.cos(inclination)]])) zvec= numpy.dot(zvec_rot,numpy.array([0.,0.,1.])) int_rot= _rotate_to_arbitrary_vector(numpy.array([[0.,0.,1.]]), zvec,inv=False)[0] pa= numpy.dot(int_rot,numpy.dot(zvec_rot,[1.,0.,0.])) return (zvec,galaxy_pa+numpy.arctan2(pa[1],pa[0])) def _setup_zvec_pa(self,zvec,pa): if not pa is None: pa_rot= numpy.array([[numpy.cos(pa),numpy.sin(pa),0.], [-numpy.sin(pa),numpy.cos(pa),0.], [0.,0.,1.]]) else: pa_rot = numpy.eye(3) if not zvec is None: if not isinstance(zvec,numpy.ndarray): zvec= numpy.array(zvec) zvec/= numpy.sqrt(numpy.sum(zvec**2.)) zvec_rot= _rotate_to_arbitrary_vector(\ numpy.array([[0.,0.,1.]]),zvec,inv=True)[0] else: zvec_rot = numpy.eye(3) self._rot = numpy.dot(pa_rot,zvec_rot) self._inv_rot = numpy.linalg.inv(self._rot) return None def __getattr__(self,attribute): return super(RotateAndTiltWrapperPotential,self)\ .__getattr__(attribute) @check_potential_inputs_not_arrays def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2021-04-18 - Written - Bovy (UofT) """ x,y,z= coords.cyl_to_rect(R,phi,z) if numpy.isinf(R): y= 0. xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) Rp,phip,zp = coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) return _evaluatePotentials(self._pot,Rp,zp,phi=phip,t=t) @check_potential_inputs_not_arrays def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2021-04-18 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)*Fxyz[0]+numpy.sin(phi)*Fxyz[1] @check_potential_inputs_not_arrays def _phiforce(self,R,z,phi=0.,t=0.): """ NAME: _phiforce PURPOSE: evaluate the azimuthal force (torque) for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal force (torque) HISTORY: 2021-04-18 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) return R*(-numpy.sin(phi)*Fxyz[0] + numpy.cos(phi)*Fxyz[1]) @check_potential_inputs_not_arrays def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2021-04-18 - Written - Bovy (UofT) """ return self._force_xyz(R,z,phi=phi,t=t)[2] def _force_xyz(self,R,z,phi=0.,t=0.): """Get the rectangular forces in the transformed frame""" x,y,z= coords.cyl_to_rect(R,phi,z) xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) Rp,phip,zp =coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) Rforcep= _evaluateRforces(self._pot,Rp,zp,phi=phip,t=t) phiforcep= _evaluatephiforces(self._pot,Rp,zp,phi=phip,t=t) zforcep= _evaluatezforces(self._pot,Rp,zp,phi=phip,t=t) xforcep= numpy.cos(phip)*Rforcep-numpy.sin(phip)*phiforcep/Rp yforcep= numpy.sin(phip)*Rforcep+numpy.cos(phip)*phiforcep/Rp return numpy.dot(self._inv_rot, numpy.array([xforcep,yforcep,zforcep])) @check_potential_inputs_not_arrays def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)**2.*phi2[0,0]+numpy.sin(phi)**2.*phi2[1,1]\ +2.*numpy.cos(phi)*numpy.sin(phi)*phi2[0,1] @check_potential_inputs_not_arrays def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, vertical derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)*phi2[0,2]+numpy.sin(phi)*phi2[1,2] @check_potential_inputs_not_arrays def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ return self._2ndderiv_xyz(R,z,phi=phi,t=t)[2,2] @check_potential_inputs_not_arrays def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R**2.*(numpy.sin(phi)**2.*phi2[0,0] +numpy.cos(phi)**2.*phi2[1,1]\ -2.*numpy.cos(phi)*numpy.sin(phi)*phi2[0,1])\ +R*(numpy.cos(phi)*Fxyz[0]+numpy.sin(phi)*Fxyz[1]) @check_potential_inputs_not_arrays def _Rphideriv(self,R,z,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial, azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R*numpy.cos(phi)*numpy.sin(phi)*\ (phi2[1,1]-phi2[0,0])+R*numpy.cos(2.*phi)*phi2[0,1]\ +numpy.sin(phi)*Fxyz[0]-numpy.cos(phi)*Fxyz[1] @check_potential_inputs_not_arrays def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed azimuthal, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed azimuthal, vertical derivative HISTORY: 2021-04-30 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R*(numpy.cos(phi)*phi2[1,2]-numpy.sin(phi)*phi2[0,2]) def _2ndderiv_xyz(self,R,z,phi=0.,t=0.): """Get the rectangular forces in the transformed frame""" x,y,z= coords.cyl_to_rect(R,phi,z) xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) Rp,phip,zp =coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) Rforcep= _evaluateRforces(self._pot,Rp,zp,phi=phip,t=t) phiforcep= _evaluatephiforces(self._pot,Rp,zp,phi=phip,t=t) R2derivp= evaluateR2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) phi2derivp= evaluatephi2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) z2derivp= evaluatez2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) Rzderivp= evaluateRzderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) Rphiderivp= evaluateRphiderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) phizderivp= evaluatephizderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) cp, sp= numpy.cos(phip), numpy.sin(phip) cp2, sp2, cpsp= cp**2., sp**2., cp*sp Rp2= Rp*Rp x2derivp= R2derivp*cp2-2.*Rphiderivp*cpsp/Rp+phi2derivp*sp2/Rp2\ -Rforcep*sp2/Rp-2.*phiforcep*cpsp/Rp2 y2derivp= R2derivp*sp2+2.*Rphiderivp*cpsp/Rp+phi2derivp*cp2/Rp2\ -Rforcep*cp2/Rp+2.*phiforcep*cpsp/Rp2 xyderivp= R2derivp*cpsp+Rphiderivp*(cp2-sp2)/Rp-phi2derivp*cpsp/Rp2\ +Rforcep*cpsp/Rp+phiforcep*(cp2-sp2)/Rp2 xzderivp= Rzderivp*cp-phizderivp*sp/Rp yzderivp= Rzderivp*sp+phizderivp*cp/Rp return numpy.dot(self._inv_rot,numpy.dot(\ numpy.array([[x2derivp,xyderivp,xzderivp], [xyderivp,y2derivp,yzderivp], [xzderivp,yzderivp,z2derivp]]), self._inv_rot.T)) @check_potential_inputs_not_arrays def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2021-04-18 - Written - Bovy (UofT) """ x,y,z= coords.cyl_to_rect(R,phi,z) if numpy.isinf(R): y= 0. xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) Rp,phip,zp = coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) return evaluateDensities(self._pot,Rp,zp,phi=phip,t=t, use_physical=False)