"""
The Harmonic Oscillator (HO) primitive basis functions and DVR functions.
"""
import math
from typing import Optional
import numpy as np
from scipy.special import hermite
from pytdscf import units as _units
from pytdscf.basis.abc import DVRPrimitivesMixin
FACTORIAL_SQRT_INV = [0.0] * 25
FACTORIAL_SQRT_INV[0] = 1.0
for i in range(1, 25):
FACTORIAL_SQRT_INV[i] = FACTORIAL_SQRT_INV[i - 1] / math.sqrt(i)
[docs]
class HarmonicOscillator(DVRPrimitivesMixin):
r"""Harmonic Oscillator DVR functions
See also MCTDH review Phys.Rep. 324, 1 (2000) appendix B
https://doi.org/10.1016/S0370-1573(99)00047-2
Normalization factor :
.. math::
A_n = \frac{1}{\sqrt{n! 2^n}} \left(\frac{m\omega}{\pi\hbar}\right)^{\frac{1}{4}}\
\xrightarrow[\rm impl]{} \frac{1}{\sqrt{n! 2^n}} \left(\frac{\omega_i}{\pi}\right)^{\frac{1}{4}}
Dimensionless coordinate :
.. math::
\zeta = \sqrt{\frac{m\omega} {\hbar}}(x-a) \xrightarrow[\rm impl]{} \sqrt{\omega_i}q_i
Primitive Function :
.. math::
\varphi_n = A_n H_n(\zeta) \exp\left(- \frac{\zeta^2}{2}\right)\quad (n=0,2,\ldots,N-1)
Attributes:
ngrid (int) : # of grid
nprim (int) : # of primitive function. same as ``ngrid``
omega (float) : frequency in a.u.
q_eq (float) : eq_point in mass-weighted coordinate
dimensionless (bool) : Input is dimensionless coordinate or not.
doAnalytical (bool) : Use analytical integral or diagonalization.
"""
def __init__(
self,
ngrid: int,
omega: float,
q_eq: Optional[float] = 0.0,
units: Optional[str] = "cm-1",
dimnsionless: Optional[bool] = False,
doAnalytical: Optional[bool] = True,
):
super().__init__(ngrid)
if units.lower() in ["cm1", "cm-1", "kaiser"]:
self.omega = omega / _units.au_in_cm1
self.freq_cm1 = omega
elif units.lower() in ["au", "hartree", "a.u."]:
self.omega = omega
self.freq_cm1 = omega * _units.au_in_cm1
elif units.lower() == "ev":
self.omega = omega / _units.au_in_eV
self.freq_cm1 = omega / _units.au_in_eV * _units.au_in_cm1
else:
raise ValueError(f"{units} must be [cm1, au, eV]")
self.q_eq = q_eq
if dimnsionless:
self.q_eq /= math.sqrt(self.omega)
self.origin = q_eq
self.sigma = 1 / math.sqrt(self.omega)
self.lb = -ngrid * self.sigma
self.ub = ngrid * self.sigma
self.label = "HO"
self.doAnalytical = doAnalytical
[docs]
def fbr_func(self, n: int, q):
r"""Primitive functions :math:``\varphi_n(q)``
Args:
n (int) : index ``(0 <= n < ngrid)``
Returns:
float : :math:`\varphi_n(q)`
"""
if 0 <= n < self.ngrid:
return (
self._normal_factor(n)
* self._hermite_pol(n, q)
* self._gaussian(q)
)
else:
ValueError(f"n={n} must be in [0,ngrid)=[0,{self.ngrid})")
[docs]
def get_pos_rep_matrix(self):
r"""HO has analytical formulation
.. math::
\left\langle\varphi_{j}|\hat{x}| \varphi_{k}\right\rangle=\
\sqrt{\frac{j+1}{2 m \omega}} \delta_{j, k-1}+x_{\mathrm{eq}}\
\delta_{j k}+\sqrt{\frac{j}{2 m \omega}} \delta_{j, k+1}
"""
if self.doAnalytical:
if not hasattr(self, "pos_rep_matrix"):
diag = np.ones(self.ngrid, dtype=complex) * self.q_eq
semidiag = np.sqrt(
np.arange(1, self.ngrid) / 2 / self.omega, dtype=complex
)
self.pos_rep_matrix = (
np.diag(diag)
+ np.diag(semidiag, k=1)
+ np.diag(semidiag, k=-1)
)
return self.pos_rep_matrix
else:
return super().get_pos_rep_matrix()
[docs]
def get_1st_derivative_matrix_fbr(self):
r"""Analytical Formulation
.. math::
D_{j k}^{(1)}=\
\left\langle\varphi_{j} | \frac{\mathrm{d}}{\mathrm{d} x} | \varphi_{k}\right\rangle=\
-\sqrt{\frac{m \omega}{2}}\left(\sqrt{j+1} \delta_{j, k-1}-\sqrt{j} \delta_{j, k+1}\right)
"""
if not hasattr(self, "first_derivative_matrix_fbr"):
semidiag = -np.sqrt(self.omega * np.arange(1, self.ngrid) / 2)
self.first_derivative_matrix_fbr = np.diag(semidiag, k=1) - np.diag(
semidiag, k=-1
)
return self.first_derivative_matrix_fbr
[docs]
def get_1st_derivative_matrix_dvr(self):
return super().get_1st_derivative_matrix_dvr()
[docs]
def get_2nd_derivative_matrix_fbr(self):
r"""Analytical Formulation
.. math::
D_{j k}^{(2)}=\
\frac{m\omega}{2}\left(\sqrt{(j-1)j}\delta_{j,k+2}-(2j+1)\delta_{j,k} + \sqrt{(j+2)(j+1)}\delta_{j,k-2}\right)
"""
if not hasattr(self, "second_derivative_matrix_fbr"):
diag = -self.omega / 2 * (2 * np.arange(self.ngrid) + 1)
semisemidiag = (
self.omega
/ 2
* np.sqrt(
np.arange(1, self.ngrid - 1) * np.arange(2, self.ngrid)
)
)
self.second_derivative_matrix_fbr = (
np.diag(diag)
+ np.diag(semisemidiag, k=2)
+ np.diag(semisemidiag, k=-2)
)
return self.second_derivative_matrix_fbr
[docs]
def get_2nd_derivative_matrix_dvr(self):
return super().get_2nd_derivative_matrix_dvr()
[docs]
def diagnalize_pos_rep_matrix(self):
"""Analytical formulation has not yet derived."""
return super().diagnalize_pos_rep_matrix()
[docs]
def get_ovi_CS_HO(
self, p: float, q: float, type: str = "DVR"
) -> np.ndarray:
"""Get overlap integral 1D-array between coherent state <p,q,ω,| and |HO(ω)>
Args:
p (float) : momentum of coherent state in mass-weighted a.u.
q (float) : position of coherent state in mass-weighted a.u.
type (str) : Whether "DVR" or "FBR". Default is "DVR".
Returns:
np.ndarray : overlap integral 1D-array between coherent state <p,q,ω,| and |HO(ω)>
"""
z = math.sqrt(self.omega * 0.5) * (q + 1j * p / self.omega)
expo = math.exp(-0.5 * abs(z) ** 2)
zp = 1.0 + 0.0j
ints = np.zeros(self.nprim, dtype=complex)
for v in range(self.nprim):
ints[v] = FACTORIAL_SQRT_INV[v] * zp * expo
zp *= z
if type == "DVR":
return np.conjugate(self.get_unitary().T) @ ints
elif type == "FBR":
return ints
else:
raise ValueError(
f"type argument must be 'DVR' or 'FBR', not {type}"
)
def _normal_factor(self, n: int) -> float:
r"""
.. math::
A_n = \frac{1}{\sqrt{n! 2^n}} \left(\frac{\omega_i}{\pi}\right)^{\frac{1}{4}}
Args:
n (int) : index ``(0 <= n < ngrid)``
Returns:
float : :math:`A_n`
"""
if 0 <= n < self.ngrid:
return (
1
/ math.sqrt(math.factorial(n) * pow(2, n))
* pow(self.omega / math.pi, 0.25)
)
else:
raise ValueError(f"{n} is not in [0,{self.ngrid})")
def _hermite_pol(self, n: int, q):
"""Hermite Polynomial
Args:
n (int) : index ``(0 <= n < ngrid)``
q (_ArrayLike0D) : mass-weighted coordinate array
Returns:
_ArrayLike0D : :math:`H_n(q)`
"""
q = q - self.q_eq
if 0 <= n < self.ngrid:
return hermite(n)(math.sqrt(self.omega) * q)
else:
raise ValueError(f"{n} is not in [0,{self.ngrid})")
def _gaussian(self, q):
r"""Gaussian :math:`\exp\left(- \frac{\omega q^2}{2}\right)`"""
q = q - self.q_eq
return np.exp(-self.omega * q * q / 2)
[docs]
class PrimBas_HO:
r""" The Harmonic Oscillator eigenfunction primitive basis.
This class holds information on Gauss Hermite type SPF basis functions.
The `n`-th primitive represents
Normalization factor :
.. math::
A_n = \frac{1}{\sqrt{n! 2^n}} \left(\frac{m\omega}{\pi\hbar}\right)^{\frac{1}{4}}
Dimensionless coordinate :
.. math::
\zeta = \sqrt{\frac{m\omega} {\hbar}}(x-a)
Primitive Function :
.. math::
\chi_n = A_n H_n(\zeta) \exp\left(- \frac{\zeta^2}{2}\right)
Args:
origin (float) : The center (equilibrium) dimensionless coordinate \
:math:`\sqrt{\frac{m\omega}{\hbar}} a` of Hermite polynomial.
freq_cm1 (float) : The frequency :math:`\omega` of Hermite polynomial. \
The unit is cm-1.
nprim (int) : The number (max order) of primitive basis on a certain SPF.
origin_is_dimless (bool, optional) : If True, given ``self.origin`` is \
dimensionless coordinate. Defaults to True.
Attributes:
origin (float) : The center (equilibrium) dimensionless coordinate \
:math:`\sqrt{\frac{m\omega}{\hbar}} a` of Hermite polynomial.
freq_cm1 (float) : The frequency :math:`\omega` of Hermite polynomial. \
The unit is cm-1.
nprim (int) : The number (max order) of primitive basis on a certain SPF.
freq_au (float) : ``self.freq_cm1`` in a.u.
origin_mwc (float) : Mass weighted coordinate ``self.origin`` in a.u.
"""
def __init__(
self,
origin: float,
freq_cm1: float,
nprim: int,
origin_is_dimless: bool = True,
):
self.freq_cm1 = freq_cm1
self.nprim = nprim
self.freq_au = freq_cm1 / _units.au_in_cm1
if origin_is_dimless:
self.origin_mwc = origin / math.sqrt(self.freq_au)
self.origin = origin
else:
"""origin is mass-weghted"""
self.origin_mwc = origin
self.origin = origin * math.sqrt(self.freq_au)
def __len__(self) -> int:
return self.nprim
[docs]
def todvr(self):
return HarmonicOscillator(
ngrid=self.nprim, omega=self.freq_cm1, q_eq=self.origin
)