diff --git a/doc/source/ct.rst b/doc/source/ct.rst index 33837c86..b105f511 100644 --- a/doc/source/ct.rst +++ b/doc/source/ct.rst @@ -1,13 +1,15 @@ Charge Transport ************************************* -charge diffusion simulation +Obtain mobility by Green-Kubo formula +=============================== +.. automodule:: renormalizer.transport.kubo + :members: + +Charge diffusion simulation dynamics =========================== -.. automodule:: renormalizer.transport.transport +.. automodule:: renormalizer.transport.dynamics :members: -obtain mobility by Kubo formula -=============================== -.. automodule:: renormalizer.transport.autocorr - :members: + diff --git a/example/transport.py b/example/dynamics.py similarity index 72% rename from example/transport.py rename to example/dynamics.py index 8da59ddd..1d963add 100644 --- a/example/transport.py +++ b/example/dynamics.py @@ -9,8 +9,8 @@ import yaml from renormalizer.model import load_from_dict -from renormalizer.transport import ChargeTransport -from renormalizer.utils import log, Quantity, EvolveConfig, EvolveMethod, RungeKutta, CompressConfig +from renormalizer.transport import ChargeDiffusionDynamics +from renormalizer.utils import log, EvolveConfig, EvolveMethod, CompressConfig logger = logging.getLogger(__name__) @@ -27,15 +27,15 @@ mol_list, temperature = load_from_dict(param, 3, False) compress_config = CompressConfig(max_bonddim=16) evolve_config = EvolveConfig(EvolveMethod.tdvp_ps, adaptive=True, guess_dt=2) - ct = ChargeTransport( + cdd = ChargeDiffusionDynamics( mol_list, temperature=temperature, compress_config=compress_config, evolve_config=evolve_config, rdm=False, ) - ct.dump_dir = param["output dir"] - ct.job_name = param["fname"] - ct.custom_dump_info["comment"] = param["comment"] - ct.evolve(param.get("evolve dt"), param.get("nsteps"), param.get("evolve time")) + cdd.dump_dir = param["output dir"] + cdd.job_name = param["fname"] + cdd.custom_dump_info["comment"] = param["comment"] + cdd.evolve(param.get("evolve dt"), param.get("nsteps"), param.get("evolve time")) # ct.evolve(evolve_dt, 100, param.get("evolve time")) diff --git a/example/fmo.py b/example/fmo.py index d42cd885..42c04dbe 100644 --- a/example/fmo.py +++ b/example/fmo.py @@ -3,7 +3,7 @@ from renormalizer.model import Phonon, Mol, MolList from renormalizer.utils import Quantity, EvolveConfig, CompressConfig, CompressCriteria, EvolveMethod from renormalizer.utils.constant import cm2au -from renormalizer.transport import ChargeTransport, InitElectron +from renormalizer.transport import ChargeDiffusionDynamics, InitElectron import numpy as np @@ -157,7 +157,7 @@ evolve_dt = 160 evolve_config = EvolveConfig(EvolveMethod.tdvp_ps, guess_dt=evolve_dt) compress_config = CompressConfig(CompressCriteria.fixed, max_bonddim=32) - ct = ChargeTransport(mol_list, evolve_config=evolve_config, compress_config=compress_config, init_electron=InitElectron.fc) + ct = ChargeDiffusionDynamics(mol_list, evolve_config=evolve_config, compress_config=compress_config, init_electron=InitElectron.fc) ct.dump_dir = "./" ct.job_name = 'fmo' ct.stop_at_edge = False diff --git a/example/run.sh b/example/run.sh index e89c66cc..e2b72f6a 100644 --- a/example/run.sh +++ b/example/run.sh @@ -1,11 +1,11 @@ export PYTHONPATH=../:PYTHONPATH code=0 -for python_args in fmo.py sbm.py h2o_qc.py "transport.py std.yaml" "transport_autocorr.py std.yaml"; do +for python_args in fmo.py sbm.py h2o_qc.py "dynamics.py std.yaml" "transport_kubo.py std.yaml"; do echo ============================$python_args============================= timeout 20s python $python_args exit_code=$? echo ============================$python_args============================= - # the time out exit code or normal exit code + # if not the time out exit code or normal exit code if [ $exit_code -ne 124 ] && [ $exit_code -ne 0 ]; then echo "The script failed with exit code $exit_code" >&2 code=1 diff --git a/example/transport_autocorr.py b/example/transport_kubo.py similarity index 77% rename from example/transport_autocorr.py rename to example/transport_kubo.py index 8111a502..4bd90148 100644 --- a/example/transport_autocorr.py +++ b/example/transport_kubo.py @@ -9,7 +9,7 @@ import yaml from renormalizer.model import load_from_dict -from renormalizer.transport import TransportAutoCorr +from renormalizer.transport import TransportKubo from renormalizer.utils import log, Quantity, EvolveConfig, EvolveMethod, RungeKutta, CompressConfig, BondDimDistri logger = logging.getLogger(__name__) @@ -28,9 +28,9 @@ compress_config = CompressConfig(threshold=1e-4) ievolve_config = EvolveConfig(adaptive=True, guess_dt=temperature.to_beta() / 1000j) evolve_config = EvolveConfig(adaptive=True, guess_dt=2) - ct = TransportAutoCorr(mol_list, temperature=temperature, ievolve_config=ievolve_config, - compress_config=compress_config, evolve_config=evolve_config, dump_dir=param["output dir"], - job_name=param["fname"] + "_autocorr") + ct = TransportKubo(mol_list, temperature=temperature, ievolve_config=ievolve_config, + compress_config=compress_config, evolve_config=evolve_config, dump_dir=param["output dir"], + job_name=param["fname"] + "_autocorr") # ct.latest_mps.compress_add = True ct.evolve(param.get("evolve dt"), param.get("nsteps"), param.get("evolve time")) # ct.evolve(evolve_dt, 100, param.get("evolve time")) diff --git a/renormalizer/model/mlist.py b/renormalizer/model/mlist.py index cb63372d..c28a059d 100644 --- a/renormalizer/model/mlist.py +++ b/renormalizer/model/mlist.py @@ -2,7 +2,7 @@ # Author: Jiajun Ren import copy -from collections import OrderedDict +from collections import OrderedDict, defaultdict from typing import List, Union, Dict from enum import Enum @@ -489,6 +489,7 @@ def get_pure_dmrg_mollist(self): def mol_list2_para(self, formula="vibronic"): mol_list2 = MolList2.MolList_to_MolList2(self, formula) + self.multi_dof_basis = self.scheme == 4 self.order = mol_list2.order self.basis = mol_list2.basis self.model = mol_list2.model @@ -510,6 +511,7 @@ def rewrite_model(self, model, model_translator): mol_list_new.basis = self.basis mol_list_new.model = model mol_list_new.model_translator = model_translator + mol_list_new.multi_dof_basis = self.multi_dof_basis return mol_list_new def __getitem__(self, idx): @@ -553,3 +555,33 @@ def load_from_dict(param, scheme, lam: bool): j_constant, scheme=scheme ) return mol_list, temperature + + +def vibronic_to_general(model): + new_model = defaultdict(list) + for e_k, e_v in model.items(): + for kk, vv in e_v.items(): + # it's difficult to rename `kk` because sometimes it's related to + # phonons sometimes it's `"J"` + if e_k == "I": + # operators without electronic dof, simple phonon + new_model[kk] = vv + else: + # operators with electronic dof + assert isinstance(e_k, tuple) and len(e_k) == 2 + if e_k[0] == e_k[1]: + # diagonal + new_e_k = (e_k[0],) + e_op = (Op(r"a^\dagger a", 0),) + else: + # off-diagonal + new_e_k = e_k + e_op = (Op(r"a^\dagger", 1), Op("a", -1)) + if kk == "J": + new_model[new_e_k] = [e_op + (vv,)] + else: + for term in vv: + new_key = new_e_k + kk + new_value = e_op + term + new_model[new_key].append(new_value) + return new_model \ No newline at end of file diff --git a/renormalizer/mps/mps.py b/renormalizer/mps/mps.py index 22f8c148..b9041e8f 100644 --- a/renormalizer/mps/mps.py +++ b/renormalizer/mps/mps.py @@ -1808,9 +1808,8 @@ def _mu_regularize(s, epsilon=1e-10): class BraKetPair: def __init__(self, bra_mps, ket_mps, mpo=None): - # do copy so that clear_memory won't clear previous braket - self.bra_mps = bra_mps.copy() - self.ket_mps = ket_mps.copy() + self.bra_mps = bra_mps + self.ket_mps = ket_mps self.mpo = mpo self.ft = self.calc_ft() @@ -1818,7 +1817,7 @@ def calc_ft(self): if self.mpo is None: dot = self.bra_mps.conj().dot(self.ket_mps) else: - dot = self.bra_mps.conj().expectation(self.mpo, self.ket_mps) + dot = self.ket_mps.expectation(self.mpo, self.bra_mps.conj()) return complex( dot * np.conjugate(self.bra_mps.coeff) * self.ket_mps.coeff @@ -1833,7 +1832,6 @@ def __str__(self): ft_str = "%g" % self.ft return "bra: %s, ket: %s, ft: %s" % (self.bra_mps, self.ket_mps, ft_str) - # todo: not used? def __iter__(self): return iter((self.bra_mps, self.ket_mps)) diff --git a/renormalizer/tests/parameter_exact.py b/renormalizer/tests/parameter_exact.py index 3987f0a9..fde46ced 100644 --- a/renormalizer/tests/parameter_exact.py +++ b/renormalizer/tests/parameter_exact.py @@ -4,7 +4,7 @@ from renormalizer.mps import Mpo from renormalizer.model import Phonon, Mol, MolList from renormalizer.utils import Quantity -from renormalizer.utils.qutip_utils import get_clist, get_blist, get_hamiltonian, get_gs +from renormalizer.utils.qutip_utils import get_clist, get_blist, get_holstein_hamiltonian, get_gs OMEGA = 1 @@ -21,6 +21,6 @@ qutip_blist = get_blist(N_SITES, N_LEVELS) G = np.sqrt(DISPLACEMENT**2 * OMEGA / 2) -qutip_h = get_hamiltonian(N_SITES, J, OMEGA, G, qutip_clist, qutip_blist) +qutip_h = get_holstein_hamiltonian(N_SITES, J, OMEGA, G, qutip_clist, qutip_blist) qutip_gs = get_gs(N_SITES, N_LEVELS) \ No newline at end of file diff --git a/renormalizer/transport/__init__.py b/renormalizer/transport/__init__.py index 7d826c24..0ccc3888 100644 --- a/renormalizer/transport/__init__.py +++ b/renormalizer/transport/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- # Author: Jiajun Ren -from renormalizer.transport.transport import ChargeTransport, InitElectron, EDGE_THRESHOLD -from renormalizer.transport.autocorr import TransportAutoCorr +from renormalizer.transport.kubo import TransportKubo +from renormalizer.transport.dynamics import ChargeDiffusionDynamics, InitElectron, EDGE_THRESHOLD diff --git a/renormalizer/transport/autocorr.py b/renormalizer/transport/autocorr.py deleted file mode 100644 index b07023b5..00000000 --- a/renormalizer/transport/autocorr.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- - -import logging - -import scipy.integrate - -from renormalizer.mps import MpDm, Mpo, BraKetPair, ThermalProp, load_thermal_state -from renormalizer.mps.backend import np -from renormalizer.mps.lib import compressed_sum -from renormalizer.utils.constant import mobility2au -from renormalizer.utils import TdMpsJob, Quantity, EvolveConfig, CompressConfig, Op -from renormalizer.model import MolList, MolList2, ModelTranslator -from renormalizer.property import Property - -logger = logging.getLogger(__name__) - - -class TransportAutoCorr(TdMpsJob): - - def __init__(self, mol_list, temperature: Quantity, j_oper: Mpo =None, - insteps: int=1, ievolve_config=None, compress_config=None, - evolve_config=None, dump_dir: str=None, job_name: str=None, properties: Property = None,): - self.mol_list = mol_list - self.h_mpo = Mpo(mol_list) - logger.info(f"Bond dim of h_mpo: {self.h_mpo.bond_dims}") - if j_oper is None: - self.j_oper = self._construct_flux_operator() - else: - self.j_oper = j_oper - self.temperature = temperature - - # imaginary time evolution config - if ievolve_config is None: - self.ievolve_config = EvolveConfig() - if insteps is None: - self.ievolve_config.adaptive = True - # start from a small step - self.ievolve_config.guess_dt = temperature.to_beta() / 1e5j - insteps = 1 - else: - self.ievolve_config = ievolve_config - self.insteps = insteps - - if compress_config is None: - logger.debug("using default compress config") - self.compress_config = CompressConfig() - else: - self.compress_config = compress_config - - self.properties = properties - self._auto_corr = [] - super().__init__(evolve_config=evolve_config, dump_dir=dump_dir, - job_name=job_name) - - - def _construct_flux_operator(self): - # construct flux operator - logger.info("constructing 1-d Holstein model flux operator ") - - if isinstance(self.mol_list, MolList): - if self.mol_list.periodic: - itera = range(len(self.mol_list)) - else: - itera = range(len(self.mol_list)-1) - j_list = [] - for i in itera: - conne = (i+1) % len(self.mol_list) # connect site index - j1 = Mpo.intersite(self.mol_list, {i:r"a", conne:r"a^\dagger"}, {}, - Quantity(self.mol_list.j_matrix[i, conne])) - j1.compress_config.threshold = 1e-8 - j2 = j1.conj_trans().scale(-1) - j_list.extend([j1, j2]) - j_oper = compressed_sum(j_list, batchsize=10) - - elif isinstance(self.mol_list, MolList2): - - e_nsite = self.mol_list.n_edofs - model = {} - for i in range(e_nsite): - conne = (i+1) % e_nsite # connect site index - model[(f"e_{i}", f"e_{conne}")] = [(Op(r"a^\dagger",1), Op("a", -1), - -self.mol_list.j_matrix[i, conne]), (Op(r"a",-1), Op(r"a^\dagger", 1), - self.mol_list.j_matrix[conne, i])] - j_oper = Mpo.general_mpo(self.mol_list, model=model, - model_translator=ModelTranslator.general_model) - else: - assert False - logger.debug(f"flux operator bond dim: {j_oper.bond_dims}") - - return j_oper - - def init_mps(self): - # first try to load - if self._defined_output_path: - mpdm = load_thermal_state(self.mol_list, self._thermal_dump_path) - else: - mpdm = None - # then try to calculate - if mpdm is None: - i_mpdm = MpDm.max_entangled_ex(self.mol_list) - i_mpdm.compress_config = self.compress_config - if self.job_name is None: - job_name = None - else: - job_name = self.job_name + "_thermal_prop" - tp = ThermalProp(i_mpdm, self.h_mpo, evolve_config=self.ievolve_config, dump_dir=self.dump_dir, job_name=job_name) - # only propagate half beta - tp.evolve(None, self.insteps, self.temperature.to_beta() / 2j) - mpdm = tp.latest_mps - if self._defined_output_path: - mpdm.dump(self._thermal_dump_path) - mpdm.compress_config = self.compress_config - e = mpdm.expectation(self.h_mpo) - self.h_mpo = Mpo(self.mol_list, offset=Quantity(e)) - mpdm.evolve_config = self.evolve_config - ket_mpdm = self.j_oper.contract(mpdm).canonical_normalize() - bra_mpdm = mpdm.copy() - return BraKetPair(bra_mpdm, ket_mpdm, self.j_oper) - - def process_mps(self, mps): - self._auto_corr.append(mps.ft) - - # calculate other properties defined in Property - if self.properties is not None: - self.properties.calc_properties_braketpair(mps) - - def evolve_single_step(self, evolve_dt): - prev_bra_mpdm, prev_ket_mpdm = self.latest_mps - latest_ket_mpdm = prev_ket_mpdm.evolve(self.h_mpo, evolve_dt) - latest_bra_mpdm = prev_bra_mpdm.evolve(self.h_mpo, evolve_dt) - return BraKetPair(latest_bra_mpdm, latest_ket_mpdm, self.j_oper) - - def stop_evolve_criteria(self): - corr = self.auto_corr - if len(corr) < 10: - return False - last_corr = corr[-10:] - first_corr = corr[0] - return np.abs(last_corr.mean()) < 1e-5 * np.abs(first_corr) and last_corr.std() < 1e-5 * np.abs(first_corr) - - @property - def auto_corr(self): - return np.array(self._auto_corr) - - def get_dump_dict(self): - dump_dict = dict() - dump_dict["mol list"] = self.mol_list.to_dict() - dump_dict["temperature"] = self.temperature.as_au() - dump_dict["time series"] = self.evolve_times - dump_dict["auto correlation"] = self.auto_corr - dump_dict["mobility"] = self.calc_mobility()[1] - if self.properties is not None: - for prop_str in self.properties.prop_res.keys(): - dump_dict[prop_str] = self.properties.prop_res[prop_str] - - return dump_dict - - def calc_mobility(self): - time_series = self.evolve_times - corr_real = self.auto_corr.real - inte = scipy.integrate.trapz(corr_real, time_series) - mobility_in_au = inte / self.temperature.as_au() - mobility = mobility_in_au / mobility2au - return mobility_in_au, mobility diff --git a/renormalizer/transport/transport.py b/renormalizer/transport/dynamics.py similarity index 92% rename from renormalizer/transport/transport.py rename to renormalizer/transport/dynamics.py index 2810641c..137fec5c 100644 --- a/renormalizer/transport/transport.py +++ b/renormalizer/transport/dynamics.py @@ -29,17 +29,19 @@ class InitElectron(Enum): relaxed = "analytically relaxed phonon(s)" -class ChargeTransport(TdMpsJob): +class ChargeDiffusionDynamics(TdMpsJob): r""" - Simulate charge diffusion by TD-DMRG. + Simulate charge diffusion dynamics by TD-DMRG. It is possible to obtain mobility from the simulation, + but care must be taken to ensure that mean square displacement grows linearly with time. Args: - mol_list (:class:`~renormalizer.model.MolList`): system information. - temperature (:class:`~renormalizer.utils.Quantity`): simulation temperature. Default is zero temperature. - compress_config (:class:`~renormalizer.utils.CompressConfig`): config when compressing MPS. - evolve_config (:class:`~renormalizer.utils.EvolveConfig`): config when evolving MPS. + mol_list (:class:`~renormalizer.model.mlist.MolList`): system information. + temperature (:class:`~renormalizer.utils.quantity.Quantity`): simulation temperature. Default is zero temperature. + compress_config (:class:`~renormalizer.utils.configs.CompressConfig`): config when compressing MPS. + evolve_config (:class:`~renormalizer.utils.configs.EvolveConfig`): config when evolving MPS. stop_at_edge (bool): whether stop when charge has diffused to the boundary of the system. Default is ``True``. - init_electron (:class:`~renormalizer.utils.InitElectron`): the method to prepare the initial state. + init_electron (:class:`~renormalizer.transport.transport.InitElectron`): + the method to prepare the initial state. rdm (bool): whether calculate reduced density matrix and k-space representation for the electron. Default is ``False`` because usually the calculation is time consuming. Using scheme 4 might partly solve the problem. @@ -113,8 +115,8 @@ def __init__( # entropy at each bond self.bond_vn_entropy_array = [] self.coherent_length_array = [] - super(ChargeTransport, self).__init__(evolve_config=evolve_config, - dump_dir=dump_dir, job_name=job_name) + super(ChargeDiffusionDynamics, self).__init__(evolve_config=evolve_config, + dump_dir=dump_dir, job_name=job_name) assert self.mpo is not None self.elocalex_arrays = [] @@ -271,7 +273,7 @@ def get_dump_dict(self): dump_dict["time series"] = list(self.evolve_times) return dump_dict - def is_similar(self, other: "ChargeTransport", rtol=1e-3): + def is_similar(self, other: "ChargeDiffusionDynamics", rtol=1e-3): all_close_with_tol = partial(np.allclose, rtol=rtol, atol=1e-3) if len(self.evolve_times) != len(other.evolve_times): return False diff --git a/renormalizer/transport/kubo.py b/renormalizer/transport/kubo.py new file mode 100644 index 00000000..f1fc89c9 --- /dev/null +++ b/renormalizer/transport/kubo.py @@ -0,0 +1,361 @@ +# -*- coding: utf-8 -*- + +import logging + +import scipy.integrate + +from renormalizer.mps import MpDm, Mpo, BraKetPair, ThermalProp, load_thermal_state +from renormalizer.mps.backend import np +from renormalizer.utils.constant import mobility2au +from renormalizer.utils import TdMpsJob, Quantity, EvolveConfig, CompressConfig +from renormalizer.model import MolList, ModelTranslator +from renormalizer.model.mlist import vibronic_to_general +from renormalizer.property import Property + +logger = logging.getLogger(__name__) + + +class TransportKubo(TdMpsJob): + r""" + Calculate mobility via Green-Kubo formula: + + .. math:: + \mu = \frac{1}{k_B T} \int_0^\infty dt \langle \hat j (t) \hat j(0) \rangle + = \frac{1}{k_B T} \int_0^\infty dt C(t) + where + + .. math:: + \hat j = -\frac{i}{\hbar}[\hat P, \hat H] + and :math:`\hat P = e_0 \sum_m R_m a^\dagger_m a_m` is the polarization operator. + + .. note:: + Although in principle :math:`\hat H` can take any form, only Holstein-Peierls model are well tested. + + More explicitly, :math:`C(t)` has the form: + + .. math:: + C(t) = \textrm{Tr}\{\rho(T) e^{i \hat H t} \hat j(0) e^{- i \hat H t} \hat j (0)\} + where we have assumed :math:`\rho(T)` is normalized + (i.e. it is divided by the partition function :math:`\textrm{Tr}\{\rho(T)\}`). + + In terms of practical implementation, it is ideal if :math:`\rho(T)` is split into two parts + to (hopefully) speed up calculation and minimize time evolution error: + + .. math:: + \begin{align} + C(t) & = \textrm{Tr}\{\rho(T) e^{i \hat H t} \hat j(0) e^{- i \hat H t} \hat j(0)\} \\ + & = \textrm{Tr}\{e^{-\beta \hat H} e^{i \hat H t} \hat j(0) e^{- i \hat H t} \hat j(0)\} \\ + & = \textrm{Tr}\{e^{-\beta \hat H / 2} e^{i \hat H t} \hat j(0) e^{- i \hat H t} \hat j(0) e^{-\beta \hat H / 2}\} + \end{align} + + In this class, imaginary time propagation from infinite temperature to :math:`\beta/2` is firstly carried out + to obtain :math:`e^{-\beta \hat H / 2}`, and then real time propagation is carried out for :math:`e^{-\beta \hat H / 2}` + and :math:`\hat J(0) e^{-\beta \hat H / 2}` respectively to obtain :math:`e^{-\beta \hat H / 2} e^{i \hat H t}` + and :math:`e^{- i \hat H t} \hat j(0) e^{-\beta \hat H / 2}`. The correlation function at time :math:`t` can thus + be calculated via expectation calculation. + + .. note:: + Although the class is able to carry out imaginary time propagation, in practice for large scale calculation + it is usually preferable to carry out imaginary time propagation in another job and load the dumped initial + state directly in this class. + + Args: + mol_list (:class:`~renormalizer.model.mlist.MolList`): system information. + temperature (:class:`~renormalizer.utils.quantity.Quantity`): simulation temperature. + Zero temperature is not supported. + distance_matrix (np.ndarray): two-dimensional array :math:`D_{ij} = P_i - P_j` representing + distance between the :math:`i` th electronic degree of freedom and the :math:`j` th degree of freedom. + The parameter takes the roll of :math:`\hat P` and can better handle periodic boundary condition. + The default value is ``None`` in which case the distance matrix is constructed assuming the system + is a one-dimensional chain. + + .. note:: + The construction of the matrix should be taken with great care if periodic boundary condition + is applied. Take a one-dimensional chain as an example, the distance between the leftmost site + and the rightmost site is :math:`\pm R` where :math:`R` is the intersite distance, + rather than :math:`\pm (N-1)R` where :math:`N` is the total number of electronic degrees of freedom. + insteps (int): steps for imaginary time propagation. + ievolve_config (:class:`~renormalizer.utils.configs.EvolveConfig`): config when carrying out imaginary time propagation. + compress_config (:class:`~renormalizer.utils.configs.CompressConfig`): config when compressing MPS. + Note that even if TDVP based methods are chosen for time evolution, compression is still necessary + when :math:`\hat j` is applied on :math:`\rho^{\frac{1}{2}}`. + evolve_config (:class:`~renormalizer.utils.configs.EvolveConfig`): config when carrying out real time propagation. + dump_dir (str): the directory for logging and numerical result output. + Also the directory from which to load previous thermal propagated initial state (if exists). + job_name (str): the name of the calculation job which determines the file name of the logging and numerical result output. + For thermal propagated initial state input/output the file name is appended with ``"_impdm.npz"``. + properties (:class:`~renormalizer.property.property.Property`): other properties to calculate during real time evolution. + Currently only supports Holstein model. + """ + def __init__(self, mol_list, temperature: Quantity, distance_matrix: np.ndarray = None, + insteps: int=1, ievolve_config=None, compress_config=None, + evolve_config=None, dump_dir: str=None, job_name: str=None, properties: Property = None): + self.mol_list = mol_list + self.distance_matrix = distance_matrix + self.h_mpo = Mpo(mol_list) + logger.info(f"Bond dim of h_mpo: {self.h_mpo.bond_dims}") + self._construct_current_operator() + if temperature == 0: + raise ValueError("Can't set temperature to 0.") + self.temperature = temperature + + # imaginary time evolution config + if ievolve_config is None: + self.ievolve_config = EvolveConfig() + if insteps is None: + self.ievolve_config.adaptive = True + # start from a small step + self.ievolve_config.guess_dt = temperature.to_beta() / 1e5j + insteps = 1 + else: + self.ievolve_config = ievolve_config + self.insteps = insteps + + if compress_config is None: + logger.debug("using default compress config") + self.compress_config = CompressConfig() + else: + self.compress_config = compress_config + + self.properties = properties + self._auto_corr = [] + self._auto_corr_deomposition = [] + super().__init__(evolve_config=evolve_config, dump_dir=dump_dir, + job_name=job_name) + + def _construct_current_operator(self): + # Construct current operator. The operator is taken to be real as an optimization. + logger.info("constructing current operator ") + + if isinstance(self.mol_list, MolList): + self.mol_list.mol_list2_para("general") + mol_num = self.mol_list.mol_num + model = self.mol_list.model + else: + mol_num = self.mol_list.n_edofs + if self.mol_list.model_translator == ModelTranslator.general_model: + model = self.mol_list.model + elif self.mol_list.model_translator == ModelTranslator.vibronic_model: + model = vibronic_to_general(self.mol_list.model) + else: + raise ValueError(f"Unsupported model {self.mol_list.model_translator}") + if self.distance_matrix is None: + logger.info("Constructing distance matrix based on a periodic one-dimension chain.") + self.distance_matrix = np.arange(mol_num).reshape(-1, 1) - np.arange(mol_num).reshape(1, -1) + self.distance_matrix[0][-1] = 1 + self.distance_matrix[-1][0] = -1 + + # current caused by pure eletronic coupling + holstein_current_model = {} + # current related to phonons + peierls_current_model = {} + # checkout that things like r"a^\dagger_0 a_1" are not present + for terms in model.values(): + for term in terms: + for op in term[:-1]: + if "_" in op.symbol: + raise ValueError(f"{op} not supported.") + # loop through the Hamiltonian to construct current operator + for dof_names, terms in model.items(): + # find out terms that contains two electron operators + # idx of the dof for the model + dof_idx1 = dof_idx2 = None + # idx of the dof in `terms` + term_idx1 = term_idx2 = None + for term_idx, dof_name in enumerate(dof_names): + e_or_ph, dof_idx = dof_name.split("_") + dof_idx = int(dof_idx) + if e_or_ph == "e": + if dof_idx1 is None: + dof_idx1 = dof_idx + term_idx1 = term_idx + elif dof_idx2 is None: + dof_idx2 = dof_idx + term_idx2 = term_idx + else: + raise ValueError(f"The model contains three-electron (or more complex) operator for {dof_names}") + del term_idx, dof_name + # two electron operators not found. Not relevant to the current operator + if dof_idx1 is None or dof_idx2 is None: + continue + # two electron operators found. Relevant to the current operator + # at most 3 dofs are involved. More complex cases are probably supported but not tested + if len(dof_names) not in (2, 3): + raise NotImplementedError("Complex vibration potential not implemented") + # Holstein terms + if len(dof_names) == 2: + current_model = holstein_current_model + # Peierls terms + else: + current_model = peierls_current_model + current_model[dof_names] = [] + # translate every term in the Hamiltonian into terms in the current operator + for term in terms: + if len(dof_names) == 3: + # total term idx should be 0 + 1 + 2 = 3 + phonon_term_idx = 3 - term_idx1 - term_idx2 + assert term[phonon_term_idx].symbol in (r"b^\dagger + b", "x") + symbol1, symbol2 = term[term_idx1].symbol, term[term_idx2].symbol + if not {symbol1, symbol2} == {r"a^\dagger", "a"}: + raise ValueError(f"Unknown symbol: {symbol1}, {symbol2}") + if symbol1 == r"a^\dagger": + factor = self.distance_matrix[dof_idx1][dof_idx2] + else: + factor = self.distance_matrix[dof_idx2][dof_idx1] + current_term = list(term[:-1]) + [factor * term[-1]] + current_model[dof_names].append(tuple(current_term)) + + assert len(holstein_current_model) != 0 + self.j_oper = Mpo.general_mpo(self.mol_list, model=holstein_current_model, model_translator=ModelTranslator.general_model) + logger.info(f"current operator bond dim: {self.j_oper.bond_dims}") + if len(peierls_current_model) != 0: + self.j_oper2 = Mpo.general_mpo(self.mol_list, model=peierls_current_model, model_translator=ModelTranslator.general_model) + logger.info(f"Peierls coupling induced current operator bond dim: {self.j_oper2.bond_dims}") + else: + self.j_oper2 = None + + def init_mps(self): + # first try to load + if self._defined_output_path: + mpdm = load_thermal_state(self.mol_list, self._thermal_dump_path) + else: + mpdm = None + # then try to calculate + if mpdm is None: + i_mpdm = MpDm.max_entangled_ex(self.mol_list) + i_mpdm.compress_config = self.compress_config + if self.job_name is None: + job_name = None + else: + job_name = self.job_name + "_thermal_prop" + tp = ThermalProp(i_mpdm, self.h_mpo, evolve_config=self.ievolve_config, dump_dir=self.dump_dir, job_name=job_name) + # only propagate half beta + tp.evolve(None, self.insteps, self.temperature.to_beta() / 2j) + mpdm = tp.latest_mps + if self._defined_output_path: + mpdm.dump(self._thermal_dump_path) + mpdm.compress_config = self.compress_config + e = mpdm.expectation(self.h_mpo) + self.h_mpo = Mpo(self.mol_list, offset=Quantity(e)) + mpdm.evolve_config = self.evolve_config + ket_mpdm = self.j_oper.contract(mpdm).canonical_normalize() + bra_mpdm = mpdm.copy() + if self.j_oper2 is None: + return BraKetPair(bra_mpdm, ket_mpdm, self.j_oper) + else: + ket_mpdm2 = self.j_oper2.contract(mpdm).canonical_normalize() + return BraKetPair(bra_mpdm, ket_mpdm, self.j_oper), BraKetPair(bra_mpdm, ket_mpdm2, self.j_oper2) + + def process_mps(self, mps): + # add the negative sign because `self.j_oper` is taken to be real + if self.j_oper2 is None: + self._auto_corr.append(-mps.ft) + # calculate other properties defined in Property + if self.properties is not None: + self.properties.calc_properties_braketpair(mps) + else: + (bra_mpdm, ket_mpdm), (bra_mpdm, ket_mpdm2) = mps + # + ft1 = -BraKetPair(bra_mpdm, ket_mpdm, self.j_oper).ft + # + ft2 = -BraKetPair(bra_mpdm, ket_mpdm2, self.j_oper).ft + # + ft3 = -BraKetPair(bra_mpdm, ket_mpdm, self.j_oper2).ft + # + ft4 = -BraKetPair(bra_mpdm, ket_mpdm2, self.j_oper2).ft + self._auto_corr.append(ft1 + ft2 + ft3 + ft4) + self._auto_corr_deomposition.append([ft1, ft2, ft3, ft4]) + + + + def evolve_single_step(self, evolve_dt): + if self.j_oper2 is None: + prev_bra_mpdm, prev_ket_mpdm = self.latest_mps + prev_ket_mpdm2 = None + else: + (prev_bra_mpdm, prev_ket_mpdm), (prev_bra_mpdm, prev_ket_mpdm2) = self.latest_mps + + latest_ket_mpdm = prev_ket_mpdm.evolve(self.h_mpo, evolve_dt) + latest_bra_mpdm = prev_bra_mpdm.evolve(self.h_mpo, evolve_dt) + if self.j_oper2 is None: + return BraKetPair(latest_bra_mpdm, latest_ket_mpdm, self.j_oper) + else: + latest_ket_mpdm2 = prev_ket_mpdm2.evolve(self.h_mpo, evolve_dt) + return BraKetPair(latest_bra_mpdm, latest_ket_mpdm, self.j_oper), \ + BraKetPair(latest_bra_mpdm, latest_ket_mpdm2, self.j_oper2) + + def stop_evolve_criteria(self): + corr = self.auto_corr + if len(corr) < 10: + return False + last_corr = corr[-10:] + first_corr = corr[0] + return np.abs(last_corr.mean()) < 1e-5 * np.abs(first_corr) and last_corr.std() < 1e-5 * np.abs(first_corr) + + @property + def auto_corr(self) -> np.ndarray: + """ + Correlation function :math:`C(t)`. + + :returns: 1-d numpy array containing the correlation function evaluated at each time step. + """ + return np.array(self._auto_corr) + + @property + def auto_corr_decomposition(self) -> np.ndarray: + r""" + Correlation function :math:`C(t)` decomposed into contributions from different parts + of the current operator. Generally, the current operator can be split into two parts: + current without phonon assistance and current with phonon assistance. + For example, if the Holstein-Peierls model is considered: + + .. math:: + \hat H = \sum_{mn} [\epsilon_{mn} + \sum_\lambda \hbar g_{mn\lambda} \omega_\lambda + (b^\dagger_\lambda + b_\lambda) ] a^\dagger_m a_n + + \sum_\lambda \hbar \omega_\lambda b^\dagger_\lambda b_\lambda + Then current operator without phonon assistance is defined as: + + .. math:: + \hat j_1 = \frac{e_0}{i\hbar} \sum_{mn} (R_m - R_n) \epsilon_{mn} a^\dagger_m a_n + and the current operator with phonon assistance is defined as: + + .. math:: + \hat j_2 = \frac{e_0}{i\hbar} \sum_{mn} (R_m - R_n) \hbar g_{mn\lambda} \omega_\lambda + (b^\dagger_\lambda + b_\lambda) a^\dagger_m a_n + With :math:`\hat j = \hat j_1 + \hat j_2`, the correlation function can be + decomposed into four parts: + + .. math:: + \begin{align} + C(t) & = \langle \hat j(t) \hat j(0) \rangle \\ + & = \langle ( \hat j_1(t) + \hat j_2(t) ) (\hat j_1(0) + \hat j_2(0) ) \rangle \\ + & = \langle \hat j_1(t) \hat j_1(0) \rangle + \langle \hat j_1(t) \hat j_2(0) \rangle + + \langle \hat j_2(t) \hat j_1(0) \rangle + \langle \hat j_2(t) \hat j_2(0) \rangle + \end{align} + + :return: :math:`n \times 4` array for the decomposed correlation function defined as above + where :math:`n` is the number of time steps. + """ + return np.array(self._auto_corr_deomposition) + + def get_dump_dict(self): + dump_dict = dict() + dump_dict["mol list"] = self.mol_list.to_dict() + dump_dict["temperature"] = self.temperature.as_au() + dump_dict["time series"] = self.evolve_times + dump_dict["auto correlation"] = self.auto_corr + dump_dict["auto correlation decomposition"] = self.auto_corr_decomposition + dump_dict["mobility"] = self.calc_mobility()[1] + if self.properties is not None: + for prop_str in self.properties.prop_res.keys(): + dump_dict[prop_str] = self.properties.prop_res[prop_str] + + return dump_dict + + def calc_mobility(self): + time_series = self.evolve_times + corr_real = self.auto_corr.real + inte = scipy.integrate.trapz(corr_real, time_series) + mobility_in_au = inte / self.temperature.as_au() + mobility = mobility_in_au / mobility2au + return mobility_in_au, mobility diff --git a/renormalizer/transport/tests/test_autocorr.py b/renormalizer/transport/tests/test_autocorr.py deleted file mode 100644 index e366fc94..00000000 --- a/renormalizer/transport/tests/test_autocorr.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np -import qutip -import pytest - -from renormalizer.model import Phonon, Mol, MolList, MolList2 -from renormalizer.transport.autocorr import TransportAutoCorr -from renormalizer.utils import Quantity, CompressConfig, EvolveConfig, EvolveMethod, CompressCriteria -from renormalizer.utils.qutip_utils import get_clist, get_blist, get_hamiltonian, get_qnidx - - -@pytest.mark.parametrize("scheme", ( - 3, - 4, -)) -@pytest.mark.parametrize("mollist2", ( - True, - False, -)) -def test_autocorr(scheme, mollist2): - ph = Phonon.simple_phonon(Quantity(1), Quantity(1), 2) - mol = Mol(Quantity(0), [ph]) - mol_list1 = MolList([mol] * 5, Quantity(1), scheme) - if mollist2: - mol_list = MolList2.MolList_to_MolList2(mol_list1) - else: - mol_list = mol_list1 - temperature = Quantity(50000, 'K') - compress_config = CompressConfig(CompressCriteria.fixed, max_bonddim=24) - evolve_config = EvolveConfig(EvolveMethod.tdvp_ps, adaptive=True, guess_dt=0.5, adaptive_rtol=1e-3) - ievolve_config = EvolveConfig(EvolveMethod.tdvp_ps, adaptive=True, guess_dt=-0.1j) - ac = TransportAutoCorr(mol_list, temperature, compress_config=compress_config, ievolve_config=ievolve_config, evolve_config=evolve_config) - ac.evolve(nsteps=5, evolve_time=5) - corr_real = ac.auto_corr.real - exact_real = get_exact_autocorr(mol_list1, temperature, ac.evolve_times_array).real - atol = 1e-2 - # direct comparison may fail because of different sign - assert np.allclose(corr_real, exact_real, atol=atol) or np.allclose(corr_real, -exact_real, atol=atol) - - -def get_exact_autocorr(mol_list, temperature, time_series): - - nsites = len(mol_list) - J = mol_list.j_constant.as_au() - ph = mol_list[0].dmrg_phs[0] - ph_levels = ph.n_phys_dim - omega = ph.omega[0] - g = - ph.coupling_constant - clist = get_clist(nsites, ph_levels) - blist = get_blist(nsites, ph_levels) - - qn_idx = get_qnidx(ph_levels, nsites) - H = get_hamiltonian(nsites, J, omega, g, clist, blist).extract_states(qn_idx) - init_state = (-temperature.to_beta() * H).expm().unit() - - terms = [] - for i in range(nsites - 1): - terms.append(clist[i].dag() * clist[i + 1]) - terms.append(-clist[i] * clist[i + 1].dag()) - j_oper = sum(terms).extract_states(qn_idx) - - corr = qutip.correlation(H, init_state, [0], time_series, [], j_oper, j_oper)[0] - return corr - - - diff --git a/renormalizer/transport/tests/test_transport.py b/renormalizer/transport/tests/test_dynamics.py similarity index 91% rename from renormalizer/transport/tests/test_transport.py rename to renormalizer/transport/tests/test_dynamics.py index 83b23021..f3430f7c 100644 --- a/renormalizer/transport/tests/test_transport.py +++ b/renormalizer/transport/tests/test_dynamics.py @@ -9,7 +9,7 @@ from renormalizer.model import Phonon, Mol, MolList from renormalizer.mps import Mps, Mpo, ThermalProp, MpDm, MpDmFull from renormalizer.mps.solver import optimize_mps -from renormalizer.transport import ChargeTransport +from renormalizer.transport import ChargeDiffusionDynamics from renormalizer.utils import Quantity from renormalizer.utils import ( BondDimDistri, @@ -33,7 +33,7 @@ def test_zt_init_state(): mpo = Mpo(mol_list) mps = Mps.random(mol_list, 1, 10) optimize_mps(mps, mpo) - ct = ChargeTransport(mol_list) + ct = ChargeDiffusionDynamics(mol_list) assert mps.angle(ct.latest_mps) == pytest.approx(1) @@ -45,7 +45,7 @@ def test_ft_init_state(): init_mpdm = MpDm.max_entangled_ex(mol_list) tp = ThermalProp(init_mpdm, mpo, space="EX", exact=True) tp.evolve(nsteps=20, evolve_time=temperature.to_beta() / 2j) - ct = ChargeTransport(mol_list, temperature=temperature) + ct = ChargeDiffusionDynamics(mol_list, temperature=temperature) tp_mpdm = MpDmFull.from_mpdm(tp.latest_mps) ct_mpdm = MpDmFull.from_mpdm(ct.latest_mps) assert tp_mpdm.angle(ct_mpdm) == pytest.approx(1) @@ -61,7 +61,7 @@ def test_ft_init_state(): @pytest.mark.parametrize("scheme", (3, 4)) def test_bandlimit_zero_t(method, evolve_dt, nsteps, rtol, scheme): evolve_config = EvolveConfig(method) - ct = ChargeTransport( + ct = ChargeDiffusionDynamics( band_limit_mol_list.switch_scheme(scheme), evolve_config=evolve_config, ) @@ -76,7 +76,7 @@ def test_bandlimit_zero_t(method, evolve_dt, nsteps, rtol, scheme): def test_adaptive_zero_t(method): np.random.seed(0) evolve_config = EvolveConfig(method=method, guess_dt=0.1, adaptive=True) - ct = ChargeTransport( + ct = ChargeDiffusionDynamics( band_limit_mol_list, evolve_config=evolve_config, stop_at_edge=True ) ct.evolve(evolve_dt=5.) @@ -90,7 +90,7 @@ def test_gaussian_bond_dim(): max_bonddim=10, ) evolve_config = EvolveConfig(guess_dt=0.1, adaptive=True) - ct = ChargeTransport( + ct = ChargeDiffusionDynamics( band_limit_mol_list, compress_config=compress_config, evolve_config=evolve_config, @@ -143,13 +143,13 @@ def test_reduced_density_matrix( scheme=3, ) - ct3 = ChargeTransport( + ct3 = ChargeDiffusionDynamics( mol_list3, temperature=Quantity(temperature, "K"), stop_at_edge=False, rdm=True ) ct3.evolve(evolve_dt, nsteps) mol_list4 = mol_list3.switch_scheme(4) - ct4 = ChargeTransport( + ct4 = ChargeDiffusionDynamics( mol_list4, temperature=Quantity(temperature, "K"), stop_at_edge=False, rdm=True ) ct4.evolve(evolve_dt, nsteps) @@ -176,9 +176,9 @@ def test_similar( Quantity(j_constant_value, "eV"), scheme=3, ) - ct1 = ChargeTransport(mol_list) + ct1 = ChargeDiffusionDynamics(mol_list) ct1.evolve(evolve_dt, nsteps) - ct2 = ChargeTransport(mol_list) + ct2 = ChargeDiffusionDynamics(mol_list) ct2.evolve(evolve_dt + 1e-5, nsteps) assert ct1.is_similar(ct2) @@ -201,11 +201,11 @@ def test_evolve( Quantity(j_constant_value, "eV"), scheme=3, ) - ct1 = ChargeTransport(mol_list, stop_at_edge=False) + ct1 = ChargeDiffusionDynamics(mol_list, stop_at_edge=False) half_nsteps = nsteps // 2 ct1.evolve(evolve_dt, half_nsteps) ct1.evolve(evolve_dt, nsteps - half_nsteps) - ct2 = ChargeTransport(mol_list, stop_at_edge=False) + ct2 = ChargeDiffusionDynamics(mol_list, stop_at_edge=False) ct2.evolve(evolve_dt, nsteps) assert ct1.is_similar(ct2) assert_iterable_equal(ct1.get_dump_dict(), ct2.get_dump_dict()) @@ -243,9 +243,9 @@ def test_band_limit_finite_t( Quantity(j_constant_value, "eV"), scheme=scheme, ) - ct1 = ChargeTransport(mol_list, stop_at_edge=False) + ct1 = ChargeDiffusionDynamics(mol_list, stop_at_edge=False) ct1.evolve(evolve_dt, nsteps) - ct2 = ChargeTransport(mol_list, temperature=low_t, stop_at_edge=False) + ct2 = ChargeDiffusionDynamics(mol_list, temperature=low_t, stop_at_edge=False) ct2.evolve(evolve_dt, nsteps) assert ct1.is_similar(ct2) @@ -268,11 +268,11 @@ def test_scheme4_finite_t( [Mol(Quantity(elocalex_value, "a.u."), ph_list)] * mol_num, Quantity(j_constant_value, "eV"), ) - ct1 = ChargeTransport( + ct1 = ChargeDiffusionDynamics( mol_list.switch_scheme(3), temperature=temperature, stop_at_edge=False ) ct1.evolve(evolve_dt, nsteps) - ct2 = ChargeTransport( + ct2 = ChargeDiffusionDynamics( mol_list.switch_scheme(4), temperature=temperature, stop_at_edge=False ) ct2.evolve(evolve_dt, nsteps) diff --git a/renormalizer/transport/tests/test_fullmpdm.py b/renormalizer/transport/tests/test_fullmpdm.py index 64ea5229..1247205e 100644 --- a/renormalizer/transport/tests/test_fullmpdm.py +++ b/renormalizer/transport/tests/test_fullmpdm.py @@ -8,7 +8,7 @@ from renormalizer.mps import MpDm, Mpo, MpDmFull, SuperLiouville, Mps, ThermalProp from renormalizer.utils import Quantity, CompressConfig from renormalizer.model import Phonon, Mol, MolList -from renormalizer.transport.transport import calc_r_square +from renormalizer.transport.dynamics import calc_r_square from renormalizer.transport.tests.band_param import band_limit_mol_list, low_t, get_analytical_r_square diff --git a/renormalizer/transport/tests/test_kubo.py b/renormalizer/transport/tests/test_kubo.py new file mode 100644 index 00000000..2111a7f3 --- /dev/null +++ b/renormalizer/transport/tests/test_kubo.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import qutip +import pytest + +from renormalizer.model import Phonon, Mol, MolList, MolList2, ModelTranslator +from renormalizer.transport.kubo import TransportKubo +from renormalizer.utils import Quantity, CompressConfig, EvolveConfig, EvolveMethod, CompressCriteria, Op +from renormalizer.utils.basis import BasisSimpleElectron, BasisSHO +from renormalizer.utils.qutip_utils import get_clist, get_blist, get_holstein_hamiltonian, get_qnidx, get_peierls_hamiltonian + + +@pytest.mark.parametrize("scheme", ( + 3, + 4, +)) +@pytest.mark.parametrize("mollist2", ( + True, + False, +)) +def test_holstein_kubo(scheme, mollist2): + ph = Phonon.simple_phonon(Quantity(1), Quantity(1), 2) + mol = Mol(Quantity(0), [ph]) + mol_list1 = MolList([mol] * 5, Quantity(1), scheme) + if mollist2: + mol_list = MolList2.MolList_to_MolList2(mol_list1, "general") + else: + mol_list = mol_list1 + temperature = Quantity(50000, 'K') + compress_config = CompressConfig(CompressCriteria.fixed, max_bonddim=24) + evolve_config = EvolveConfig(EvolveMethod.tdvp_ps, adaptive=True, guess_dt=0.5, adaptive_rtol=1e-3) + ievolve_config = EvolveConfig(EvolveMethod.tdvp_ps, adaptive=True, guess_dt=-0.1j) + kubo = TransportKubo(mol_list, temperature, compress_config=compress_config, ievolve_config=ievolve_config, evolve_config=evolve_config) + kubo.evolve(nsteps=5, evolve_time=5) + qutip_res = get_qutip_holstein_kubo(mol_list1, temperature, kubo.evolve_times_array) + rtol = 5e-2 + assert np.allclose(kubo.auto_corr, qutip_res, rtol=rtol) + + +def get_qutip_holstein_kubo(mol_list, temperature, time_series): + + nsites = len(mol_list) + J = mol_list.j_constant.as_au() + ph = mol_list[0].dmrg_phs[0] + ph_levels = ph.n_phys_dim + omega = ph.omega[0] + g = - ph.coupling_constant + clist = get_clist(nsites, ph_levels) + blist = get_blist(nsites, ph_levels) + + qn_idx = get_qnidx(ph_levels, nsites) + H = get_holstein_hamiltonian(nsites, J, omega, g, clist, blist).extract_states(qn_idx) + init_state = (-temperature.to_beta() * H).expm().unit() + + terms = [] + for i in range(nsites - 1): + terms.append(J * clist[i].dag() * clist[i + 1]) + terms.append(-J * clist[i] * clist[i + 1].dag()) + j_oper = sum(terms).extract_states(qn_idx) + + # Add the negative sign because j is taken to be real + return -qutip.correlation(H, init_state, [0], time_series, [], j_oper, j_oper)[0] + + +def test_peierls_kubo(): + # number of mol + n = 4 + # electronic coupling + V = -Quantity(120, "meV").as_au() + # intermolecular vibration freq + omega = Quantity(50, "cm-1").as_au() + # intermolecular coupling constant + g = 4 + # number of quanta + nlevels = 2 + # temperature + temperature = Quantity(300, "K") + + # the Peierls model + model = {} + # H_e + e_dofs = [f"e_{i}" for i in range(n)] + e_pairs = [] + for i in range(n): + i1, i2 = i, (i+1) % n + e_pairs.append([e_dofs[i1], e_dofs[i2]]) + + for e_pair in e_pairs: + hop1 = (Op(r"a^\dagger", 1), Op(r"a", -1), V) + hop2 = (Op(r"a", -1), Op(r"a^\dagger", 1), V) + model[tuple(e_pair)] = [hop1, hop2] + + # H_ph and H_(e-ph) + for ni in range(n): + v_str = f"v_{ni}" + model[(v_str,)] = [(Op(r"b^\dagger b", 0), omega)] + coup1 = (Op(r"b^\dagger + b", 0), Op(r"a^\dagger", 1), Op(r"a", -1), g * omega) + coup2 = (Op(r"b^\dagger + b", 0), Op(r"a", -1), Op(r"a^\dagger", 1), g * omega) + model[tuple([v_str] + e_pairs[ni])] = [coup1, coup2] + + order = [] + basis = [] + for ni in range(n): + order.append(f"e_{ni}") + basis.append(BasisSimpleElectron()) + order.append(f"v_{ni}") + basis.append(BasisSHO(omega, nlevels)) + + mol_list = MolList2(order, basis, model, ModelTranslator.general_model) + compress_config = CompressConfig(CompressCriteria.fixed, max_bonddim=24) + ievolve_config = EvolveConfig(EvolveMethod.tdvp_vmf, ivp_atol=1e-3, ivp_rtol=1e-5) + evolve_config = EvolveConfig(EvolveMethod.tdvp_vmf, ivp_atol=1e-3, ivp_rtol=1e-5) + kubo = TransportKubo(mol_list, temperature, compress_config=compress_config, ievolve_config=ievolve_config, evolve_config=evolve_config) + kubo.evolve(nsteps=5, evolve_time=1000) + + qutip_corr, qutip_corr_decomp = get_qutip_peierls_kubo(V, n, nlevels, omega, g, temperature, kubo.evolve_times_array) + atol = 1e-7 + rtol = 5e-2 + # direct comparison may fail because of different sign + assert np.allclose(kubo.auto_corr, qutip_corr, atol=atol, rtol=rtol) + assert np.allclose(kubo.auto_corr_decomposition, qutip_corr_decomp, atol=atol, rtol=rtol) + + +def get_qutip_peierls_kubo(J, nsites, ph_levels, omega, g, temperature, time_series): + clist = get_clist(nsites, ph_levels) + blist = get_blist(nsites, ph_levels) + + qn_idx = get_qnidx(ph_levels, nsites) + H = get_peierls_hamiltonian(nsites, J, omega, g, clist, blist).extract_states(qn_idx) + init_state = (-temperature.to_beta() * H).expm().unit() + + holstein_terms = [] + peierls_terms = [] + for i in range(nsites): + next_i = (i + 1) % nsites + holstein_terms.append( J * clist[i].dag() * clist[next_i]) + holstein_terms.append(-J * clist[i] * clist[next_i].dag()) + peierls_terms.append( g * omega * clist[i].dag() * clist[next_i] * (blist[i].dag() + blist[i])) + peierls_terms.append(-g * omega * clist[i] * clist[next_i].dag() * (blist[i].dag() + blist[i])) + j_oper1 = sum(holstein_terms).extract_states(qn_idx) + j_oper2 = sum(peierls_terms).extract_states(qn_idx) + + # Add negative signs because j is taken to be real + corr1 = -qutip.correlation(H, init_state, [0], time_series, [], j_oper1, j_oper1)[0] + corr2 = -qutip.correlation(H, init_state, [0], time_series, [], j_oper1, j_oper2)[0] + corr3 = -qutip.correlation(H, init_state, [0], time_series, [], j_oper2, j_oper1)[0] + corr4 = -qutip.correlation(H, init_state, [0], time_series, [], j_oper2, j_oper2)[0] + corr = corr1 + corr2 + corr3 + corr4 + return corr, np.array([corr1, corr2, corr3, corr4]).T diff --git a/renormalizer/utils/configs.py b/renormalizer/utils/configs.py index 4e61575b..4fc906eb 100644 --- a/renormalizer/utils/configs.py +++ b/renormalizer/utils/configs.py @@ -40,10 +40,11 @@ class CompressConfig: MPS Compress Configuration. Args: - criteria (:class:`CompressCriteria`): the criteria for compression. + criteria (:class:`renormalization.utils.configs.CompressCriteria`): the criteria for compression. threshold (float): the threshold to keep states if ``criteria`` is set to ``CompressCriteria.threshold`` or ``CompressCriteria.both``. - bonddim_distri (:class:`BondDimDistri`): Bond dimension distribution if ``criteria`` is set to + bonddim_distri (:class:`renormalization.utils.configs.BondDimDistri`): + Bond dimension distribution if ``criteria`` is set to ``CompressCriteria.fixed`` or ``CompressCriteria.both``. max_bonddim (int): Maximum bond dimension under various bond dimension distributions. """ diff --git a/renormalizer/utils/qutip_utils.py b/renormalizer/utils/qutip_utils.py index f2d8916a..2ab9923b 100644 --- a/renormalizer/utils/qutip_utils.py +++ b/renormalizer/utils/qutip_utils.py @@ -35,7 +35,7 @@ def get_blist(nsites, ph_levels): return blist -def get_hamiltonian(nsites, J, omega, g, clist, blist): +def get_holstein_hamiltonian(nsites, J, omega, g, clist, blist): lam = g ** 2 * omega terms = [] for i in range(nsites): @@ -45,8 +45,25 @@ def get_hamiltonian(nsites, J, omega, g, clist, blist): for i in range(nsites - 1): terms.append(J * clist[i].dag() * clist[i + 1]) terms.append(J * clist[i] * clist[i + 1].dag()) - H = sum(terms) - return H + + return sum(terms) + + +def get_peierls_hamiltonian(nsites, J, omega, g, clist, blist): + terms = [] + for i in range(nsites): + next_i = (i + 1) % nsites + # electronic coupling + terms.append(J * clist[i].dag() * clist[next_i]) + terms.append(J * clist[i] * clist[next_i].dag()) + # phonon energy + terms.append(omega * blist[i].dag() * blist[i]) + # electron-phonon coupling + terms.append(g * omega * clist[i].dag() * clist[next_i] * (blist[i].dag() + blist[i])) + terms.append(g * omega * clist[i] * clist[next_i].dag() * (blist[i].dag() + blist[i])) + + + return sum(terms) def get_gs(nsites, ph_levels): diff --git a/renormalizer/utils/tdmps.py b/renormalizer/utils/tdmps.py index 5743a0a5..bd81cb01 100644 --- a/renormalizer/utils/tdmps.py +++ b/renormalizer/utils/tdmps.py @@ -50,18 +50,13 @@ def process_mps(self, mps): def evolve(self, evolve_dt=None, nsteps=None, evolve_time=None): ''' - Parameters: - evolve_dt : - the time step to run `process_mps(self, mps)` to obtain the - properties - nsteps: int - the total number of evolution steps - evolve_time: - the total evolution time + Args: + evolve_dt (float): the time step to run `evolve_single_step` and `process_mps`. + nsteps (int): the total number of evolution steps + evolve_time (float): the total evolution time - Notes: - evolve_dt math: `\times` nsteps = evolve_time - otherwise nsteps has a higher priority + .. Notes:: + ``evolve_dt`` math: `\times` ``nsteps`` = ``evolve_time``, otherwise nsteps has a higher priority. ''' # deal with arguments if (evolve_dt is not None) and (nsteps is not None) and (evolve_time is not None): @@ -150,14 +145,17 @@ def evolve(self, evolve_dt=None, nsteps=None, evolve_time=None): def evolve_single_step(self, evolve_dt): """ + Evolve the mps for a single step with step size ``evolve_dt``. + :return: new mps after the evolution """ raise NotImplementedError def get_dump_dict(self): """ + Obtain calculated properties to dump in ``dict`` type. - :return: return a (ordered) dict to dump as json or npz + :return: return a (ordered) dict to dump as npz """ raise NotImplementedError diff --git a/renormalizer/vibronic/tests/test_pyr4.py b/renormalizer/vibronic/tests/test_pyr4.py index b37788b4..cf80073c 100644 --- a/renormalizer/vibronic/tests/test_pyr4.py +++ b/renormalizer/vibronic/tests/test_pyr4.py @@ -1,19 +1,19 @@ import logging +from collections import defaultdict from itertools import permutations as permut from itertools import product -from collections import defaultdict import pytest -from renormalizer.utils.constant import ev2au, fs2au -from renormalizer.utils import Op from renormalizer.model import MolList2, ModelTranslator -from renormalizer.utils import basis as ba -from renormalizer.vibronic import VibronicModelDynamics -from renormalizer.utils import EvolveConfig, CompressConfig, CompressCriteria, EvolveMethod +from renormalizer.model.mlist import vibronic_to_general from renormalizer.mps import Mps, Mpo from renormalizer.mps.backend import np - +from renormalizer.utils import EvolveConfig, CompressConfig, CompressCriteria, EvolveMethod +from renormalizer.utils import Op +from renormalizer.utils import basis as ba +from renormalizer.utils.constant import ev2au, fs2au +from renormalizer.vibronic import VibronicModelDynamics logger = logging.getLogger(__name__) @@ -163,36 +163,6 @@ def construct_vibronic_model(multi_e, dvr): return order, basis, model -def vibronic_to_general(model): - new_model = defaultdict(list) - for e_k, e_v in model.items(): - for kk, vv in e_v.items(): - # it's difficult to rename `kk` because sometimes it's related to - # phonons sometimes it's `"J"` - if e_k == "I": - # operators without electronic dof, simple phonon - new_model[kk] = vv - else: - # operators with electronic dof - assert isinstance(e_k, tuple) and len(e_k) == 2 - if e_k[0] == e_k[1]: - # diagonal - new_e_k = (e_k[0],) - e_op = (Op(r"a^\dagger a", 0),) - else: - # off-diagonal - new_e_k = e_k - e_op = (Op(r"a^\dagger", 1), Op("a", -1)) - if kk == "J": - new_model[new_e_k] = [e_op + (vv,)] - else: - for term in vv: - new_key = new_e_k + kk - new_value = e_op + term - new_model[new_key].append(new_value) - return new_model - - @pytest.mark.parametrize("multi_e, translator, dvr", ( [False, ModelTranslator.vibronic_model, True], [False, ModelTranslator.general_model, False],