From aaad48436922423c0bd4ed45479b79b2dc9f4a2b Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 20 Jan 2026 13:55:05 +0100 Subject: [PATCH 01/28] start of ip/ea-adc in refactored adcc --- adcc/AdcMatrix.py | 63 ++++-- adcc/AdcMethod.py | 26 ++- adcc/AmplitudeVector.py | 4 +- adcc/__init__.py | 50 +++++ adcc/adc_ea/__init__.py | 34 ++++ adcc/adc_ea/matrix.py | 212 +++++++++++++++++++ adcc/adc_ea/pole_strength.py | 191 +++++++++++++++++ adcc/adc_ea/state2state_transition_dm.py | 145 +++++++++++++ adcc/adc_ea/state_diffdm.py | 126 ++++++++++++ adcc/adc_ea/util.py | 64 ++++++ adcc/adc_ip/__init__.py | 34 ++++ adcc/adc_ip/matrix.py | 222 ++++++++++++++++++++ adcc/adc_ip/pole_strength.py | 222 ++++++++++++++++++++ adcc/adc_ip/state2state_transition_dm.py | 145 +++++++++++++ adcc/adc_ip/state_diffdm.py | 126 ++++++++++++ adcc/adc_ip/util.py | 64 ++++++ adcc/adc_pp/matrix.py | 16 +- adcc/block.py | 22 ++ adcc/workflow.py | 249 ++++++++++++++++------- 19 files changed, 1904 insertions(+), 111 deletions(-) create mode 100644 adcc/adc_ea/__init__.py create mode 100644 adcc/adc_ea/matrix.py create mode 100644 adcc/adc_ea/pole_strength.py create mode 100644 adcc/adc_ea/state2state_transition_dm.py create mode 100644 adcc/adc_ea/state_diffdm.py create mode 100644 adcc/adc_ea/util.py create mode 100644 adcc/adc_ip/__init__.py create mode 100644 adcc/adc_ip/matrix.py create mode 100644 adcc/adc_ip/pole_strength.py create mode 100644 adcc/adc_ip/state2state_transition_dm.py create mode 100644 adcc/adc_ip/state_diffdm.py create mode 100644 adcc/adc_ip/util.py diff --git a/adcc/AdcMatrix.py b/adcc/AdcMatrix.py index 54bec0698..1e9fe7c64 100644 --- a/adcc/AdcMatrix.py +++ b/adcc/AdcMatrix.py @@ -27,6 +27,8 @@ from .LazyMp import LazyMp from .adc_pp import matrix as ppmatrix +from .adc_ip import matrix as ipmatrix +from .adc_ea import matrix as eamatrix from .timings import Timer, timed_member_call from .AdcMethod import AdcMethod from .functions import ones_like @@ -73,6 +75,8 @@ class AdcMatrixlike: _special_block_orders = { "adc2x": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 1}, + "ip-adc2x": {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 1}, + "ea-adc2x": {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 1}, } @classmethod @@ -90,7 +94,9 @@ def _default_block_orders(cls, method: AdcMethod) -> dict[str, int]: # - determine which spaces are available in the ADC(n) matrix # starting from the given minimal space min_space = { - "pp": "ph" + "pp": "ph", + "ip": "h", + "ea": "p" }.get(method.adc_type, None) if min_space is None: raise ValueError(f"Unknown adc type {method.adc_type} for method " @@ -178,6 +184,10 @@ def _is_valid_space(cls, space: str, method: AdcMethod) -> bool: # be equal or differ e.g. by +-1 (IP/EA) if method.adc_type == "pp": return n_particle == n_hole + elif method.adc_type == "ip": + return n_particle == n_hole - 1 + elif method.adc_type == "ea": + return n_particle == n_hole + 1 raise ValueError(f"Unknown adc type {method.adc_type} for method " f"{method.name}. Can not validate space.") @@ -252,11 +262,19 @@ def __init__(self, method, hf_or_mp, block_orders=None, intermediates=None, variant = None if self.is_core_valence_separated: variant = "cvs" + # Directly import block dispatch functions? + BLOCK_DISPATCH = { + "pp": ppmatrix.block, + "ip": ipmatrix.block, + "ea": eamatrix.block} + block_dispatch_fun = BLOCK_DISPATCH[self.method.adc_type] blocks = { - block: ppmatrix.block(self.ground_state, block.split("_"), - order=order, intermediates=self.intermediates, + block: block_dispatch_fun(self.ground_state, block.split("_"), + order=order, + intermediates=self.intermediates, variant=variant) - for block, order in self.block_orders.items() if order is not None + for block, order in self.block_orders.items() + if order is not None } self.blocks = {bl: blocks[bl].apply for bl in blocks} if diagonal_precomputed: @@ -362,7 +380,8 @@ def block_apply(self, block, tensor): with another AmplitudeVector or Tensor. Non-matching blocks in the AmplitudeVector will be ignored. """ - if not isinstance(tensor, libadcc.Tensor): + # TODO: Allow AmplitudeVector? + if not isinstance(tensor, (libadcc.Tensor, AmplitudeVector)): raise TypeError("tensor should be an adcc.Tensor") with self.timer.record(f"apply/{block}"): @@ -426,16 +445,30 @@ def construct_symmetrisation_for_blocks(self): Returns a dictionary block identifier -> function """ ret = {} - if self.is_core_valence_separated: - # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a) - ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)]) - else: - def symmetrise_generic_adc_doubles(invec): - # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a) - scratch = invec.antisymmetrise([(2, 3)]) - # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a) - return scratch.symmetrise([(0, 1), (2, 3)]) - ret["pphh"] = symmetrise_generic_adc_doubles + # TODO: IP/EA doubles + if self.method.adc_type == "pp": + if self.is_core_valence_separated: + # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a) + ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)]) + else: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a) + scratch = invec.antisymmetrise([(2, 3)]) + # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a) + return scratch.symmetrise([(0, 1), (2, 3)]) + ret["pphh"] = symmetrise_generic_adc_doubles + elif self.method.adc_type == "ip": + if not self.is_core_valence_separated: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,j,a) <-> (j,i,a) + return invec.antisymmetrise([(0, 1)]) + ret["phh"] = symmetrise_generic_adc_doubles + elif self.method.adc_type == "ea": + if not self.is_core_valence_separated: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,a,b) <-> (i,a,b) + return invec.antisymmetrise([(1, 2)]) + ret["pph"] = symmetrise_generic_adc_doubles return ret def dense_basis(self, axis_blocks=None, ordering="adcc"): diff --git a/adcc/AdcMethod.py b/adcc/AdcMethod.py index d1781e28e..5e250e99c 100644 --- a/adcc/AdcMethod.py +++ b/adcc/AdcMethod.py @@ -24,10 +24,13 @@ def get_valid_methods(): valid_prefixes = ["cvs"] + valid_adc_types = ["ip", "ea"] valid_bases = ["adc0", "adc1", "adc2", "adc2x", "adc3"] - ret = valid_bases + [p + "-" + m for p in valid_prefixes - for m in valid_bases] + ret = (valid_bases + + [p + "-" + m for p in valid_prefixes for m in valid_bases] + + [t + "-" + m for t in valid_adc_types for m in valid_bases] + ) return ret @@ -43,8 +46,13 @@ def __init__(self, method): self.__base_method = split[-1] split = split[:-1] self.is_core_valence_separated = "cvs" in split - # NOTE: added this to make the testdata generation ready for IP/EA - self.adc_type = "pp" + + if "ip" in split: + self.adc_type = "ip" + elif "ea" in split: + self.adc_type = "ea" + else: + self.adc_type = "pp" try: if self.__base_method == "adc2x": @@ -66,10 +74,12 @@ def at_level(self, newlevel): @property def name(self): + name = self.__base_method + if self.adc_type != "pp": + name = self.adc_type + "-" + name if self.is_core_valence_separated: - return "cvs-" + self.__base_method - else: - return self.__base_method + name = "cvs-" + name + return name @property def property_method(self): @@ -89,6 +99,8 @@ def base_method(self): The base (full) method, i.e. with all approximations such as CVS stripped off. """ + if self.adc_type != "pp": + return AdcMethod(self.adc_type + "-" + self.__base_method) return AdcMethod(self.__base_method) def __eq__(self, other): diff --git a/adcc/AmplitudeVector.py b/adcc/AmplitudeVector.py index aff573b60..b08adad29 100644 --- a/adcc/AmplitudeVector.py +++ b/adcc/AmplitudeVector.py @@ -26,7 +26,9 @@ class AmplitudeVector(dict): def __init__(self, **kwargs): """ Construct an AmplitudeVector. Typical use cases are - ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``. + ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``. For IP-ADC + ``AmplitudeVector(h=tensor_singles, phh=tensor_doubles)``, and for + EA-ADC ``AmplitudeVector(p=tensor_singles, pph=tensor_doubles)`` """ super().__init__(**kwargs) diff --git a/adcc/__init__.py b/adcc/__init__.py index 0ab232d8e..07c933a1c 100644 --- a/adcc/__init__.py +++ b/adcc/__init__.py @@ -136,6 +136,56 @@ def cvs_adc3(*args, **kwargs): return run_adc(*args, **kwargs, method="cvs-adc3") +@with_runadc_doc +def ip_adc0(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc0") + + +@with_runadc_doc +def ip_adc1(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc1") + + +@with_runadc_doc +def ip_adc2(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc2") + + +@with_runadc_doc +def ip_adc2x(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc2x") + + +@with_runadc_doc +def ip_adc3(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc3") + + +@with_runadc_doc +def ea_adc0(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc0") + + +@with_runadc_doc +def ea_adc1(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc1") + + +@with_runadc_doc +def ea_adc2(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc2") + + +@with_runadc_doc +def ea_adc2x(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc2x") + + +@with_runadc_doc +def ea_adc3(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc3") + + def banner(colour=sys.stdout.isatty()): """Return a nice banner describing adcc and its components diff --git a/adcc/adc_ea/__init__.py b/adcc/adc_ea/__init__.py new file mode 100644 index 000000000..08c3d83b8 --- /dev/null +++ b/adcc/adc_ea/__init__.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from .state_diffdm import state_diffdm +from .pole_strength import pole_strength +from .state2state_transition_dm import state2state_transition_dm + +""" +Submodule, which contains rather lengthy low-level kernels +(e.g. matrix-vector products or working equations), which are called +from the high-level objects in the adcc main module. +""" + +__all__ = ["state_diffdm", "state2state_transition_dm", + "pole_strength"] diff --git a/adcc/adc_ea/matrix.py b/adcc/adc_ea/matrix.py new file mode 100644 index 000000000..63deeb6c6 --- /dev/null +++ b/adcc/adc_ea/matrix.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt +from collections import namedtuple + +from adcc import block as b +from adcc.functions import direct_sum, einsum +from adcc.Intermediates import Intermediates, register_as_intermediate +from adcc.AmplitudeVector import AmplitudeVector + +# +# Dispatch routine lives in 'adc_pp/matrix.py' +# +__all__ = ["block"] + +AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"]) + + +def block(ground_state, spaces, order, variant=None, intermediates=None): + """ + Gets ground state, potentially intermediates, spaces (ph, pphh and so on) + and the perturbation theory order for the block, + variant is "cvs" or sth like that. + + It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space, + while one would probably want in the long run that one can have an "o2" space, + but not do CVS. + """ + reference_state = ground_state.reference_state + if intermediates is None: + intermediates = Intermediates(ground_state) + + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) + + if fn not in globals(): + raise ValueError("Could not dispatch: " + f"spaces={spaces} order={order} variant={variant}. " + "Probably the secular matrix is not implemented for " + "the requested method.") + return globals()[fn](reference_state, ground_state, intermediates) + + +# +# 0th order main +# +def block_p_p_0(hf, mp, intermediates): + # M_{11} + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", hf.fvv, ampl.p)) + diagonal = AmplitudeVector(p=hf.fvv.diagonal()) + return AdcBlock(apply, diagonal) + + +def diagonal_pph_pph_0(hf): + res = direct_sum("-i+a+b->iab", + hf.foo.diagonal(), hf.fvv.diagonal(), hf.fvv.diagonal()) + return AmplitudeVector(pph=res.symmetrise(1, 2)) + + +def block_pph_pph_0(hf, mp, intermediates): + # M_{22} + def apply(ampl): + return AmplitudeVector(pph=( + - einsum("jab,ij->iab", ampl.pph, hf.foo) + + 2 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2) + )) + return AdcBlock(apply, diagonal_pph_pph_0(hf)) + + +# +# 1st order main +# +def block_p_p_1(hf, mp, intermediates): + # M_{11}, same as ADC(0) + return block_p_p_0(hf, mp, intermediates) + + +def diagonal_pph_pph_1(hf): + # TODO + pass + + +def block_pph_pph_1(hf, mp, intermediates): + # M_{22} + def apply(ampl): + return AmplitudeVector(pph=( + - einsum("jab,ij->iab", ampl.pph, hf.foo) + + 2 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2) + + 0.5 * einsum("abcd,icd->iab", hf.vvvv, ampl.pph) + - 2 * einsum("icka,kcb->iab", hf.ovov, ampl.pph + ).antisymmetrise(1, 2) + )) + return AdcBlock(apply, diagonal_pph_pph_0(hf)) + + +# +# 1st order coupling +# +def block_p_pph_1(hf, mp, intermediates): + # M_{12} + def apply(ampl): + return AmplitudeVector(p=( + - 1 / sqrt(2) * einsum("jabc,jbc->a", hf.ovvv, ampl.pph))) + return AdcBlock(apply, 0) + + +def block_pph_p_1(hf, mp, intermediates): + # M_{21} + def apply(ampl): + return AmplitudeVector(pph=( + - 1 / sqrt(2) * einsum("icab,c->iab", hf.ovvv, ampl.p))) + return AdcBlock(apply, 0) + + +# +# 2nd order main +# +def block_p_p_2(hf, mp, intermediates): + # M_{11} + # Intermediate can be found in 'adc_pp/matrix.py' + i1 = intermediates.adc2_i1 + diagonal = AmplitudeVector(p=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p)) + return AdcBlock(apply, diagonal) + + +# +# 2nd order coupling +# +def block_p_pph_2(hf, mp, intermediates): + # M_{12} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = - intermediates.adc3_pib + + def apply(ampl): + return AmplitudeVector(p=( + + 1 / sqrt(2) * einsum("jabc,jbc->a", i2, ampl.pph))) + return AdcBlock(apply, 0) + + +def block_pph_p_2(hf, mp, intermediates): + # M_{21} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = - intermediates.adc3_pib + + def apply(ampl): + return AmplitudeVector(pph=( + + 1 / sqrt(2) * einsum("icab,c->iab", i2, ampl.p))) + return AdcBlock(apply, 0) + + +# +# 3rd order main +# +def block_p_p_3(hf, mp, intermediates): + # M_{11} + i1 = intermediates.adc3_ea_i1 + diagonal = AmplitudeVector(p=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p)) + return AdcBlock(apply, diagonal) + + +# +# Intermediates +# + +@register_as_intermediate +def adc3_ea_i1(hf, mp, intermediates): + return ( + hf.fvv + ( + + 0.5 * einsum("ijac,ijbc->ab", mp.t2oo, hf.oovv) + - 0.25 * einsum("ijbc,ijac->ab", mp.t2oo, mp.t2eri(b.oovv, b.oo)) + + 0.5 * einsum("ijbc,ijca->ab", mp.t2oo, mp.t2eri(b.oovv, b.vv)) + + einsum("ijbc,jiac->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + - 2 * einsum("ijbc,jica->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + ).symmetrise() + + intermediates.sigma_vv + ) + + +@register_as_intermediate +def sigma_vv(hf, mp, intermediates): + # Static self-energy, oo part \Sigma_{ij}(\infty) + p0 = mp.mp2_diffdm + return (einsum("iajb,ij->ab", hf.ovov, p0.oo) + + 2 * einsum("iacb,ic->ab", hf.ovvv, p0.ov) + + einsum("acbd,cd->ab", hf.vvvv, p0.vv)).symmetrise() diff --git a/adcc/adc_ea/pole_strength.py b/adcc/adc_ea/pole_strength.py new file mode 100644 index 000000000..3ee1f0220 --- /dev/null +++ b/adcc/adc_ea/pole_strength.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2019 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like, dot, direct_sum +from adcc.Intermediates import Intermediates, register_as_intermediate + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def pole_strength_ea_adc0(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + # Build Kronecker delta + hf = mp.reference_state + d_vv = zeros_like(hf.fvv) + d_vv.set_mask("aa", 1.0) + f11 = d_vv + + # Calculate the spectroscopic amplitude x + xa = einsum("b,ba->a", amplitude.p, f11) + + return dot(xa, xa) + + +def pole_strength_ea_adc2(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + check_doubles_amplitudes([b.o, b.v, b.v], amplitude) + u1, u2 = amplitude.p, amplitude.pph + + f11 = intermediates.ea_adc2_f11 + f12 = - mp.mp2_diffdm.ov # -t_ia + f22 = intermediates.ea_adc2_f22 + + # Calculate the spectroscopic amplitude x + xi = einsum("ia,a->i", f12, u1) + einsum("ijab,jab->i", f22, u2) + xa = einsum("b,ba->a", u1, f11) + + return dot(xi, xi) + dot(xa, xa) + + +def pole_strength_ea_adc3(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + check_doubles_amplitudes([b.o, b.v, b.v], amplitude) + u1, u2 = amplitude.p, amplitude.pph + + f11 = intermediates.ea_adc3_f11 + # TODO: when mp3_diffdm is implemented, intermediate can be directly reused + # to avoid redundancy + # f12 = - intermediates.mp3_diffdm.ov + f12 = intermediates.ea_adc3_f12 + f22 = intermediates.ea_adc3_f22 + + # Calculate the spectroscopic amplitude x + xi = einsum("ia,a->i", f12, u1) + einsum("ijab,jab->i", f22, u2) + xa = einsum("b,ba->a", u1, f11) + + return dot(xi, xi) + dot(xa, xa) + + +# +# Intermediates +# + +@register_as_intermediate +def ea_adc2_f11(hf, mp, intermediates): + # effective transition moments, vv part f_ab + # Build Kronecker delta + d_vv = zeros_like(hf.fvv) + d_vv.set_mask("aa", 1.0) + + t2 = mp.t2(b.oovv) + + return d_vv - 0.25 * einsum("ijbc,ijac->ab", t2, t2) + + +@register_as_intermediate +def ea_adc2_f22(hf, mp, intermediates): + # effective transition moments, oovv part f_ijab + return 1/sqrt(2) * mp.t2(b.oovv) + + +@register_as_intermediate +def ea_adc3_f11(hf, mp, intermediates): + # effective transition moments, vv part f_ab + # Build Kronecker delta + d_vv = zeros_like(hf.fvv) + d_vv.set_mask("aa", 1.0) + + df = mp.df(b.ov) + df2 = direct_sum("ib+jc->ijbc", df, df).symmetrise((2, 3)) + + t2 = mp.t2(b.oovv) + + return (d_vv + - 0.25 * einsum("ijbc,ijac->ab", t2, t2) + + (+ 0.25 * einsum("ijac,ijbc->ab", t2, + mp.t2eri(b.oovv, b.vv) / df2) + + 0.25 * einsum("ijac,ijbc->ab", t2, + mp.t2eri(b.oovv, b.oo) / df2) + + einsum("ijac,ijbc->ab", t2, mp.t2eri(b.oovv, b.ov) / df2) + - einsum("ijac,jibc->ab", t2, mp.t2eri(b.oovv, b.ov) / df2)) + ) + + +@register_as_intermediate +def ea_adc3_f12(hf, mp, intermediates): + # effective transition moments, ov part f_ia + # Intermediates are defined in /adcc/adc_ip/pole_strength.py + return (- mp.mp2_diffdm.ov + (intermediates.sigma_ov + + intermediates.m_3_plus + + intermediates.m_3_minus + ) / mp.df) + + +@register_as_intermediate +def ea_adc3_f22(hf, mp, intermediates): + # effective transition moments, oovv part f_ijab + df = mp.df(b.ov) + df2 = direct_sum("ia+jb->ijab", df, df).symmetrise((2, 3)) + + return (1/sqrt(2) * mp.t2(b.oovv) + + 1/sqrt(2) * (0.5 * (mp.t2eri(b.oovv, b.oo) + + mp.t2eri(b.oovv, b.vv)) + + ((mp.t2eri(b.oovv, b.ov) + ).antisymmetrise(2, 3)).antisymmetrise(0, 1) + ) / df2 + ) + + +DISPATCH = { + "ea-adc0": pole_strength_ea_adc0, + "ea-adc1": pole_strength_ea_adc0, + "ea-adc2": pole_strength_ea_adc2, + "ea-adc3": pole_strength_ea_adc3, +} + + +def pole_strength(method, ground_state, amplitude, intermediates=None): + """Compute the pole strength of the electron attached state for the + provided ADC method from the spectroscopic amplitude x. + + Parameters + ---------- + method: adc.Method + Provide a method at which to compute the MTMs + ground_state : adcc.LazyMp + The MP ground state + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + + Returns + ------- + Scalar + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + if method.name not in DISPATCH: + raise NotImplementedError("pole_strength is not " + f"implemented for {method.name}.") + + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret diff --git a/adcc/adc_ea/state2state_transition_dm.py b/adcc/adc_ea/state2state_transition_dm.py new file mode 100644 index 000000000..738b9e557 --- /dev/null +++ b/adcc/adc_ea/state2state_transition_dm.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2018 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.OneParticleOperator import OneParticleOperator + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def s2s_tdm_ea_adc0(mp, amplitude_l, amplitude_r, intermediates): + check_singles_amplitudes([b.v], amplitude_l, amplitude_r) + ul1 = amplitude_l.p + ur1 = amplitude_r.p + + dm = OneParticleOperator(mp, is_symmetric=False) + dm.vv = einsum("a,b->ab", ul1, ur1) + return dm + + +def s2s_tdm_ea_adc2(mp, amplitude_l, amplitude_r, intermediates): + check_doubles_amplitudes([b.o, b.v, b.v], amplitude_l, amplitude_r) + dm = s2s_tdm_ea_adc0(mp, amplitude_l, amplitude_r, intermediates) + + ul1, ul2 = amplitude_l.p, amplitude_l.pph + ur1, ur2 = amplitude_r.p, amplitude_r.pph + + t2 = mp.t2(b.oovv) + p0 = mp.mp2_diffdm + p1_vv = dm.vv.evaluate() # ADC(1) diffdm + + # Zeroth order doubles contributions + p2_oo = -einsum("jab,iab->ij", ul2, ur2) + p2_vv = 2 * einsum("iac,ibc->ab", ul2, ur2) + p_ov = sqrt(2) * einsum("b,iba->ia", ul1, ur2) + p_vo = sqrt(2) * einsum("iba,b->ai", ul2, ur1) + + # ADC(2) ISR intermediate (TODO Move to intermediates) +# ru1 = einsum("i,ijab->jab", u1, t2).evaluate() + + # Compute second-order contributions to the density matrix + dm.oo = ( # ea_adc2_p_oo + + p2_oo + + einsum("ikc,jkc->ij", einsum("a,ikac->ikc", ul1, t2), + einsum("b,jkbc->jkc", ur1, t2)) + ) + + dm.vv = ( # ea_adc2_p_vv + + p1_vv + p2_vv + - 0.5 * einsum("c,ac,b->ab", ul1, p0.vv, ur1) + - 0.5 * einsum("a,bc,c->ab", ul1, p0.vv, ur1) + + 0.5 * einsum("ijb,ija->ab", einsum("c,ijcb->ijb", ul1, t2), + einsum("d,ijad->ija", ur1, t2)) + ) + + dm.ov = ( # ea_adc2_p_ov + + p_ov + + 1/sqrt(2) * ( + + einsum("jbc,ijbc,a->ia", ul2, t2, ur1) + + 2 * einsum("jc,ijac->ia", einsum("jcb,b->jc", ul2, ur1), t2)) + - einsum("b,ib,a->ia", ul1, p0.ov, ur1) + ) + + dm.vo = ( # ea_adc2_p_vo + + p_vo + + 1/sqrt(2) * ( + + einsum("a,ijbc,jbc->ai", ul1, t2, ur2) + + 2 * einsum("jc,ijac->ai", einsum("b,jcb->jc", ul1, ur2), t2)) + - einsum("b,ib,a->ai", ur1, p0.ov, ul1) + # switched indices because p0.ov is used instead of p0.vo + ) + return dm + + +DISPATCH = { + "ea-adc0": s2s_tdm_ea_adc0, + "ea-adc1": s2s_tdm_ea_adc0, # same as ADC(0) + "ea-adc2": s2s_tdm_ea_adc2, + "ea-adc2x": s2s_tdm_ea_adc2, # same as ADC(2) +} + + +def state2state_transition_dm(method, ground_state, amplitude_from, + amplitude_to, intermediates=None): + """ + Compute the state to state transition density matrix + state in the MO basis using the intermediate-states representation. + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude_from : AmplitudeVector + The amplitude vector of the state to start from + amplitude_to : AmplitudeVector + The amplitude vector of the state to excite to + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude_from, AmplitudeVector): + raise TypeError("amplitude_from should be an AmplitudeVector object.") + if not isinstance(amplitude_to, AmplitudeVector): + raise TypeError("amplitude_to should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state2state_transition_dm is not " + f"implemented for {method.name}.") + else: + # final state is on the bra side/left (complex conjugate) + # see ref https://doi.org/10.1080/00268976.2013.859313, appendix A2 + ret = DISPATCH[method.name](ground_state, amplitude_to, amplitude_from, + intermediates) + return ret.evaluate() diff --git a/adcc/adc_ea/state_diffdm.py b/adcc/adc_ea/state_diffdm.py new file mode 100644 index 000000000..181d10f02 --- /dev/null +++ b/adcc/adc_ea/state_diffdm.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.OneParticleOperator import OneParticleOperator + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def diffdm_ea_adc0(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + u1 = amplitude.p + + dm = OneParticleOperator(mp, is_symmetric=True) + dm.vv = einsum("a,b->ab", u1, u1) + return dm + + +def diffdm_ea_adc2(mp, amplitude, intermediates): + dm = diffdm_ea_adc0(mp, amplitude, intermediates) # Get ADC(0/1) result + check_doubles_amplitudes([b.o, b.v, b.v], amplitude) + u1, u2 = amplitude.p, amplitude.pph + + t2 = mp.t2(b.oovv) + p0 = mp.mp2_diffdm + p1_vv = dm.vv.evaluate() # ADC(1) diffdm + + # Zeroth order doubles contributions + p2_oo = -einsum("jab,iab->ij", u2, u2) + p2_vv = 2 * einsum("iac,ibc->ab", u2, u2) + p_ov = sqrt(2) * einsum("b,iba->ia", u1, u2) + + # ADC(2) ISR intermediate (TODO Move to intermediates) + # ru1 = einsum("i,ijab->jab", u1, t2).evaluate() + + # Compute second-order contributions to the density matrix + dm.oo = ( # ea_adc2_p_oo + + p2_oo + + einsum("ikc,jkc->ij", einsum("a,ikac->ikc", u1, t2), + einsum("b,jkbc->jkc", u1, t2)) + ) + + dm.vv = ( # ea_adc2_p_vv + + p1_vv + p2_vv + - 0.5 * einsum("c,ac,b->ab", u1, p0.vv, u1) + - 0.5 * einsum("a,bc,c->ab", u1, p0.vv, u1) + + 0.5 * einsum("ijb,ija->ab", einsum("c,ijcb->ijb", u1, t2), + einsum("d,ijad->ija", u1, t2)) + ) + + dm.ov = ( # ea_adc2_p_ov + + p_ov + + 1/sqrt(2) * ( + + einsum("jbc,ijbc,a->ia", u2, t2, u1) + + 2 * einsum("jc,ijac->ia", einsum("jcb,b->jc", u2, u1), t2)) + - einsum("b,ib,a->ia", u1, p0.ov, u1) + ) + return dm + + +# dict controlling the dispatch of the state_diffdm function +DISPATCH = { + "ea-adc0": diffdm_ea_adc0, + "ea-adc1": diffdm_ea_adc0, # same as ADC(0) + "ea-adc2": diffdm_ea_adc2, + "ea-adc2x": diffdm_ea_adc2, # same as ADC(2) +} + + +def state_diffdm(method, ground_state, amplitude, intermediates=None): + """ + Compute the one-particle difference density matrix of an excited state + in the MO basis. + + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude, AmplitudeVector): + raise TypeError("amplitude should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state_diffdm is not implemented " + f"for {method.name}.") + else: + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret.evaluate() diff --git a/adcc/adc_ea/util.py b/adcc/adc_ea/util.py new file mode 100644 index 000000000..951a5212c --- /dev/null +++ b/adcc/adc_ea/util.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + + +def check_singles_amplitudes(spaces, *amplitudes): + check_have_singles_block(*amplitudes) + check_singles_subspaces(spaces, *amplitudes) + + +def check_doubles_amplitudes(spaces, *amplitudes): + check_have_doubles_block(*amplitudes) + check_doubles_subspaces(spaces, *amplitudes) + + +def check_have_singles_block(*amplitudes): + if any("p" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(0) level and " + "beyond expects an excitation amplitude with a " + "singles part.") + + +def check_have_doubles_block(*amplitudes): + if any("pph" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(2) level and " + "beyond expects an excitation amplitude with a " + "singles and a doubles part.") + + +def check_singles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u1 = amplitude.p + if u1.subspaces != spaces: + raise ValueError("Mismatch in subspaces singles part " + f"(== {u1.subspaces}), where {spaces} " + "was expected.") + + +def check_doubles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u2 = amplitude.pph + if u2.subspaces != spaces: + raise ValueError("Mismatch in subspaces doubles part " + f"(== {u2.subspaces}), where " + f"{spaces} was expected.") diff --git a/adcc/adc_ip/__init__.py b/adcc/adc_ip/__init__.py new file mode 100644 index 000000000..08c3d83b8 --- /dev/null +++ b/adcc/adc_ip/__init__.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from .state_diffdm import state_diffdm +from .pole_strength import pole_strength +from .state2state_transition_dm import state2state_transition_dm + +""" +Submodule, which contains rather lengthy low-level kernels +(e.g. matrix-vector products or working equations), which are called +from the high-level objects in the adcc main module. +""" + +__all__ = ["state_diffdm", "state2state_transition_dm", + "pole_strength"] diff --git a/adcc/adc_ip/matrix.py b/adcc/adc_ip/matrix.py new file mode 100644 index 000000000..9b2a56606 --- /dev/null +++ b/adcc/adc_ip/matrix.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt +from collections import namedtuple + +from adcc import block as b +from adcc.functions import direct_sum, einsum, zeros_like +from adcc.Intermediates import Intermediates, register_as_intermediate +from adcc.AmplitudeVector import AmplitudeVector + +# +# Dispatch routine lives in 'adc_pp/matrix.py' +# +__all__ = ["block"] + +AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"]) + + +def block(ground_state, spaces, order, variant=None, intermediates=None): + """ + Gets ground state, potentially intermediates, spaces (ph, pphh and so on) + and the perturbation theory order for the block, + variant is "cvs" or sth like that. + + It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space, + while one would probably want in the long run that one can have an "o2" space, + but not do CVS. + """ + reference_state = ground_state.reference_state + if intermediates is None: + intermediates = Intermediates(ground_state) + + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) + + if fn not in globals(): + raise ValueError("Could not dispatch: " + f"spaces={spaces} order={order} variant={variant}. " + "Probably the secular matrix is not implemented for " + "the requested method.") + return globals()[fn](reference_state, ground_state, intermediates) + + +# +# 0th order main +# +def block_h_h_0(hf, mp, intermediates): + # M_{11} + def apply(ampl): + return AmplitudeVector(h=-einsum("ij,j->i", hf.foo, ampl.h)) + diagonal = AmplitudeVector(h=-hf.foo.diagonal()) + return AdcBlock(apply, diagonal) + + +def diagonal_phh_phh_0(hf): + fCC = hf.fcc if hf.has_core_occupied_space else hf.foo + res = direct_sum("-i-J+a->iJa", + hf.foo.diagonal(), fCC.diagonal(), hf.fvv.diagonal()) + return AmplitudeVector(phh=res) + + +def block_phh_phh_0(hf, mp, intermediates): + # M_{22} + def apply(ampl): + return AmplitudeVector(phh=( + + einsum("ab,ijb->ija", hf.fvv, ampl.phh) + - 2 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1) + )) + return AdcBlock(apply, diagonal_phh_phh_0(hf)) + + +# +# 1st order main +# +def block_h_h_1(hf, mp, intermediates): + # M_{11}, same as ADC(0) + return block_h_h_0(hf, mp, intermediates) + + +def diagonal_phh_phh_1(hf): + fCC = hf.fcc if hf.has_core_occupied_space else hf.foo + i1 = direct_sum("-i-J+a->iJa", + hf.foo.diagonal(), fCC.diagonal(), hf.fvv.diagonal()) + + # Build Kronecker delta + d_vv = zeros_like(hf.fvv) + d_vv.set_mask("aa", 1.0) + + i2 = einsum("ijij,aa->ija", hf.oooo, d_vv) + res = i1 + i2 + return AmplitudeVector(phh=res.symmetrise(0, 1)) + + +def block_phh_phh_1(hf, mp, intermediates): + # M_{22} + def apply(ampl): + return AmplitudeVector(phh=( + + einsum("ac,ijc->ija", hf.fvv, ampl.phh) + - 2 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1) + + 0.5 * einsum("ijkl,kla->ija", hf.oooo, ampl.phh) + - 2 * einsum("kaic,kjc->ija", hf.ovov, ampl.phh + ).antisymmetrise(0, 1) + )) + return AdcBlock(apply, diagonal_phh_phh_1(hf)) + + +# +# 1st order coupling +# +def block_h_phh_1(hf, mp, intermediates): + # M_{12} + def apply(ampl): + return AmplitudeVector(h=( + + 1 / sqrt(2) * einsum("jkib,jkb->i", hf.ooov, ampl.phh))) + return AdcBlock(apply, 0) + + +def block_phh_h_1(hf, mp, intermediates): + # M_{21} + def apply(ampl): + return AmplitudeVector(phh=( + + 1 / sqrt(2) * einsum("ijka,k->ija", hf.ooov, ampl.h))) + return AdcBlock(apply, 0) + + +# +# 2nd order main +# +def block_h_h_2(hf, mp, intermediates): + # M_{11} + # Intermediate can be found in 'adc_pp/matrix.py' + i1 = - intermediates.adc2_i2 + diagonal = AmplitudeVector(h=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h)) + return AdcBlock(apply, diagonal) + + +# +# 2nd order coupling +# +def block_h_phh_2(hf, mp, intermediates): + # M_{12} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = intermediates.adc3_pia + + def apply(ampl): + return AmplitudeVector(h=( + + 1 / sqrt(2) * einsum("jkib,jkb->i", i2, ampl.phh))) + return AdcBlock(apply, 0) + + +def block_phh_h_2(hf, mp, intermediates): + # M_{21} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = intermediates.adc3_pia + + def apply(ampl): + return AmplitudeVector(phh=( + + 1 / sqrt(2) * einsum("ijka,k->ija", i2, ampl.h))) + return AdcBlock(apply, 0) + + +# +# 3rd order main +# +def block_h_h_3(hf, mp, intermediates): + # M_{11} + i1 = intermediates.adc3_ip_i1 + diagonal = AmplitudeVector(h=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h)) + return AdcBlock(apply, diagonal) + + +# +# Intermediates +# + +@register_as_intermediate +def adc3_ip_i1(hf, mp, intermediates): + return ( + - hf.foo + ( + + 0.5 * einsum("ikab,jkab->ij", mp.t2oo, hf.oovv) + - 0.25 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.vv)) + + 0.5 * einsum("jkab,ikba->ij", mp.t2oo, mp.t2eri(b.oovv, b.oo)) + + einsum("jkab,kiab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + - 2 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + ).symmetrise() + - intermediates.sigma_oo + ) + + +@register_as_intermediate +def sigma_oo(hf, mp, intermediates): + # Static self-energy, oo part \Sigma_{ij}(\infty) + p0 = mp.mp2_diffdm + return (einsum("ikjl,kl->ij", hf.oooo, p0.oo) + + 2 * einsum("ikja,ka->ij", hf.ooov, p0.ov) + + einsum("iajb,ab->ij", hf.ovov, p0.vv)).symmetrise() diff --git a/adcc/adc_ip/pole_strength.py b/adcc/adc_ip/pole_strength.py new file mode 100644 index 000000000..0d8ad94eb --- /dev/null +++ b/adcc/adc_ip/pole_strength.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2019 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like, dot, direct_sum +from adcc.Intermediates import Intermediates, register_as_intermediate + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def pole_strength_ip_adc0(mp, amplitude, intermediates): + check_singles_amplitudes([b.o], amplitude) + # Build Kronecker delta + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1.0) + f11 = d_oo + + # Calculate the spectroscopic amplitude x + xi = einsum("j,ji->i", amplitude.h, f11) + + return dot(xi, xi) + + +def pole_strength_ip_adc2(mp, amplitude, intermediates): + check_singles_amplitudes([b.o], amplitude) + check_doubles_amplitudes([b.o, b.o, b.v], amplitude) + u1, u2 = amplitude.h, amplitude.phh + + f11 = intermediates.ip_adc2_f11 + f12 = mp.mp2_diffdm.ov # t_ia + f22 = intermediates.ip_adc2_f22 + + # Calculate the spectroscopic amplitude x + xi = einsum("j,ji->i", u1, f11) + xa = einsum("j,ja->a", u1, f12) + einsum("ijb,ijba->a", u2, f22) + + return dot(xi, xi) + dot(xa, xa) + + +def pole_strength_ip_adc3(mp, amplitude, intermediates): + check_singles_amplitudes([b.o], amplitude) + check_doubles_amplitudes([b.o, b.o, b.v], amplitude) + u1, u2 = amplitude.h, amplitude.phh + + f11 = intermediates.ip_adc3_f11 + # TODO: when mp3_diffdm is implemented, intermediate can be directly reused + # to avoid redundancy + # f12 = intermediates.mp3_diffdm.ov + f12 = intermediates.ip_adc3_f12 + f22 = intermediates.ip_adc3_f22 + + # Calculate the spectroscopic amplitude x + xi = einsum("j,ji->i", u1, f11) + xa = einsum("j,ja->a", u1, f12) + einsum("ijb,ijba->a", u2, f22) + + return dot(xi, xi) + dot(xa, xa) + + +# +# Intermediates +# + +@register_as_intermediate +def ip_adc2_f11(hf, mp, intermediates): + # effective transition moments, oo part f_ij + # Build Kronecker delta + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1.0) + + t2 = mp.t2(b.oovv) + + return d_oo - 0.25 * einsum("ilab,jlab->ij", t2, t2) + + +@register_as_intermediate +def ip_adc2_f22(hf, mp, intermediates): + # effective transition moments, oovv part f_ijab + return - 1/sqrt(2) * mp.t2(b.oovv) + + +@register_as_intermediate +def ip_adc3_f11(hf, mp, intermediates): + # effective transition moments, oo part f_ij + # Build Kronecker delta + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1.0) + + df = mp.df(b.ov) + df2 = direct_sum("ia+kb->ikab", df, df).symmetrise((0, 1)) + + t2 = mp.t2(b.oovv) + + return (d_oo + - 0.25 * einsum("ilab,jlab->ij", t2, t2) + + (+ 0.25 * einsum("jkab,ikab->ij", t2, + mp.t2eri(b.oovv, b.vv) / df2) + + 0.25 * einsum("jkab,ikab->ij", t2, + mp.t2eri(b.oovv, b.oo) / df2) + + einsum("jkab,ikab->ij", t2, mp.t2eri(b.oovv, b.ov) / df2) + - einsum("jkab,kiab->ij", t2, mp.t2eri(b.oovv, b.ov) / df2)) + ) + + +@register_as_intermediate +def ip_adc3_f12(hf, mp, intermediates): + # effective transition moments, ov part f_ia + return mp.mp2_diffdm.ov - (intermediates.sigma_ov + + intermediates.m_3_plus + + intermediates.m_3_minus + ) / mp.df(b.ov) + + +@register_as_intermediate +def ip_adc3_f22(hf, mp, intermediates): + # effective transition moments, oovv part f_ijab + df = mp.df(b.ov) + df2 = direct_sum("ia+jb->ijab", df, df).symmetrise((2, 3)) + + return (- 1/sqrt(2) * mp.t2(b.oovv) + + 1/sqrt(2) * (0.5 * (mp.t2eri(b.oovv, b.oo) + + mp.t2eri(b.oovv, b.vv)) + + ((mp.t2eri(b.oovv, b.ov) + ).antisymmetrise(2, 3)).antisymmetrise(0, 1) + ) / df2 + ) + + +# TODO: Intermediates are also necessary in LazyMP, avoid redundancy +# TODO: Proper testing against Q-Chem of the pole strengths +@register_as_intermediate +def sigma_ov(hf, mp, intermediates): + # Static self-energy, oo part \Sigma_{ij}(\infty) + p0 = mp.mp2_diffdm + return (+ einsum("jika,jk->ia", hf.ooov, p0.oo) + + einsum("ijab,jb->ia", hf.oovv, p0.ov) + - einsum("ibja,jb->ia", hf.ovov, p0.ov) + + einsum("ibac,bc->ia", hf.ovvv, p0.vv)) + + +@register_as_intermediate +def m_3_plus(hf, mp, intermediates): + # Intermediate M_ia^{(3)+}, parts of the dynamic self-energy + return (+ 1 * einsum("ijbc,jabc->ia", mp.t2oo, mp.t2eri(b.ovvv, b.ov)) + + 0.5 * einsum("ijbc,jabc->ia", mp.td2(b.oovv), hf.ovvv) + - 0.25 * einsum( + "ijbc,jabc->ia", mp.t2oo, mp.t2eri(b.ovvv, b.oo)) + ) + + +@register_as_intermediate +def m_3_minus(hf, mp, intermediates): + # Intermediate M_ia^{(3)-}, parts of the dynamic self-energy + return (+ 0.5 * einsum("jkab,jkib->ia", mp.td2(b.oovv), hf.ooov) + - 1 * einsum("jkab,jkib->ia", mp.t2oo, mp.t2eri(b.ooov, b.ov)) + - 0.25 * einsum( + "jkab,jkib->ia", mp.t2oo, mp.t2eri(b.ooov, b.vv) + ) + ) + +DISPATCH = { + "ip-adc0": pole_strength_ip_adc0, + "ip-adc1": pole_strength_ip_adc0, + "ip-adc2": pole_strength_ip_adc2, + "ip-adc3": pole_strength_ip_adc3, +} + + +def pole_strength(method, ground_state, amplitude, intermediates=None): + """Compute the pole strength of the ionized state for the + provided ADC method from the spectroscopic amplitude x. + + Parameters + ---------- + method: adc.Method + Provide a method at which to compute the MTMs + ground_state : adcc.LazyMp + The MP ground state + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + + Returns + ------- + Scalar + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + if method.name not in DISPATCH: + raise NotImplementedError("pole_strength is not " + f"implemented for {method.name}.") + + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret diff --git a/adcc/adc_ip/state2state_transition_dm.py b/adcc/adc_ip/state2state_transition_dm.py new file mode 100644 index 000000000..d78878d64 --- /dev/null +++ b/adcc/adc_ip/state2state_transition_dm.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2018 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.OneParticleOperator import OneParticleOperator + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def s2s_tdm_ip_adc0(mp, amplitude_l, amplitude_r, intermediates): + check_singles_amplitudes([b.o], amplitude_l, amplitude_r) + ul1 = amplitude_l.h + ur1 = amplitude_r.h + + dm = OneParticleOperator(mp, is_symmetric=False) + dm.oo = -einsum("j,i->ij", ul1, ur1) + return dm + + +def s2s_tdm_ip_adc2(mp, amplitude_l, amplitude_r, intermediates): + check_doubles_amplitudes([b.o, b.o, b.v], amplitude_l, amplitude_r) + dm = s2s_tdm_ip_adc0(mp, amplitude_l, amplitude_r, intermediates) + + ul1, ul2 = amplitude_l.h, amplitude_l.phh + ur1, ur2 = amplitude_r.h, amplitude_r.phh + + t2 = mp.t2(b.oovv) + p0 = mp.mp2_diffdm + p1_oo = dm.oo.evaluate() # ADC(1) diffdm + + # Zeroth order doubles contributions + p2_oo = 2 * einsum("kja,ika->ij", ul2, ur2) + p2_vv = einsum("ija,ijb->ab", ul2, ur2) + p_ov = sqrt(2) * einsum("j,ija->ia", ul1, ur2) + p_vo = sqrt(2) * einsum("ija,j->ai", ul2, ur1) + + # ADC(2) ISR intermediate (TODO Move to intermediates) +# ru1 = einsum("i,ijab->jab", u1, t2).evaluate() + + # Compute second-order contributions to the density matrix + dm.oo = ( # ip_adc2_p_oo + + p1_oo + p2_oo + - 0.5 * einsum("k,jk,i->ij", ul1, p0.oo, ur1) + - 0.5 * einsum("j,ki,k->ij", ul1, p0.oo, ur1) + + 0.5 * einsum("iab,jab->ij", einsum("k,kiab->iab", ul1, t2), + einsum("l,ljab->jab", ur1, t2)) + ) + + dm.vv = ( # ip_adc2_p_vv + + p2_vv + - einsum("kcb,kca->ab", einsum("i,kicb->kcb", ul1, t2), + einsum("j,kjca->kca", ur1, t2)) + ) + + dm.ov = ( # ip_adc2_p_ov + + p_ov + + 1/sqrt(2) * ( + + einsum("klb,klba,i->ia", ul2, t2, ur1) + + 2 * einsum("kb,ikba->ia", einsum("klb,l->kb", ul2, ur1), t2)) + - einsum("k,ka,i->ia", ul1, p0.ov, ur1) + ) + + dm.vo = ( # ip_adc2_p_vo + + p_vo + + 1/sqrt(2) * ( + + einsum("i,klba,klb->ai", ul1, t2, ur2) + + 2 * einsum("lb,liba->ai", einsum("k,klb->lb", ul1, ur2), t2)) + - einsum("k,ka,i->ai", ur1, p0.ov, ul1) + # switched indices because p0_ov is used instead of p0_vo + ) + return dm + + +DISPATCH = { + "ip-adc0": s2s_tdm_ip_adc0, + "ip-adc1": s2s_tdm_ip_adc0, # same as ADC(0) + "ip-adc2": s2s_tdm_ip_adc2, + "ip-adc2x": s2s_tdm_ip_adc2, # same as ADC(2) +} + + +def state2state_transition_dm(method, ground_state, amplitude_from, + amplitude_to, intermediates=None): + """ + Compute the state to state transition density matrix + state in the MO basis using the intermediate-states representation. + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude_from : AmplitudeVector + The amplitude vector of the state to start from + amplitude_to : AmplitudeVector + The amplitude vector of the state to excite to + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude_from, AmplitudeVector): + raise TypeError("amplitude_from should be an AmplitudeVector object.") + if not isinstance(amplitude_to, AmplitudeVector): + raise TypeError("amplitude_to should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state2state_transition_dm is not " + f"implemented for {method.name}.") + else: + # final state is on the bra side/left (complex conjugate) + # see ref https://doi.org/10.1080/00268976.2013.859313, appendix A2 + ret = DISPATCH[method.name](ground_state, amplitude_to, amplitude_from, + intermediates) + return ret.evaluate() diff --git a/adcc/adc_ip/state_diffdm.py b/adcc/adc_ip/state_diffdm.py new file mode 100644 index 000000000..9fab5225b --- /dev/null +++ b/adcc/adc_ip/state_diffdm.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt + +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.OneParticleOperator import OneParticleOperator + +from .util import check_doubles_amplitudes, check_singles_amplitudes + + +def diffdm_ip_adc0(mp, amplitude, intermediates): + check_singles_amplitudes([b.o], amplitude) + u1 = amplitude.h + + dm = OneParticleOperator(mp, is_symmetric=True) + dm.oo = -einsum("j,i->ij", u1, u1) + return dm + + +def diffdm_ip_adc2(mp, amplitude, intermediates): + dm = diffdm_ip_adc0(mp, amplitude, intermediates) # Get ADC(0/1) result + check_doubles_amplitudes([b.o, b.o, b.v], amplitude) + u1, u2 = amplitude.h, amplitude.phh + + t2 = mp.t2(b.oovv) + p0 = mp.mp2_diffdm + p1_oo = dm.oo.evaluate() # ADC(1) diffdm + + # Zeroth order doubles contributions + p2_oo = 2 * einsum("kja,ika->ij", u2, u2) + p2_vv = einsum("ija,ijb->ab", u2, u2) + p_ov = sqrt(2) * einsum("j,ija->ia", u1, u2) + + # ADC(2) ISR intermediate (TODO Move to intermediates) + # ru1 = einsum("i,ijab->jab", u1, t2).evaluate() + + # Compute second-order contributions to the density matrix + dm.oo = ( # ip_adc2_p_oo + + p1_oo + p2_oo + - 0.5 * einsum("k,jk,i->ij", u1, p0.oo, u1) + - 0.5 * einsum("j,ki,k->ij", u1, p0.oo, u1) + + 0.5 * einsum("iab,jab->ij", einsum("k,kiab->iab", u1, t2), + einsum("l,ljab->jab", u1, t2)) + ) + + dm.vv = ( # ip_adc2_p_vv + + p2_vv + - einsum("kcb,kca->ab", einsum("i,kicb->kcb", u1, t2), + einsum("j,kjca->kca", u1, t2)) + ) + + dm.ov = ( # ip_adc2_p_ov + + p_ov + + 1/sqrt(2) * ( + + einsum("klb,klba,i->ia", u2, t2, u1) + + 2 * einsum("kb,ikba->ia", einsum("klb,l->kb", u2, u1), t2)) + - einsum("k,ka,i->ia", u1, p0.ov, u1) + ) + return dm + + +# dict controlling the dispatch of the state_diffdm function +DISPATCH = { + "ip-adc0": diffdm_ip_adc0, + "ip-adc1": diffdm_ip_adc0, # same as ADC(0) + "ip-adc2": diffdm_ip_adc2, + "ip-adc2x": diffdm_ip_adc2, # same as ADC(2) +} + + +def state_diffdm(method, ground_state, amplitude, intermediates=None): + """ + Compute the one-particle difference density matrix of an excited state + in the MO basis. + + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude, AmplitudeVector): + raise TypeError("amplitude should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state_diffdm is not implemented " + f"for {method.name}.") + else: + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret.evaluate() diff --git a/adcc/adc_ip/util.py b/adcc/adc_ip/util.py new file mode 100644 index 000000000..8b13a9459 --- /dev/null +++ b/adcc/adc_ip/util.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + + +def check_singles_amplitudes(spaces, *amplitudes): + check_have_singles_block(*amplitudes) + check_singles_subspaces(spaces, *amplitudes) + + +def check_doubles_amplitudes(spaces, *amplitudes): + check_have_doubles_block(*amplitudes) + check_doubles_subspaces(spaces, *amplitudes) + + +def check_have_singles_block(*amplitudes): + if any("h" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(0) level and " + "beyond expects an excitation amplitude with a " + "singles part.") + + +def check_have_doubles_block(*amplitudes): + if any("phh" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(2) level and " + "beyond expects an excitation amplitude with a " + "singles and a doubles part.") + + +def check_singles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u1 = amplitude.h + if u1.subspaces != spaces: + raise ValueError("Mismatch in subspaces singles part " + f"(== {u1.subspaces}), where {spaces} " + "was expected.") + + +def check_doubles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u2 = amplitude.phh + if u2.subspaces != spaces: + raise ValueError("Mismatch in subspaces doubles part " + f"(== {u2.subspaces}), where " + f"{spaces} was expected.") diff --git a/adcc/adc_pp/matrix.py b/adcc/adc_pp/matrix.py index de36b66a4..2a9befdb6 100644 --- a/adcc/adc_pp/matrix.py +++ b/adcc/adc_pp/matrix.py @@ -60,24 +60,12 @@ def block(ground_state, spaces, order, variant=None, intermediates=None): while one would probably want in the long run that one can have an "o2" space, but not do CVS. """ - if isinstance(variant, str): - variant = [variant] - elif variant is None: - variant = [] reference_state = ground_state.reference_state if intermediates is None: intermediates = Intermediates(ground_state) - if ground_state.has_core_occupied_space and "cvs" not in variant: - raise ValueError("Cannot run a general (non-core-valence approximated) " - "ADC method on top of a ground state with a " - "core-valence separation.") - if not ground_state.has_core_occupied_space and "cvs" in variant: - raise ValueError("Cannot run a core-valence approximated ADC method on " - "top of a ground state without a " - "core-valence separation.") - - fn = "_".join(["block"] + variant + spaces + [str(order)]) + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) if fn not in globals(): raise ValueError("Could not dispatch: " diff --git a/adcc/block.py b/adcc/block.py index d827719d8..f40a7e64a 100644 --- a/adcc/block.py +++ b/adcc/block.py @@ -31,3 +31,25 @@ def __getattr__(attr): raise AttributeError mapping = {"o": "o1", "v": "v1", "c": "o2"} return "".join(mapping[c] for c in attr) + + +def get_block_name(spaces, order, variant, has_core_occupied_space): + """ + Assembles name of block function with variant, spaces, and order. + Does some sanity checks. + """ + if isinstance(variant, str): + variant = [variant] + elif variant is None: + variant = [] + + if has_core_occupied_space and "cvs" not in variant: + raise ValueError("Cannot run a general (non-core-valence approximated) " + "ADC method on top of a ground state with a " + "core-valence separation.") + if not has_core_occupied_space and "cvs" in variant: + raise ValueError("Cannot run a core-valence approximated ADC method on " + "top of a ground state without a " + "core-valence separation.") + + return "_".join(["block"] + variant + spaces + [str(order)]) diff --git a/adcc/workflow.py b/adcc/workflow.py index 6c79fb4c7..3c512b68d 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -26,8 +26,8 @@ from libadcc import ReferenceState from . import solver -from .guess import (guesses_any, guesses_singlet, guesses_spin_flip, - guesses_triplet) +from .guess import (guesses_any, guesses_singlet, guesses_doublet, + guesses_spin_flip, guesses_triplet) from .LazyMp import LazyMp from .AdcMatrix import AdcMatrix, AdcMatrixlike, AdcExtraTerm from .AdcMethod import AdcMethod @@ -46,8 +46,9 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, eigensolver=None, guesses=None, n_guesses=None, n_guesses_doubles=None, output=sys.stdout, core_orbitals=None, frozen_core=None, frozen_virtual=None, method=None, - n_singlets=None, n_triplets=None, n_spin_flip=None, - environment=None, **solverargs): + n_singlets=None, n_doublets=None, n_triplets=None, + n_spin_flip=None, is_alpha=None, environment=None, + **solverargs): """Run an ADC calculation. Main entry point to run an ADC calculation. The reference to build the ADC @@ -70,17 +71,24 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, n_states : int, optional kind : str, optional n_singlets : int, optional + n_doublets : int, optional n_triplets : int, optional n_spin_flip : int, optional Specify the number and kind of states to be computed. Possible values - for kind are "singlet", "triplet", "spin_flip" and "any", which is - the default. For unrestricted references clamping spin-pure + for kind are "singlet", "doublet", "triplet", "spin_flip" and "any", + which is the default. For unrestricted references clamping spin-pure singlets/triplets is currently not possible and kind has to remain as - "any". For restricted references `kind="singlets"` or `kind="triplets"` - may be employed to enforce a particular excited states manifold. + "any". For restricted references `kind="singlets"`, `kind="doublets"` + or `kind="triplets"` may be employed to enforce a particular excited + states manifold. Specifying `n_singlets` is equivalent to setting `kind="singlet"` and - `n_states=5`. Similarly for `n_triplets` and `n_spin_flip`. - `n_spin_flip` is only valid for unrestricted references. + `n_states=5`. Similarly for `n_doublets`, `n_triplets` and + `n_spin_flip`. `n_spin_flip` is only valid for unrestricted references. + + is_alpha : bool, optional + Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. Per default it will be set to `True` for + IP- and EA-ADC calculations. conv_tol : float, optional Convergence tolerance to employ in the iterative solver for obtaining @@ -91,8 +99,8 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, The eigensolver algorithm to use. n_guesses : int, optional - Total number of guesses to compute. By default only guesses derived from - the singles block of the ADC matrix are employed. See + Total number of guesses to compute. By default only guesses derived + from the singles block of the ADC matrix are employed. See `n_guesses_doubles` for alternatives. If no number is given here `n_guesses = min(4, 2 * number of excited states to compute)` or a smaller number if the number of excitation is estimated to be less @@ -175,40 +183,82 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, ... mf.kernel() ... ... state = adcc.cvs_adc3(mf, core_orbitals=1, n_singlets=3) - """ + + Run an IP-ADC(2) calculation of water with a detached alpha + electron + + >>> import psi4 + ... import adcc + ... # Run SCF in Psi4 + ... mol = psi4.geometry(''' + ... 0 1 + ... O 0.0000000000 0.0000000000 0.0000000000 + ... H 0.0000000000 0.0000000000 0.9570000000 + ... H 0.9270000000 0.0000000000 -0.2400000000 + ... symmetry c1 + ... units Angstrom + ... ''') + ... psi4.core.be_quiet() + ... psi4.set_options({'basis': "6-31++G(d)", 'e_convergence': 1e-13, + ... 'd_convergence': 1e-7, 'reference': "uhf", + ... 'scf_type': "direct"}) + ... scf_e, wfn = psi4.energy('SCF', return_wfn=True) + ... + ... state = adcc.ip_adc2(wfn, n_doublets=3, is_alpha=True) +""" matrix = construct_adcmatrix( data_or_matrix, core_orbitals=core_orbitals, frozen_core=frozen_core, frozen_virtual=frozen_virtual, method=method) - n_states, kind = validate_state_parameters( - matrix.reference_state, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, kind=kind) + n_states, kind, is_alpha = validate_state_parameters( + matrix, n_states=n_states, n_singlets=n_singlets, + n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, + kind=kind, is_alpha=is_alpha) + + # Set matrix attribute is_spin_flip to track whether it is a spin_flip + # calculation. Simplifications later? + matrix.method.is_spin_flip = kind == "spin_flip" # Determine spin change during excitation. If guesses is not None, # i.e. user-provided, we cannot guarantee for obtaining a particular # spin_change in case of a spin_flip calculation. - spin_change = None - if kind == "spin_flip" and guesses is None: - spin_change = -1 + if guesses: + spin_change = None + elif matrix.method.adc_type == "pp": + if kind == "spin_flip": + spin_change = -1 + else: + spin_change = 0 + elif matrix.method.adc_type == "ip": + if kind == "spin_flip": + spin_change = 0 + else: + spin_change = +0.5 - int(is_alpha) + elif matrix.method.adc_type == "ea": + if kind == "spin_flip": + spin_change = 0 + else: + spin_change = -0.5 + int(is_alpha) # Select solver to run if eigensolver is None: eigensolver = "davidson" # Setup environment coupling terms and energy corrections - ret = setup_environment(matrix, environment) - env_matrix_term, env_energy_corrections = ret + env_matrix_term, env_energy_corrections = setup_environment(matrix, + environment) # add terms to matrix if env_matrix_term: matrix += env_matrix_term - diagres = diagonalise_adcmatrix( matrix, n_states, kind, guesses=guesses, n_guesses=n_guesses, n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output, - eigensolver=eigensolver, **solverargs) + eigensolver=eigensolver, is_alpha=is_alpha, + spin_change=spin_change, **solverargs) exstates = ExcitedStates(diagres) exstates.kind = kind exstates.spin_change = spin_change + exstates.is_alpha = is_alpha # add environment corrections to excited states exstates += env_energy_corrections @@ -252,20 +302,20 @@ def construct_adcmatrix(data_or_matrix, core_orbitals=None, frozen_core=None, elif core_orbitals is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored core_orbitals parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of core_orbitals={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of core_orbitals={})." "".format(mospaces.n_orbs_alpha("o2"))) elif frozen_core is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored frozen_core parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of frozen_core={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of frozen_core={})." "".format(mospaces.n_orbs_alpha("o3"))) elif frozen_virtual is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored frozen_virtual parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of frozen_virtual={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of frozen_virtual={})." "".format(mospaces.n_orbs_alpha("v2"))) # Make AdcMatrix (if not done) @@ -282,18 +332,22 @@ def construct_adcmatrix(data_or_matrix, core_orbitals=None, frozen_core=None, return data_or_matrix -def validate_state_parameters(reference_state, n_states=None, n_singlets=None, - n_triplets=None, n_spin_flip=None, kind="any"): +def validate_state_parameters(matrix, n_states=None, n_singlets=None, + n_doublets=None, n_triplets=None, + n_spin_flip=None, kind="any", is_alpha=None): """ Check the passed state parameters for consistency with itself and with the passed reference and normalise them. In the end return the number of - states and the corresponding kind parameter selected. + states, the corresponding kind parameter selected and is_alpha which will + only be set to a Boolean for IP- and EA-ADC calculations. Internal function called from run_adc. """ - if sum(nst is not None for nst in [n_states, n_singlets, + reference_state = matrix.reference_state + adc_type = matrix.method.adc_type + if sum(nst is not None for nst in [n_states, n_singlets, n_doublets, n_triplets, n_spin_flip]) > 1: raise InputError("One may only specify one out of n_states, " - "n_singlets, n_triplets and n_spin_flip") + "n_singlets, n_doublets, n_triplets and n_spin_flip") if n_singlets is not None: if not reference_state.restricted: @@ -304,6 +358,15 @@ def validate_state_parameters(reference_state, n_states=None, n_singlets=None, "with n_singlets > 0") kind = "singlet" n_states = n_singlets + if n_doublets is not None: + if not reference_state.restricted: + raise InputError("The n_doublets parameter may only be employed " + "for restricted references") + if kind not in ["doublet", "any"]: + raise InputError(f"Kind parameter {kind} not compatible " + "with n_doublets > 0") + kind = "doublet" + n_states = n_doublets if n_triplets is not None: if not reference_state.restricted: raise InputError("The n_triplets parameter may only be employed " @@ -323,31 +386,54 @@ def validate_state_parameters(reference_state, n_states=None, n_singlets=None, kind = "spin_flip" n_states = n_spin_flip + # Check for IP- and EA-ADC parameter is_alpha + if adc_type == "pp": + if is_alpha is not None: + raise InputError("is_alpha may only be set for IP- and EA-ADC " + "calculations") + else: + if not isinstance(is_alpha, bool) and is_alpha is not None: + raise InputError("is_alpha has to be a Boolean or None.") + if is_alpha is None or reference_state.restricted: + # Per default set to True and for restricted references, only alpha + # states will be computed (beta states are identical) + is_alpha = True + # Check if there are states to be computed if n_states is None or n_states == 0: raise InputError("No excited states to be computed. Specify at least " - "one of n_states, n_singlets, n_triplets, " - "or n_spin_flip") + "one of n_states, n_singlets, n_doublets, " + "n_triplets, or n_spin_flip.") if n_states < 0: raise InputError("n_states needs to be positive") - if kind not in ["any", "spin_flip", "singlet", "triplet"]: + if kind not in ["any", "spin_flip", "singlet", "doublet", "triplet"]: raise InputError("The kind parameter may only take the values 'any', " - "'singlet', 'triplet' or 'spin_flip'") - if kind in ["singlet", "triplet"] and not reference_state.restricted: - raise InputError("kind==singlet and kind==triplet are only valid for " - "ADC calculations in combination with a restricted " - "ground state.") + "'singlet', 'doublet', 'triplet' or 'spin_flip'") + if (kind in ["singlet", "doublet", "triplet"] + and not reference_state.restricted): + raise InputError("kind==singlet, kind==doublet and kind==triplet are " + "only valid for ADC calculations in combination with " + "a restricted ground state.") if kind in ["spin_flip"] and reference_state.restricted: raise InputError("kind==spin_flip is only valid for " - "ADC calculations in combination with an unrestricted " + "ADC calculations in combination with an unrestricted" + " ground state.") + if kind in ["spin_flip", "singlet", "triplet"] and adc_type != "pp": + raise InputError("kind==singlet, kind==triplet and kind==spin_flip are" + " only valid for PP-ADC calculations in combination " + "with a restricted ground state.") + if kind == "doublet" and adc_type == "pp": + raise InputError("kind==doublet is only valid for IP/EA-ADC " + "calculations in combination with a restricted " "ground state.") - return n_states, kind + return n_states, kind, is_alpha def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", guesses=None, n_guesses=None, n_guesses_doubles=None, - conv_tol=None, output=sys.stdout, **solverargs): + conv_tol=None, output=sys.stdout, is_alpha=None, + spin_change=None, **solverargs): """ This function seeks appropriate guesses and afterwards proceeds to diagonalise the ADC matrix using the specified eigensolver. @@ -367,7 +453,7 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", # Determine explicit_symmetrisation explicit_symmetrisation = IndexSymmetrisation - if kind in ["singlet", "triplet"]: + if kind in ["singlet", "doublet", "triplet"]: explicit_symmetrisation = IndexSpinSymmetrisation( matrix, enforce_spin_kind=kind ) @@ -377,13 +463,13 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", n_guesses_per_state = 2 callback = setup_solver_printing( "Jacobi-Davidson", matrix, kind, solver.davidson.default_print, - output=output) + is_alpha=is_alpha, output=output) run_eigensolver = jacobi_davidson elif eigensolver == "lanczos": n_guesses_per_state = 1 callback = setup_solver_printing( "Lanczos", matrix, kind, solver.lanczos.default_print, - output=output) + is_alpha=is_alpha, output=output) run_eigensolver = lanczos else: raise InputError(f"Solver {eigensolver} unknown, try 'davidson'.") @@ -391,15 +477,11 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", # Obtain or check guesses if guesses is None: if n_guesses is None: - # restrict to the number of available singles guesses if no doubles - # are available - n_guesses = estimate_n_guesses( - matrix=matrix, n_states=n_states, - singles_only=("pphh" not in matrix.axis_blocks), - n_guesses_per_state=n_guesses_per_state - ) + n_guesses = estimate_n_guesses(matrix, n_states, + n_guesses_per_state) guesses = obtain_guesses_by_inspection(matrix, n_guesses, kind, - n_guesses_doubles) + n_guesses_doubles, is_alpha, + spin_change=spin_change) else: if len(guesses) < n_states: raise InputError("Less guesses provided via guesses (== {}) " @@ -409,8 +491,8 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", warnings.warn("Ignoring n_guesses parameter, since guesses are " "explicitly provided.") if n_guesses_doubles is not None: - warnings.warn("Ignoring n_guesses_doubles parameter, since guesses " - "are explicitly provided.") + warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" + " are explicitly provided.") solverargs.setdefault("which", "SA") return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol, @@ -419,8 +501,8 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", **solverargs) -def estimate_n_guesses(matrix, n_states, singles_only=True, - n_guesses_per_state=2): +def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2, + singles_only=True): """ Implementation of a basic heuristic to find a good number of guess vectors to be searched for using the find_guesses function. @@ -447,14 +529,20 @@ def estimate_n_guesses(matrix, n_states, singles_only=True, sp_occ = "o2" if matrix.is_core_valence_separated else "o1" n_virt_a = mospaces.n_orbs_alpha("v1") n_occ_a = mospaces.n_orbs_alpha(sp_occ) - n_guesses = min(n_guesses, n_occ_a * n_virt_a) + estimate = n_occ_a * n_virt_a + if matrix.method.level < 2 and matrix.method.adc_type != "pp": + # Adjustment for IP- and EA-ADC(0/1) calculations + estimate = n_occ_a if matrix.method.adc_type == "ip" else n_virt_a + n_guesses = min(n_guesses, estimate) # Adjust if we overshoot the maximal number of sensible singles block # guesses, but make sure we get at least n_states guesses return max(n_states, n_guesses) -def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_doubles=None): +def obtain_guesses_by_inspection(matrix, n_guesses, kind, + n_guesses_doubles=None, is_alpha=None, + spin_change=None): """ Obtain guesses by inspecting the diagonal matrix elements. If n_guesses_doubles is not None, this is number is always adhered to. @@ -463,31 +551,37 @@ def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_doubles=None Internal function called from run_adc. """ if n_guesses_doubles is not None and n_guesses_doubles > 0 \ - and "pphh" not in matrix.axis_blocks: + and not set(["phh", "pph", "pphh"]) & set(matrix.axis_blocks): raise InputError("n_guesses_doubles > 0 is only sensible if the ADC " - "method has a doubles block (i.e. it is *not* ADC(0), " - "ADC(1) or a variant thereof.") + "method has a doubles block (i.e. it is *not* ADC(0)," + " ADC(1) or a variant thereof.") # Determine guess function guess_function = {"any": guesses_any, "singlet": guesses_singlet, - "triplet": guesses_triplet, + "doublet": guesses_doublet, "triplet": guesses_triplet, "spin_flip": guesses_spin_flip}[kind] # Determine number of singles guesses to request n_guess_singles = n_guesses if n_guesses_doubles is not None: n_guess_singles = n_guesses - n_guesses_doubles - singles_guesses = guess_function(matrix, n_guess_singles, block="ph") + singles_guesses = guess_function(matrix, n_guess_singles, + block=matrix.axis_blocks[0], + is_alpha=is_alpha, + spin_change=spin_change) doubles_guesses = [] - if "pphh" in matrix.axis_blocks: + if set(["phh", "pph", "pphh"]) & set(matrix.axis_blocks): # Determine number of doubles guesses to request if not # explicitly specified if n_guesses_doubles is None: n_guesses_doubles = n_guesses - len(singles_guesses) if n_guesses_doubles > 0: doubles_guesses = guess_function(matrix, n_guesses_doubles, - block="pphh") + block=matrix.axis_blocks[1], + kind=kind, + is_alpha=is_alpha, + spin_change=spin_change) total_guesses = singles_guesses + doubles_guesses if len(total_guesses) < n_guesses: @@ -497,19 +591,22 @@ def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_doubles=None def setup_solver_printing(solmethod_name, matrix, kind, default_print, - output=None): + is_alpha=None, output=None): """ Setup default printing for solvers. Internal function called from run_adc. """ - kstr = " " + kstr = "" if kind != "any": kstr = " " + kind method_name = f"{matrix}" if hasattr(matrix, "method"): method_name = matrix.method.name + spin_type = "" + if is_alpha is not None: + spin_type = "alpha " if is_alpha else "beta " if output is not None: - print(f"Starting {method_name}{kstr} {solmethod_name} ...", + print(f"Starting {spin_type}{method_name}{kstr} {solmethod_name} ...", file=output) def inner_callback(state, identifier): @@ -522,6 +619,10 @@ def setup_environment(matrix, environment): Setup environment matrix terms and/or energy corrections. Internal function called from run_adc. """ + if environment and matrix.method.adc_type != "pp": + raise NotImplementedError("Environment for IP- and EA-ADC calculations" + " not implemented.") + valid_envs = ["ptss", "ptlr", "linear_response"] hf = matrix.reference_state if hf.environment and environment is None: @@ -580,8 +681,8 @@ def setup_environment(matrix, environment): from adcc.adc_pp import environment as adcpp_env block_key = f"block_ph_ph_0_{hf.environment}" if not hasattr(adcpp_env, block_key): - raise NotImplementedError("Matrix term for linear response coupling" - f" with solvent {hf.environment}" + raise NotImplementedError("Matrix term for linear response " + f"coupling with solvent {hf.environment}" " not implemented.") block_fun = getattr(adcpp_env, block_key) env_matrix_term = AdcExtraTerm(matrix, {'ph_ph': block_fun}) From 856489a68e273c710403d18ad7503747edb5e65a Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 20 Jan 2026 19:48:05 +0100 Subject: [PATCH 02/28] Refactored guess setup and implemented ip/ea guesses making guess/__init__.py obsolete --- adcc/guess/Guesses.py | 159 ++++++ adcc/guess/__init__.py | 97 ---- adcc/guess/guess_zero.py | 197 ++++--- adcc/guess/guesses_from_diagonal.py | 110 ++-- adcc/workflow.py | 206 ++------ .../amplitude_vector_enforce_spin_kind.cc | 129 ++++- .../amplitude_vector_enforce_spin_kind.hh | 6 +- libadcc_src/fill_ea_doubles_guesses.cc | 78 +++ libadcc_src/fill_ea_doubles_guesses.hh | 53 ++ libadcc_src/fill_ip_doubles_guesses.cc | 78 +++ libadcc_src/fill_ip_doubles_guesses.hh | 53 ++ libadcc_src/guess/ea_adc_guess_d.cc | 484 +++++++++++++++++ libadcc_src/guess/ea_adc_guess_d.hh | 31 ++ libadcc_src/guess/index_group_h2p.cc | 84 +++ libadcc_src/guess/index_group_h2p.hh | 164 ++++++ libadcc_src/guess/index_group_p2h.cc | 84 +++ libadcc_src/guess/index_group_p2h.hh | 164 ++++++ libadcc_src/guess/ip_adc_guess_d.cc | 485 ++++++++++++++++++ libadcc_src/guess/ip_adc_guess_d.hh | 31 ++ libadcc_src/pyiface/ExportAdcc.cc | 4 + libadcc_src/pyiface/export_adc_ea.cc | 56 ++ libadcc_src/pyiface/export_adc_ip.cc | 56 ++ 22 files changed, 2441 insertions(+), 368 deletions(-) create mode 100644 adcc/guess/Guesses.py delete mode 100644 adcc/guess/__init__.py create mode 100644 libadcc_src/fill_ea_doubles_guesses.cc create mode 100644 libadcc_src/fill_ea_doubles_guesses.hh create mode 100644 libadcc_src/fill_ip_doubles_guesses.cc create mode 100644 libadcc_src/fill_ip_doubles_guesses.hh create mode 100644 libadcc_src/guess/ea_adc_guess_d.cc create mode 100644 libadcc_src/guess/ea_adc_guess_d.hh create mode 100644 libadcc_src/guess/index_group_h2p.cc create mode 100644 libadcc_src/guess/index_group_h2p.hh create mode 100644 libadcc_src/guess/index_group_p2h.cc create mode 100644 libadcc_src/guess/index_group_p2h.hh create mode 100644 libadcc_src/guess/ip_adc_guess_d.cc create mode 100644 libadcc_src/guess/ip_adc_guess_d.hh create mode 100644 libadcc_src/pyiface/export_adc_ea.cc create mode 100644 libadcc_src/pyiface/export_adc_ip.cc diff --git a/adcc/guess/Guesses.py b/adcc/guess/Guesses.py new file mode 100644 index 000000000..022b8ef04 --- /dev/null +++ b/adcc/guess/Guesses.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2018 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + +from .guess import guesses_from_diagonal +from .exceptions import InputError +import warnings + + +def determine_spin_change(self, adc_type: str, kind: str, is_alpha: bool): + if adc_type == "pp": + if kind == "spin_flip": + spin_change = -1 + else: + spin_change = 0 + elif adc_type == "ip": + if kind == "spin_flip": + spin_change = 0 + else: + spin_change = +0.5 - int(is_alpha) + elif adc_type == "ea": + if kind == "spin_flip": + spin_change = 0 + else: + spin_change = -0.5 + int(is_alpha) + return spin_change + + +class Guesses: + def __init__(self, guesses: list|None, kind: str, + adc_type: str, is_alpha: bool): + if guesses is None: + self.guesses = [] + else: + self.guesses = guesses + self.n_guesses = len(self.guesses) + self.kind = kind + self.is_alpha = self.is_alpha + + symmetrisation = { + "singlet": "symmetric", + "doublet": "none", + "triplet": "antisymmetric", + "spin_flip":"none", + "any": "none" + } + self.spin_block_symmetrisation = symmetrisation.get(kind, 'none') + + if guesses is not None: + self.spin_change = None + else: + self.spin_change = determine_spin_change(adc_type, kind, is_alpha) + + + def estimate_n_guesses(self, matrix, n_states, n_guesses_per_state=2, + singles_only=True): + """ + Implementation of a basic heuristic to find a good number of guess + vectors to be searched for using the find_guesses function. + Internal function called from run_adc. + + matrix ADC matrix + n_states Number of states to be computed + singles_only Try to stay withing the singles excitation space + with the number of guess vectors. + n_guesses_per_state Number of guesses to search for for each state + """ + # Try to use at least 4 or twice the number of states + # to be computed as guesses + n_guesses = n_guesses_per_state * max(2, n_states) + + if singles_only: + # Compute the maximal number of sensible singles block guesses. + # This is roughly the number of occupied alpha orbitals + # times the number of virtual alpha orbitals + # + # If the system is core valence separated, then only the + # core electrons count as "occupied". + mospaces = matrix.mospaces + sp_occ = "o2" if matrix.is_core_valence_separated else "o1" + n_virt_a = mospaces.n_orbs_alpha("v1") + n_occ_a = mospaces.n_orbs_alpha(sp_occ) + estimate = n_occ_a * n_virt_a + if matrix.method.level < 2 and matrix.method.adc_type != "pp": + # Adjustment for IP- and EA-ADC(0/1) calculations + estimate = n_occ_a if matrix.method.adc_type == "ip" else n_virt_a + n_guesses = min(n_guesses, estimate) + + # Adjust if we overshoot the maximal number of sensible singles block + # guesses, but make sure we get at least n_states guesses + self.n_guesses = max(n_states, n_guesses) + + + def obtain_guesses_by_inspection(self, matrix, n_guesses_doubles=None, + **kwargs): + """ + Obtain guesses by inspecting the diagonal matrix elements. + If n_guesses_doubles is not None, this number is always adhered to. + Otherwise the number of doubles guesses is adjusted to fill up whatever + the singles guesses cannot provide to reach n_guesses. + + matrix The matrix for which guesses are to be constructed + is_alpha Is the detached/attached electron alpha spin for the + respective IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + if n_guesses_doubles is not None and len(matrix.axis_blocks) < 2: + raise InputError("n_guesses_doubles > 0 is only sensible if the " + "ADC method has a doubles block (i.e. it is *not* " + "ADC(0), ADC(1) or a variant thereof.") + + # Determine number of singles guesses to request + if n_guesses_doubles is None: + n_guesses_doubles = 0 + else: + if len(matrix.axis_blocks) < 2: + warnings.warn("Ignoring n_guesses_doubles parameter, since no " + "double guesses exist.") + + n_guesses_singles = self.n_guesses - n_guesses_doubles + + self.guesses += guesses_from_diagonal( + matrix, n_guesses_singles, matrix.axis_blocks[0], self.kind, + self.is_alpha, self.spin_change, self.spin_block_symmetrisation, + **kwargs) + + # Determine number of doubles guesses to request if not + # explicitly specified + n_guesses_doubles = self.n_guesses - len(self.guesses) + if n_guesses_doubles > 0: + self.guesses += guesses_from_diagonal( + matrix, n_guesses_doubles, matrix.axis_blocks[1], self.kind, + self.is_alpha, self.spin_change, + self.spin_block_symmetrisation, **kwargs) + + if len(self.guesses) < self.n_guesses: + raise InputError("Less guesses found than requested: {} found, " + "{} requested".format( + len(self.guesses), self.n_guesses)) + self.n_guesses = len(self.guesses) \ No newline at end of file diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py deleted file mode 100644 index b2690876b..000000000 --- a/adcc/guess/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab -## --------------------------------------------------------------------- -## -## Copyright (C) 2020 by the adcc authors -## -## This file is part of adcc. -## -## adcc is free software: you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## adcc is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with adcc. If not, see . -## -## --------------------------------------------------------------------- -from .guess_zero import guess_symmetries, guess_zero -from .guesses_from_diagonal import guesses_from_diagonal - -__all__ = ["guess_zero", "guesses_from_diagonal", - "guesses_singlet", "guesses_triplet", "guesses_any", - "guesses_spin_flip", "guess_symmetries"] - - -def guess_kwargs_kind(kind): - """ - Return the kwargs required to be passed to `guesses_from_diagonal` to - computed states of the passed excitation `kind`. - """ - kwargsmap = dict( - singlet=dict(spin_block_symmetrisation="symmetric", spin_change=0), - triplet=dict(spin_block_symmetrisation="antisymmetric", spin_change=0), - spin_flip=dict(spin_block_symmetrisation="none", spin_change=-1), - any=dict(spin_block_symmetrisation="none", spin_change=0), - ) - try: - return kwargsmap[kind] - except KeyError: - raise ValueError(f"Kind not known: {kind}") - - -def guesses_singlet(matrix, n_guesses, block="ph", **kwargs): - """ - Obtain guesses for computing singlet states by inspecting the passed - ADC matrix. - - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. - """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("singlet"), **kwargs) - - -def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): - """ - Obtain guesses for computing triplet states by inspecting the passed - ADC matrix. - - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. - """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("triplet"), **kwargs) - - -# guesses for computing any state (singlet or triplet) -guesses_any = guesses_from_diagonal - - -def guesses_spin_flip(matrix, n_guesses, block="ph", **kwargs): - """ - Obtain guesses for computing spin-flip states by inspecting the passed - ADC matrix. - - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. - """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("spin_flip"), **kwargs) diff --git a/adcc/guess/guess_zero.py b/adcc/guess/guess_zero.py index 67791b9be..c57cc2b1b 100644 --- a/adcc/guess/guess_zero.py +++ b/adcc/guess/guess_zero.py @@ -34,14 +34,14 @@ def guess_zero(matrix, spin_change=0, spin_block_symmetrisation="none"): spin_change The spin change to enforce in an excitation. Typical values are 0 (singlet/triplet/any) and -1 (spin-flip). spin_block_symmetrisation - Symmetrisation to enforce between equivalent spin blocks, which - all yield the desired spin_change. E.g. if spin_change == 0, - then both the alpha->alpha and beta->beta blocks of the singles - part of the excitation vector achieve a spin change of 0. - The symmetry specified with this parameter will then be imposed - between the a-a and b-b blocks. Valid values are "none", - "symmetric" and "antisymmetric", where "none" enforces - no particular symmetry. + Symmetrisation to enforce between equivalent spin blocks, + which all yield the desired spin_change. E.g. if + spin_change == 0, then both the alpha->alpha and beta->beta + blocks of the singles part of the excitation vector achieve a + spin change of 0. The symmetry specified with this parameter + will then be imposed between the a-a and b-b blocks. Valid + values are "none", "symmetric" and "antisymmetric", where + "none" enforces no particular symmetry. """ return AmplitudeVector(**{ block: Tensor(sym) for block, sym in guess_symmetries( @@ -60,14 +60,14 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"): spin_change The spin change to enforce in an excitation. Typical values are 0 (singlet/triplet/any) and -1 (spin-flip). spin_block_symmetrisation - Symmetrisation to enforce between equivalent spin blocks, which - all yield the desired spin_change. E.g. if spin_change == 0, - then both the alpha->alpha and beta->beta blocks of the singles - part of the excitation vector achieve a spin change of 0. - The symmetry specified with this parameter will then be imposed - between the a-a and b-b blocks. Valid values are "none", - "symmetric" and "antisymmetric", where "none" enforces - no particular symmetry. + Symmetrisation to enforce between equivalent spin blocks, + which all yield the desired spin_change. E.g. if + spin_change == 0, then both the alpha->alpha and beta->beta + blocks of the singles part of the excitation vector achieve a + spin change of 0. The symmetry specified with this parameter + will then be imposed between the a-a and b-b blocks. Valid + values are "none", "symmetric" and "antisymmetric", where + "none" enforces no particular symmetry. """ if not isinstance(matrix, AdcMatrixlike): raise TypeError("matrix needs to be of type AdcMatrixlike") @@ -80,52 +80,59 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"): "ADC calculations on top of restricted reference " "states.") if int(spin_change * 2) / 2 != spin_change: - raise ValueError("Only integer or half-integer spin_change is allowed. " - "You passed {}".format(spin_change)) - - max_spin_change = 0 - if "ph" in matrix.axis_blocks: - max_spin_change = 1 - if "pphh" in matrix.axis_blocks: - max_spin_change = 2 - if spin_change > max_spin_change: - raise ValueError("spin_change for singles guesses may only be in the " + raise ValueError("Only integer or half-integer spin_change is allowed." + " You passed {}".format(spin_change)) + + max_spin_change = 0.5 * len(matrix.axis_blocks[-1]) + + if abs(spin_change) > max_spin_change: + raise ValueError("spin_change for may only be in the " f"range [{-max_spin_change}, {max_spin_change}] and " f"not {spin_change}.") symmetries = {} - if "ph" in matrix.axis_blocks: - symmetries["ph"] = guess_symmetry_singles( + block = matrix.axis_blocks[0] + symmetries[block] = guess_symmetry_singles( + matrix, spin_change=spin_change, + spin_block_symmetrisation=spin_block_symmetrisation, block=block + ) + if len(matrix.axis_blocks) >= 2: + block = matrix.axis_blocks[1] + symmetries[block] = guess_symmetry_doubles( matrix, spin_change=spin_change, - spin_block_symmetrisation=spin_block_symmetrisation - ) - if "pphh" in matrix.axis_blocks: - symmetries["pphh"] = guess_symmetry_doubles( - matrix, spin_change=spin_change, - spin_block_symmetrisation=spin_block_symmetrisation + spin_block_symmetrisation=spin_block_symmetrisation, block=block ) return symmetries def guess_symmetry_singles(matrix, spin_change=0, - spin_block_symmetrisation="none"): - symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces["ph"])) + spin_block_symmetrisation="none", block="ph"): + symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces[block])) symmetry.irreps_allowed = ["A"] + if spin_change != 0 and spin_block_symmetrisation != "none": raise NotImplementedError("spin_symmetrisation != 'none' only " "implemented for spin_change == 0") - elif spin_block_symmetrisation == "symmetric": - symmetry.spin_block_maps = [("aa", "bb", 1)] - symmetry.spin_blocks_forbidden = ["ab", "ba"] - elif spin_block_symmetrisation == "antisymmetric": - symmetry.spin_block_maps = [("aa", "bb", -1)] + + if spin_block_symmetrisation in ("symmetric", "antisymmetric"): + # PP-ADC + fac = 1 if spin_block_symmetrisation == "symmetric" else -1 + symmetry.spin_block_maps = [("aa", "bb", fac)] symmetry.spin_blocks_forbidden = ["ab", "ba"] + + elif matrix.method.adc_type != "pp" and matrix.reference_state.restricted: + # IP- and EA-ADC + # attach/detach alpha electron + # forbidden beta blocks (["a"]) not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["b"] + return symmetry def guess_symmetry_doubles(matrix, spin_change=0, - spin_block_symmetrisation="none"): - spaces_d = matrix.axis_spaces["pphh"] + spin_block_symmetrisation="none", block="pphh"): + spaces_d = matrix.axis_spaces[block] symmetry = Symmetry(matrix.mospaces, "".join(spaces_d)) symmetry.irreps_allowed = ["A"] @@ -133,32 +140,80 @@ def guess_symmetry_doubles(matrix, spin_change=0, raise NotImplementedError("spin_symmetrisation != 'none' only " "implemented for spin_change == 0") - if spin_change == 0 \ - and spin_block_symmetrisation in ("symmetric", "antisymmetric"): - fac = 1 if spin_block_symmetrisation == "symmetric" else -1 - # Spin mapping between blocks where alpha and beta are just mirrored - symmetry.spin_block_maps = [("aaaa", "bbbb", fac), - ("abab", "baba", fac), - ("abba", "baab", fac)] - - # Mark blocks which change spin as forbidden - symmetry.spin_blocks_forbidden = ["aabb", # spin_change +2 - "bbaa", # spin_change -2 - "aaab", # spin_change +1 - "aaba", # spin_change +1 - "abaa", # spin_change -1 - "baaa", # spin_change -1 - "abbb", # spin_change +1 - "babb", # spin_change +1 - "bbab", # spin_change -1 - "bbba"] # spin_change -1 - - # Add index permutation symmetry: - permutations = ["ijab"] - if spaces_d[0] == spaces_d[1]: - permutations.append("-jiab") - if spaces_d[2] == spaces_d[3]: - permutations.append("-ijba") - if len(permutations) > 1: - symmetry.permutations = permutations + if matrix.method.adc_type == "pp": + # PP-ADC + if spin_block_symmetrisation in ("symmetric", "antisymmetric"): + fac = 1 if spin_block_symmetrisation == "symmetric" else -1 + # Spin mapping between blocks where alpha and beta are mirrored + symmetry.spin_block_maps = [("aaaa", "bbbb", fac), + ("abab", "baba", fac), + ("abba", "baab", fac)] + + # Mark blocks which change spin as forbidden + symmetry.spin_blocks_forbidden = ["aabb", # spin_change -2 + "bbaa", # spin_change +2 + "aaab", # spin_change -1 + "aaba", # spin_change -1 + "abaa", # spin_change +1 + "baaa", # spin_change +1 + "abbb", # spin_change -1 + "babb", # spin_change -1 + "bbab", # spin_change +1 + "bbba"] # spin_change +1 + + # Add index permutation symmetry: + permutations = ["ijab"] + if spaces_d[0] == spaces_d[1]: + permutations.append("-jiab") + if spaces_d[2] == spaces_d[3]: + permutations.append("-ijba") + if len(permutations) > 1: + symmetry.permutations = permutations + + elif matrix.method.adc_type == "ip": + # IP-ADC + # No spin mapping between blocks + symmetry.spin_block_maps = [] + + # Mark blocks which change spin incorrectly as forbidden + if matrix.reference_state.restricted: # kind=doublet + # detach alpha electron + # forbidden beta ionization blocks not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["aab", # spin_change -3/2 + "bba", # spin_change +3/2 + "aba", # spin_change +1/2 + "baa", # spin_change +1/2 + "bbb"] # spin_change +1/2 + + # Add index permutation symmetry: + permutations = ["ija"] + if spaces_d[0] == spaces_d[1]: + permutations.append("-jia") + if len(permutations) > 1: + symmetry.permutations = permutations + + else: + # EA-ADC + # No spin mapping between blocks + symmetry.spin_block_maps = [] + + # Mark blocks which change spin incorrectly as forbidden + if matrix.reference_state.restricted: # kind=doublet + # attach alpha electron + # forbidden beta attachment blocks not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["baa", # spin_change +3/2 + "abb", # spin_change -3/2 + "aab", # spin_change -1/2 + "aba", # spin_change -1/2 + "bbb"] # spin_change -1/2 + + # Add index permutation symmetry: + permutations = ["iab"] + if spaces_d[1] == spaces_d[2]: + permutations.append("-iba") + if len(permutations) > 1: + symmetry.permutations = permutations + return symmetry diff --git a/adcc/guess/guesses_from_diagonal.py b/adcc/guess/guesses_from_diagonal.py index 2d94f9720..a8224178f 100644 --- a/adcc/guess/guesses_from_diagonal.py +++ b/adcc/guess/guesses_from_diagonal.py @@ -31,7 +31,8 @@ from .guess_zero import guess_zero -def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, +def guesses_from_diagonal(matrix, n_guesses, block="ph", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): """ @@ -45,23 +46,26 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, vectors are returned if this many could not be found. block Diagonal block to use for obtaining the guesses (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the + respective IP-/EA-ADC calculation. spin_change The spin change to enforce in an excitation. - Typical values are 0 (singlet/triplet/any) and -1 (spin-flip). + Typical values are 0 (singlet/triplet/any), +/- 0.5 (doublet) + and -1 (spin-flip). spin_block_symmetrisation - Symmetrisation to enforce between equivalent spin blocks, which - all yield the desired spin_change. E.g. if spin_change == 0, - then both the alpha->alpha and beta->beta blocks of the singles - part of the excitation vector achieve a spin change of 0. - The symmetry specified with this parameter will then be imposed - between the a-a and b-b blocks. Valid values are "none", - "symmetric" and "antisymmetric", where "none" enforces - no particular symmetry. + Symmetrisation to enforce between equivalent spin blocks, + which all yield the desired spin_change. E.g. if + spin_change == 0, then both the alpha->alpha and beta->beta + blocks of the singles part of the excitation vector achieve a + spin change of 0. The symmetry specified with this parameter + will then be imposed between the a-a and b-b blocks. Valid + values are "none", "symmetric" and "antisymmetric", where + "none" enforces no particular symmetry. degeneracy_tolerance Tolerance for two entries of the diagonal to be considered degenerate, i.e. identical. max_diagonal_value - Maximal diagonal value, which is considered as a valid candidate - to form a guess. + Maximal diagonal value, which is considered as a valid + candidate to form a guess. """ if not isinstance(matrix, AdcMatrixlike): raise TypeError("matrix needs to be of type AdcMatrixlike") @@ -74,8 +78,8 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, "ADC calculations on top of restricted reference " "states.") if int(spin_change * 2) / 2 != spin_change: - raise ValueError("Only integer or half-integer spin_change is allowed. " - "You passed {}".format(spin_change)) + raise ValueError("Only integer or half-integer spin_change is allowed." + " You passed {}".format(spin_change)) if block not in matrix.axis_blocks: raise ValueError("The passed ADC matrix does not have the block '{}.'" @@ -83,14 +87,15 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, if n_guesses == 0: return [] - if block == "ph": + if block in ["h", "p", "ph"]: guessfunction = guesses_from_diagonal_singles - elif block == "pphh": + elif block in ["phh", "pph", "pphh"]: guessfunction = guesses_from_diagonal_doubles else: - raise ValueError(f"Don't know how to generate guesses for block {block}") + raise ValueError("Don't know how to generate guesses for block " + f"{block}") - return guessfunction(matrix, n_guesses, spin_change, + return guessfunction(matrix, n_guesses, block, kind, is_alpha, spin_change, spin_block_symmetrisation, degeneracy_tolerance, max_diagonal_value) @@ -127,8 +132,8 @@ def spin_change(self): ("v", "a"): +0.5, # add alpha ("v", "b"): -0.5, # add beta } - return int(sum(mapping_spin_change[(space[0], spin)] - for space, spin in zip(self.subspaces, self.spin_block))) + return sum(mapping_spin_change[(space[0], spin)] + for space, spin in zip(self.subspaces, self.spin_block)) def __repr__(self): return f"({self.index} {self.value})" @@ -197,11 +202,12 @@ def telem_nospin(telem): return res -def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0, +def guesses_from_diagonal_singles(matrix, n_guesses, block="ph", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): - motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces["ph"]) + motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces[block]) if n_guesses == 0: return [] @@ -216,12 +222,12 @@ def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0, # Also it filters out too large diagonal entries (which are essentially # hopeless to give useful excitations) def pred_singles(telem): - return (ret[0].ph.is_allowed(telem.index) + return (ret[0].get(block).is_allowed(telem.index) and telem.spin_change == spin_change and abs(telem.value) <= max_diagonal_value) elements = find_smallest_matching_elements( - pred_singles, matrix.diagonal().ph, motrans, n_guesses, + pred_singles, matrix.diagonal().get(block), motrans, n_guesses, degeneracy_tolerance=degeneracy_tolerance ) if len(elements) == 0: @@ -245,25 +251,25 @@ def telem_nospin(telem): group = list(group) if len(group) == 1: # Just add the single vector - ret[ivec].ph[group[0].index] = 1.0 + ret[ivec].get(block)[group[0].index] = 1.0 ivec += 1 elif len(group) == 2: # Since these two are grouped together, their # spatial parts must be identical. # Add the positive linear combination ... - ret[ivec].ph[group[0].index] = 1 - ret[ivec].ph[group[1].index] = 1 + ret[ivec].get(block)[group[0].index] = 1 + ret[ivec].get(block)[group[1].index] = 1 ivec += 1 # ... and the negative linear combination if ivec < n_guesses: - ret[ivec].ph[group[0].index] = +1 - ret[ivec].ph[group[1].index] = -1 + ret[ivec].get(block)[group[0].index] = +1 + ret[ivec].get(block)[group[1].index] = -1 ivec += 1 else: raise AssertionError("group size > 3 should not occur " - "when setting up single guesse.") + "when setting up single guesses.") assert ivec <= n_guesses # Resize in case less guesses found than requested @@ -271,7 +277,8 @@ def telem_nospin(telem): return [evaluate(v / np.sqrt(v @ v)) for v in ret[:ivec]] -def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0, +def guesses_from_diagonal_doubles(matrix, n_guesses, block="pphh", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): @@ -283,24 +290,41 @@ def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0, spin_block_symmetrisation=spin_block_symmetrisation) for _ in range(n_guesses)] - # Build delta-Fock matrices - spaces_d = matrix.axis_spaces["pphh"] - df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2]) - df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3]) - - guesses_d = [gv.pphh for gv in ret] # Extract doubles parts spin_change_twice = int(spin_change * 2) assert spin_change_twice / 2 == spin_change - n_found = libadcc.fill_pp_doubles_guesses( - guesses_d, matrix.mospaces, df02, df13, - spin_change_twice, degeneracy_tolerance - ) + # Extract doubles parts + guesses_d = [gv.get(block) for gv in ret] + + if matrix.method.adc_type == "pp": + # PP-ADC + spaces_d = matrix.axis_spaces[block] + # Build delta-Fock matrices + df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2]) + df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3]) + n_found = libadcc.fill_pp_doubles_guesses( + guesses_d, matrix.mospaces, df02, df13, + spin_change_twice, degeneracy_tolerance + ) + else: + # IP- and EA-ADC + # Build Fock matrices and multiply occ. orbitals with -1 to invert the + # order for simplicity in the C++ code + d_o = matrix.reference_state.foo.diagonal() * (-1) + d_v = matrix.reference_state.fvv.diagonal() + doublet = (kind == "doublet") + is_restricted = matrix.reference_state.restricted + double_guess_func = {"ip": libadcc.fill_ip_doubles_guesses, + "ea": libadcc.fill_ea_doubles_guesses} + n_found = double_guess_func[matrix.method.adc_type]( + guesses_d, matrix.mospaces, d_o, d_v, is_alpha, is_restricted, + doublet, spin_change_twice, degeneracy_tolerance) + # Resize in case less guesses found than requested ret = ret[:n_found] # Filter out elements above the noted diagonal value - diagonal_elements = [ret_d.pphh.dot(matrix.diagonal().pphh * ret_d.pphh) - for ret_d in ret] + diagonal_elements = [ret_d.get(block).dot(matrix.diagonal().get(block) + * ret_d.get(block)) for ret_d in ret] return [ret[i] for (i, elem) in enumerate(diagonal_elements) if elem <= max_diagonal_value] diff --git a/adcc/workflow.py b/adcc/workflow.py index 3c512b68d..a23063384 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -28,6 +28,7 @@ from . import solver from .guess import (guesses_any, guesses_singlet, guesses_doublet, guesses_spin_flip, guesses_triplet) +from .guess import Guesses from .LazyMp import LazyMp from .AdcMatrix import AdcMatrix, AdcMatrixlike, AdcExtraTerm from .AdcMethod import AdcMethod @@ -43,7 +44,7 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, - eigensolver=None, guesses=None, n_guesses=None, + eigensolver="davidson", guesses=None, n_guesses=None, n_guesses_doubles=None, output=sys.stdout, core_orbitals=None, frozen_core=None, frozen_virtual=None, method=None, n_singlets=None, n_doublets=None, n_triplets=None, @@ -214,35 +215,10 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, matrix, n_states=n_states, n_singlets=n_singlets, n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, kind=kind, is_alpha=is_alpha) - - # Set matrix attribute is_spin_flip to track whether it is a spin_flip - # calculation. Simplifications later? - matrix.method.is_spin_flip = kind == "spin_flip" - - # Determine spin change during excitation. If guesses is not None, - # i.e. user-provided, we cannot guarantee for obtaining a particular - # spin_change in case of a spin_flip calculation. - if guesses: - spin_change = None - elif matrix.method.adc_type == "pp": - if kind == "spin_flip": - spin_change = -1 - else: - spin_change = 0 - elif matrix.method.adc_type == "ip": - if kind == "spin_flip": - spin_change = 0 - else: - spin_change = +0.5 - int(is_alpha) - elif matrix.method.adc_type == "ea": - if kind == "spin_flip": - spin_change = 0 - else: - spin_change = -0.5 + int(is_alpha) - - # Select solver to run - if eigensolver is None: - eigensolver = "davidson" + + # Construct guesses + solver_guesses = construct_guesses(matrix, n_states, guesses, n_guesses, + n_guesses_doubles, kind, is_alpha, eigensolver) # Setup environment coupling terms and energy corrections env_matrix_term, env_energy_corrections = setup_environment(matrix, @@ -251,13 +227,16 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, if env_matrix_term: matrix += env_matrix_term diagres = diagonalise_adcmatrix( - matrix, n_states, kind, guesses=guesses, n_guesses=n_guesses, - n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output, - eigensolver=eigensolver, is_alpha=is_alpha, - spin_change=spin_change, **solverargs) + matrix, n_states, guesses=guesses, conv_tol=conv_tol, output=output, + eigensolver=eigensolver, **solverargs) + # diagres = diagonalise_adcmatrix( + # matrix, n_states, kind, guesses=guesses, n_guesses=n_guesses, + # n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output, + # eigensolver=eigensolver, is_alpha=is_alpha, + # spin_change=spin_change, **solverargs) exstates = ExcitedStates(diagres) exstates.kind = kind - exstates.spin_change = spin_change + exstates.spin_change = solver_guesses.spin_change exstates.is_alpha = is_alpha # add environment corrections to excited states @@ -420,9 +399,8 @@ def validate_state_parameters(matrix, n_states=None, n_singlets=None, "ADC calculations in combination with an unrestricted" " ground state.") if kind in ["spin_flip", "singlet", "triplet"] and adc_type != "pp": - raise InputError("kind==singlet, kind==triplet and kind==spin_flip are" - " only valid for PP-ADC calculations in combination " - "with a restricted ground state.") + raise InputError("kind==singlet, kind==triplet, and kind==spin_flip " + "are only valid for PP-ADC calculations.") if kind == "doublet" and adc_type == "pp": raise InputError("kind==doublet is only valid for IP/EA-ADC " "calculations in combination with a restricted " @@ -430,6 +408,43 @@ def validate_state_parameters(matrix, n_states=None, n_singlets=None, return n_states, kind, is_alpha +def construct_guesses(matrix, n_states, guesses=None, n_guesses=None, + n_guesses_doubles=None, kind="any", is_alpha=None, + eigensolver="davidson"): + """ + This function constructs appropriate guesses if not given. + Returns a class object containing all crucial guess information. + Internal function called from run_adc. + """ + solver_guesses = Guesses(guesses, kind, matrix.method.adc_type, is_alpha) + + # Obtain or check guesses + if guesses is None: + if n_guesses is None: + # Set solver-specific parameters + if eigensolver == "davidson": + n_guesses_per_state = 2 + else: + n_guesses_per_state = 1 + solver_guesses.estimate_n_guesses(matrix, n_states, + n_guesses_per_state) + solver_guesses.obtain_guesses_by_inspection(matrix, n_guesses_doubles) + else: + if len(guesses) < n_states: + raise InputError("Less guesses provided via guesses (== {}) " + "than states to be computed (== {})" + "".format(len(guesses), n_states)) + if n_guesses is not None: + warnings.warn("Ignoring n_guesses parameter, since guesses are " + "explicitly provided.") + if n_guesses_doubles is not None: + warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" + " are explicitly provided.") + solver_guesses.n_guesses = len(guesses) + + return solver_guesses + + def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", guesses=None, n_guesses=None, n_guesses_doubles=None, conv_tol=None, output=sys.stdout, is_alpha=None, @@ -453,20 +468,18 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", # Determine explicit_symmetrisation explicit_symmetrisation = IndexSymmetrisation - if kind in ["singlet", "doublet", "triplet"]: + if solver_guesses.kind in ["singlet", "doublet", "triplet"]: explicit_symmetrisation = IndexSpinSymmetrisation( - matrix, enforce_spin_kind=kind + matrix, enforce_spin_kind=solver_guesses.kind ) # Set some solver-specific parameters if eigensolver == "davidson": - n_guesses_per_state = 2 callback = setup_solver_printing( "Jacobi-Davidson", matrix, kind, solver.davidson.default_print, is_alpha=is_alpha, output=output) run_eigensolver = jacobi_davidson elif eigensolver == "lanczos": - n_guesses_per_state = 1 callback = setup_solver_printing( "Lanczos", matrix, kind, solver.lanczos.default_print, is_alpha=is_alpha, output=output) @@ -474,26 +487,6 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", else: raise InputError(f"Solver {eigensolver} unknown, try 'davidson'.") - # Obtain or check guesses - if guesses is None: - if n_guesses is None: - n_guesses = estimate_n_guesses(matrix, n_states, - n_guesses_per_state) - guesses = obtain_guesses_by_inspection(matrix, n_guesses, kind, - n_guesses_doubles, is_alpha, - spin_change=spin_change) - else: - if len(guesses) < n_states: - raise InputError("Less guesses provided via guesses (== {}) " - "than states to be computed (== {})" - "".format(len(guesses), n_states)) - if n_guesses is not None: - warnings.warn("Ignoring n_guesses parameter, since guesses are " - "explicitly provided.") - if n_guesses_doubles is not None: - warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" - " are explicitly provided.") - solverargs.setdefault("which", "SA") return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol, callback=callback, @@ -501,95 +494,6 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", **solverargs) -def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2, - singles_only=True): - """ - Implementation of a basic heuristic to find a good number of guess - vectors to be searched for using the find_guesses function. - Internal function called from run_adc. - - matrix ADC matrix - n_states Number of states to be computed - singles_only Try to stay withing the singles excitation space - with the number of guess vectors. - n_guesses_per_state Number of guesses to search for for each state - """ - # Try to use at least 4 or twice the number of states - # to be computed as guesses - n_guesses = n_guesses_per_state * max(2, n_states) - - if singles_only: - # Compute the maximal number of sensible singles block guesses. - # This is roughly the number of occupied alpha orbitals - # times the number of virtual alpha orbitals - # - # If the system is core valence separated, then only the - # core electrons count as "occupied". - mospaces = matrix.mospaces - sp_occ = "o2" if matrix.is_core_valence_separated else "o1" - n_virt_a = mospaces.n_orbs_alpha("v1") - n_occ_a = mospaces.n_orbs_alpha(sp_occ) - estimate = n_occ_a * n_virt_a - if matrix.method.level < 2 and matrix.method.adc_type != "pp": - # Adjustment for IP- and EA-ADC(0/1) calculations - estimate = n_occ_a if matrix.method.adc_type == "ip" else n_virt_a - n_guesses = min(n_guesses, estimate) - - # Adjust if we overshoot the maximal number of sensible singles block - # guesses, but make sure we get at least n_states guesses - return max(n_states, n_guesses) - - -def obtain_guesses_by_inspection(matrix, n_guesses, kind, - n_guesses_doubles=None, is_alpha=None, - spin_change=None): - """ - Obtain guesses by inspecting the diagonal matrix elements. - If n_guesses_doubles is not None, this is number is always adhered to. - Otherwise the number of doubles guesses is adjusted to fill up whatever - the singles guesses cannot provide to reach n_guesses. - Internal function called from run_adc. - """ - if n_guesses_doubles is not None and n_guesses_doubles > 0 \ - and not set(["phh", "pph", "pphh"]) & set(matrix.axis_blocks): - raise InputError("n_guesses_doubles > 0 is only sensible if the ADC " - "method has a doubles block (i.e. it is *not* ADC(0)," - " ADC(1) or a variant thereof.") - - # Determine guess function - guess_function = {"any": guesses_any, "singlet": guesses_singlet, - "doublet": guesses_doublet, "triplet": guesses_triplet, - "spin_flip": guesses_spin_flip}[kind] - - # Determine number of singles guesses to request - n_guess_singles = n_guesses - if n_guesses_doubles is not None: - n_guess_singles = n_guesses - n_guesses_doubles - singles_guesses = guess_function(matrix, n_guess_singles, - block=matrix.axis_blocks[0], - is_alpha=is_alpha, - spin_change=spin_change) - - doubles_guesses = [] - if set(["phh", "pph", "pphh"]) & set(matrix.axis_blocks): - # Determine number of doubles guesses to request if not - # explicitly specified - if n_guesses_doubles is None: - n_guesses_doubles = n_guesses - len(singles_guesses) - if n_guesses_doubles > 0: - doubles_guesses = guess_function(matrix, n_guesses_doubles, - block=matrix.axis_blocks[1], - kind=kind, - is_alpha=is_alpha, - spin_change=spin_change) - - total_guesses = singles_guesses + doubles_guesses - if len(total_guesses) < n_guesses: - raise InputError("Less guesses found than requested: {} found, " - "{} requested".format(len(total_guesses), n_guesses)) - return total_guesses - - def setup_solver_printing(solmethod_name, matrix, kind, default_print, is_alpha=None, output=None): """ diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.cc b/libadcc_src/amplitude_vector_enforce_spin_kind.cc index e5727efc4..fb4d6f04a 100644 --- a/libadcc_src/amplitude_vector_enforce_spin_kind.cc +++ b/libadcc_src/amplitude_vector_enforce_spin_kind.cc @@ -31,7 +31,8 @@ namespace libadcc { namespace lt = libtensor; void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, - std::string block, std::string spin_kind) { + std::string block, + std::string spin_kind, bool is_ip) { // Nothing to do for singles block if (block == "s") return; @@ -64,10 +65,130 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, return; } + if (spin_kind == "doublet") { + auto& u2 = asbt3(doubles_tensor); + lt::block_tensor_ctrl<3, scalar_type> ctrl(u2); + const lt::symmetry<3, scalar_type>& sym = ctrl.req_const_symmetry(); + + // Extract the number of blocks per dimension + const lt::block_index_space<3>& bis = sym.get_bis(); + lt::dimensions<3> bidims(bis.get_block_index_dims()); + + // Setup i1 to point to 0,0,0 and i2 to the half of the + // full number of blocks, i.e. to the alpha blocks in each + // dimension only. + lt::index<3> i1, i2; + for (size_t i = 0; i < 3; i++) i2[i] = bidims[i] / 2 - 1; + + // Index range over all alpha-alpha-alpha blocks + // in all point group symmetries + const lt::index_range<3> index_range_alpha(i1, i2); + + // This dimensions object contains the number of alpha blocks per dimension + lt::dimensions<3> bidims_alpha(index_range_alpha); + + // Iterate over all alpha-alpha-alpha blocks + lt::abs_index<3> ai(bidims_alpha); + do { + // This gives the block index tuple + const lt::index<3>& ii = ai.get_index(); + + // Construct the orbit corresponding to this index (i.e. the iterator + // running over all elements equivalent by symmetry + // Ignore spin-forbidden, i.e. zero orbits + lt::short_orbit<3, scalar_type> orbi(sym, ii, /* compute_if_allowed_orbit = */ true); + if (!orbi.is_allowed()) continue; + + // get_acindex -> get absolute canonical index + // Continue if our current index is larger than the canonical index + if (orbi.get_acindex() < ai.get_abs_index()) continue; + + // TODO This might be wrong ... think about it and talk to Adrian + // the point is that orbi might have other strides than bidims_alpha + // Continue if the canonical index is already past the + // alpha-alpha-alpha block + if (orbi.get_acindex() > bidims_alpha.get_size()) continue; + + // Get the index tuple of the canonical block of (alpha, alpha, alpha) + const lt::index<3>& ci = orbi.get_cindex(); + + lt::index<3> i1(ci); + lt::index<3> i2(ci); + + if (is_ip) { // IP-ADC calculation + // set i1 to (alpha, beta, beta) equivalent of the canonical index + // pinned by ai and orbi + i1[1] += bidims_alpha[1]; + i1[2] += bidims_alpha[2]; + + // set i2 to (beta, alpha, beta) + i2[0] += bidims_alpha[0]; + i2[2] += bidims_alpha[2]; + + } else { // EA-ADC calculation + // set i1 to (beta, alpha, beta) equivalent of the canonical index + // pinned by ai and orbi + i1[0] += bidims_alpha[0]; + i1[2] += bidims_alpha[2]; + + // set i2 to (beta, beta, alpha) + i2[0] += bidims_alpha[0]; + i2[1] += bidims_alpha[1]; + } + + // + // What the following code does is that it keeps the spin projection (S^2) + // properly, provided that the symmetry setup is done as in + // contrib/adc_pp/adc_guess_d.C. It assumes the coefficients as setup in + // contrib/adc_pp/adc_guess_d.C adc_guess_d::build_guesses in order to preserve + // S^2 value setup in the guess. + // + + lt::orbit<3, scalar_type> orb1(sym, i1, false), orb2(sym, i2, false); + // Canonical block of (a, b, b) for IP-ADC or (b, a, b) for EA-ADC + const lt::index<3>& ci1 = orb1.get_cindex(); + // Canonical block of (b, a, b) for IP-ADC or (b, b, a) for EA-ADC + const lt::index<3>& ci2 = orb2.get_cindex(); + bool zero1 = ctrl.req_is_zero_block(ci1); + bool zero2 = ctrl.req_is_zero_block(ci2); + if (zero1 && zero2) { + // Set (alpha, alpha, alpha) to zero + // This effectively filters out the quartet components with zero blocks + // in (alpha, beta, beta) and (beta, alpha, beta) (IP-ADC) erroneously + // introduced due to numerical errors. (beta, alpha, beta) and + // (beta, beta, alpha) blocks for EA-ADC. + ctrl.req_zero_block(ci); + continue; + } + + // Get block corresponding to canonical index of (alpha, alpha, alpha) + lt::dense_tensor_wr_i<3, scalar_type>& blk = ctrl.req_block(ci); + + if (!zero1) { + // IP: (alpha, beta, beta) / EA: (beta, alpha, beta) is not zero + lt::dense_tensor_rd_i<3, scalar_type>& blk1 = ctrl.req_const_block(ci1); + lt::tod_copy<3>(blk1, orb1.get_transf(i1)).perform(/* assign= */ true, blk); + ctrl.ret_const_block(ci1); + } + if (!zero2) { + // IP: (beta, alpha, beta) / EA: (beta, beta, alpha) is not zero + lt::dense_tensor_rd_i<3, scalar_type>& blk2 = ctrl.req_const_block(ci2); + + // Assign if (alpha, beta, beta) for IP- and (beta, alpha, beta) for + // EA-ADC is zero, else += + lt::tod_copy<3>(blk2, orb2.get_transf(i2)).perform(zero1, blk); + ctrl.ret_const_block(ci2); + } + ctrl.ret_block(ci); + + } while (ai.inc()); + return; + } + if (spin_kind != "singlet") { throw not_implemented_error( - "Only implemented for spin_kind == 'singlet' and spin_kind == " - "'triplet'."); + "Only implemented for spin_kind == 'singlet', spin_kind == " + "'triplet' or spin_kind == 'doublet'."); } auto& u2 = asbt4(doubles_tensor); @@ -169,4 +290,4 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, } while (ai.inc()); } -} // namespace libadcc +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.hh b/libadcc_src/amplitude_vector_enforce_spin_kind.hh index 6a7a4dbd8..7e6eaddfb 100644 --- a/libadcc_src/amplitude_vector_enforce_spin_kind.hh +++ b/libadcc_src/amplitude_vector_enforce_spin_kind.hh @@ -36,7 +36,9 @@ namespace libadcc { * @param block The block of an amplitude this tensor represents * @param spin_kind The kind of spin to enforce */ -void amplitude_vector_enforce_spin_kind(std::shared_ptr tensor, std::string block, - std::string spin_kind); +void amplitude_vector_enforce_spin_kind(std::shared_ptr tensor, + std::string block, + std::string spin_kind, + bool is_ip); ///@} } // namespace libadcc diff --git a/libadcc_src/fill_ea_doubles_guesses.cc b/libadcc_src/fill_ea_doubles_guesses.cc new file mode 100644 index 000000000..b3d5c2f32 --- /dev/null +++ b/libadcc_src/fill_ea_doubles_guesses.cc @@ -0,0 +1,78 @@ +// +// Copyright (C) 2020 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "fill_ea_doubles_guesses.hh" +#include "TensorImpl.hh" +#include "guess/ea_adc_guess_d.hh" + +namespace libadcc { + +size_t fill_ea_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, + std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, + scalar_type degeneracy_tolerance) { + + size_t n_guesses = guesses_d.size(); + if (n_guesses == 0) return 0; + + // Make a copy of the doubles symmetry + libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0])); + libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis()); + libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s); + + // Make ab pointers object + auto make_ab = [](const MoSpaces& mo, const std::string& space) { + const std::vector& block_spin = mo.map_block_spin.at(space); + std::vector ab; + for (size_t i = 0; i < block_spin.size(); ++i) { + ab.push_back(block_spin[i] == 'b'); + } + return ab; + }; + + const std::vector spaces_d = guesses_d[0]->subspaces(); + std::vector> abvectors; + for (size_t i = 0; i < 3; ++i) { + abvectors.push_back(make_ab(*mospaces, spaces_d[i])); + } + libtensor::sequence<3, std::vector*> ab_d; + for (size_t i = 0; i < 3; ++i) { + ab_d[i] = &abvectors[i]; + } + + // Make singles list data structure + std::list*, double>> guesspairs; + for (size_t i = 0; i < n_guesses; i++) { + guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0); + } + + if (abs(spin_change_twice) != 1){ + throw not_implemented_error("spin_change ==" + + std::to_string(spin_change_twice) + " has not been tested."); + } + + return ea_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin, + restricted, doublet, ab_d, spin_change_twice, + degeneracy_tolerance); +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/fill_ea_doubles_guesses.hh b/libadcc_src/fill_ea_doubles_guesses.hh new file mode 100644 index 000000000..4d6c3c072 --- /dev/null +++ b/libadcc_src/fill_ea_doubles_guesses.hh @@ -0,0 +1,53 @@ +// +// Copyright (C) 2020 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#pragma once +#include "Tensor.hh" + +namespace libadcc { + + +/** Fill the passed vector of doubles blocks with doubles guesses using + * the O and V matrices. + * + * + * guesses_d Vectors of guesses, all elements are assumed to be initialised to zero + * and the symmetry is assumed to be properly set up. + * mospaces Mospaces object + * d_o Fock matrix to construct guesses from (occ.) + * d_v Fock matrix to construct guesses from (virt.) + * a_spin If alpha ionization (false: beta) + * restricted Is this a restricted calculation + * doublet Doublet or quartet states (only in case of restricted calculation) + * spin_change_twice Twice the value of the spin change to enforce in an excitation. + * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered + * degenerate, i.e. identical. + * + * \returns The number of guess vectors which have been properly initialised + * (the others are invalid and should be discarded). + */ +size_t fill_ea_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, + std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, + scalar_type degeneracy_tolerance); + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/fill_ip_doubles_guesses.cc b/libadcc_src/fill_ip_doubles_guesses.cc new file mode 100644 index 000000000..aa8885d02 --- /dev/null +++ b/libadcc_src/fill_ip_doubles_guesses.cc @@ -0,0 +1,78 @@ +// +// Copyright (C) 2020 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "fill_ip_doubles_guesses.hh" +#include "TensorImpl.hh" +#include "guess/ip_adc_guess_d.hh" + +namespace libadcc { + +size_t fill_ip_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, + std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, + scalar_type degeneracy_tolerance) { + + size_t n_guesses = guesses_d.size(); + if (n_guesses == 0) return 0; + + // Make a copy of the doubles symmetry + libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0])); + libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis()); + libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s); + + // Make ab pointers object + auto make_ab = [](const MoSpaces& mo, const std::string& space) { + const std::vector& block_spin = mo.map_block_spin.at(space); + std::vector ab; + for (size_t i = 0; i < block_spin.size(); ++i) { + ab.push_back(block_spin[i] == 'b'); + } + return ab; + }; + + const std::vector spaces_d = guesses_d[0]->subspaces(); + std::vector> abvectors; + for (size_t i = 0; i < 3; ++i) { + abvectors.push_back(make_ab(*mospaces, spaces_d[i])); + } + libtensor::sequence<3, std::vector*> ab_d; + for (size_t i = 0; i < 3; ++i) { + ab_d[i] = &abvectors[i]; + } + + // Make singles list data structure + std::list*, double>> guesspairs; + for (size_t i = 0; i < n_guesses; i++) { + guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0); + } + + if (abs(spin_change_twice) != 1){ + throw not_implemented_error("spin_change ==" + + std::to_string(spin_change_twice) + " has not been tested."); + } + + return ip_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin, + restricted, doublet, ab_d, spin_change_twice, + degeneracy_tolerance); +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/fill_ip_doubles_guesses.hh b/libadcc_src/fill_ip_doubles_guesses.hh new file mode 100644 index 000000000..10bbf1d2d --- /dev/null +++ b/libadcc_src/fill_ip_doubles_guesses.hh @@ -0,0 +1,53 @@ +// +// Copyright (C) 2020 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#pragma once +#include "Tensor.hh" + +namespace libadcc { + + +/** Fill the passed vector of doubles blocks with doubles guesses using + * the O and V matrices. + * + * + * guesses_d Vectors of guesses, all elements are assumed to be initialised to zero + * and the symmetry is assumed to be properly set up. + * mospaces Mospaces object + * d_o Fock matrix to construct guesses from (occ.) + * d_v Fock matrix to construct guesses from (virt.) + * a_spin If alpha ionization (false: beta) + * restricted Is this a restricted calculation + * doublet Doublet or quartet states (only in case of restricted calculation) + * spin_change_twice Twice the value of the spin change to enforce in an excitation. + * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered + * degenerate, i.e. identical. + * + * \returns The number of guess vectors which have been properly initialised + * (the others are invalid and should be discarded). + */ +size_t fill_ip_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, + std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, + scalar_type degeneracy_tolerance); + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/ea_adc_guess_d.cc b/libadcc_src/guess/ea_adc_guess_d.cc new file mode 100644 index 000000000..9f432d69c --- /dev/null +++ b/libadcc_src/guess/ea_adc_guess_d.cc @@ -0,0 +1,484 @@ +#include "ea_adc_guess_d.hh" +#include "../exceptions.hh" + +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#include +#include +#include +#include +#include +#pragma GCC visibility pop + +namespace libadcc { + +// TODO This file definitely needs a cleanup. + +using namespace libtensor; +using libtensor::index; + +/** \brief Element type for guess vectors + **/ +template +struct guess_element { + libtensor::index bidx; //!< Block index + libtensor::index idx; //!< In block index + double coeff; //!< Coefficient; + + guess_element(const libtensor::index& bidx_, const libtensor::index& idx_, + const double& coeff_) + : bidx(bidx_), idx(idx_), coeff(coeff_) {} +}; + +/** \brief Base class for guess formation **/ +template +class index_handler { + public: + protected: + libtensor::sequence*> m_ab; //!< Alpha-beta block markers + + private: + libtensor::sequence m_na; //!< Number of alpha spin blocks + + public: + /** \brief Constructor + \param ab Alpha-beta block markers (for N orbital spaces) + \param sym Symmetry of guess vectors + \param ms Spin multiplicity + **/ + index_handler(const libtensor::sequence*>& ab) + : m_ab(ab), m_na(0) { + for (size_t i = 0; i < N; i++) { + for (size_t j = 0; j < m_ab[i]->size(); j++) { + if (!m_ab[i]->at(j)) m_na[i]++; + } + } + } + + /** \brief Calculates the spin projection \f$ m_s \f$ of the block. + \param bidx Block index + \param orb_type Orbital type per dim (true = occupied) + \return -1 or +1 for ionization of an alpha or beta electron + */ + int get_spin_proj(const libtensor::mask& orb_type, + const libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + if (bidx[i] > m_ab[i]->size()) { + throw runtime_error("Block index exceeds dim"); + } + } + + int ms = 0; + for (size_t i = 0; i < N; i++) { + // Left side is true if orbital is occ. + // Right side is true if orbital has beta spin + // Hence it is true for occ. beta orbitals and virt. alpha orbitals + if (orb_type[i] == m_ab[i]->at(bidx[i])) + ms += 1; + else + ms -= 1; + } + return ms; + } + + /** \brief Split block index into spatial part and spin part + \param bidx Input block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + **/ + void split_block_index(const libtensor::index& bidx, libtensor::mask& sp, + libtensor::index& sbidx) const { + for (size_t i = 0; i < N; i++) { + sp[i] = m_ab[i]->at(bidx[i]); + sbidx[i] = (sp[i] ? bidx[i] - m_na[i] : bidx[i]); + } + } + + /** \brief Merge spatial part and spin part of block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + \param bidx Input block index + **/ + void merge_block_index(const libtensor::mask& sp, const libtensor::index& sbidx, + libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + bidx[i] = (sp[i] ? sbidx[i] + m_na[i] : sbidx[i]); + } + } +}; + +namespace { +typedef libtensor::compare4min compare_t; +typedef libtensor::btod_select<1, compare_t>::list_type list1d_t; +typedef libtensor::btod_select<3, compare_t>::list_type list3d_t; +typedef std::list*, double>> list_t; + +/** Determine if occupied indices should be symmetrized */ +void determine_sym(const symmetry<3, double>& sym, bool& sym_v) { + + sym_v = false; + for (symmetry<3, double>::iterator it1 = sym.begin(); it1 != sym.end(); it1++) { + + const symmetry_element_set<3, double>& set = sym.get_subset(it1); + const std::string& id = set.get_id(); + + if (id.compare(se_perm<3, double>::k_sym_type) != 0) continue; + if (set.is_empty()) return; + + typedef symmetry_element_set_adapter<3, double, se_perm<3, double>> adapter_t; + + adapter_t ad(set); + for (adapter_t::iterator it2 = ad.begin(); it2 != ad.end(); it2++) { + + const se_perm<3, double>& el = ad.get_elem(it2); + + const permutation<3>& p = el.get_perm(); + sym_v |= (p[1] == 2 && p[2] == 1); + } + } +} + +/** Determine the spin of the guess vectors */ +unsigned determine_spin(bool restricted, bool doublet) { + + if (restricted) { + if (doublet) return 2; + else return 4; + } else { + return 0; + } +} + +/** Transfers the elements of a 1D list to a 3D list */ +void transfer_elements(const list1d_t& o, const list1d_t& v, + index_group_map_h2p& to, const libtensor::symmetry<3, + double>& sym, const index_handler<3>& base, int dm_s) { + + // Determine symmetry + bool sym_v; // Are the two virtual indices identical + determine_sym(sym, sym_v); + + to.clear(); + + dimensions<3> bidims = sym.get_bis().get_block_index_dims(); + + for (list1d_t::const_iterator ita = o.begin(); ita != o.end(); ita++) { + + for (list1d_t::const_iterator itb = v.begin(); itb != v.end(); itb++) { + + for (list1d_t::const_iterator itc = v.begin(); itc != v.end(); itc++) { + + // Discard element combinations which are not allowed due to the + // permutational symmetry!!! + const index<1>& bidxa = ita->get_block_index(); + const index<1>& idxa = ita->get_in_block_index(); + const index<1>& bidxb = itb->get_block_index(); + const index<1>& idxb = itb->get_in_block_index(); + const index<1>& bidxc = itc->get_block_index(); + const index<1>& idxc = itc->get_in_block_index(); + + if (sym_v && bidxb[0] == bidxc[0] && idxb[0] == idxc[0]) continue; + + double value = ita->get_value() + itb->get_value() + itc->get_value(); + + libtensor::index<3> bidx, idx; + bidx[0] = bidxa[0]; + bidx[1] = bidxb[0]; + bidx[2] = bidxc[0]; + idx[0] = idxa[0]; + idx[1] = idxb[0]; + idx[2] = idxc[0]; + + if (sym_v && bidx[1] > bidx[2]) { + std::swap(bidx[1], bidx[2]); + std::swap(idx[1], idx[2]); + } else if (sym_v && bidx[1] == bidx[2] && idx[1] > idx[2]) { + std::swap(idx[1], idx[2]); + } + + // Ignore blocks where the targeted spin_change is not achieved + mask<3> orb_type; + orb_type[0] = true; + if (base.get_spin_proj(orb_type, bidx) != dm_s) continue; + + // Check if the block is allowed in the symmetry of the guess + orbit<3, double> orb(sym, bidx); + if (!orb.is_allowed()) continue; + + // Find canonical index + abs_index<3> abi(orb.get_acindex(), bidims); + const tensor_transf<3, double>& tr = orb.get_transf(bidx); + bidx = abi.get_index(); + permutation<3> pinv(tr.get_perm(), true); + idx.permute(pinv); + + // Split block index into spin part and spatial part + mask<3> spm; + index<3> spi; + base.split_block_index(bidx, spm, spi); + + to.add_index(value, spm, spi, idx); + } // for itc + } // for itb + } // for ita +} + +size_t build_guesses(list_t::iterator& cur_guess, list_t::iterator end, + const index_group_h2p& ig, double value, + const symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, index_handler<3>& base) { + bool sym_v; // Are the two occupied indices identical + determine_sym(sym, sym_v); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + if (cur_guess == end) return 0; + + int ms = a_spin ? 1 : -1; + + const index<3>& spidx = ig.get_spatial_bidx(); + const index<3>& idx = ig.get_idx(); + + std::vector>> lv; + + // No specific spin create as many guesses as there are available in the + // index group + if (spin == 0) { + lv.resize(ig.size()); + + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + static const double coeff[3][3] = { + // in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + { 1.0, -1.0, -1.0}, // quartet + { 0.0, -1.0, 1.0}, // doublet 1 + { -2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + for (size_t i = 0; i < ig.size(); i++) + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + else if (spin == 2) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + lv.resize(1); + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + lv.resize(2); + + static const double coeff[2][3] = { + // in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + { 0.0, -1.0, 1.0}, // doublet 1 + { -2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } else if (spin == 4) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 3) { + lv.resize(1); + + static const double coeff[1][3] = { + // in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + { 1.0, -1.0, -1.0}}; // quartet + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } + + + size_t i = 0; + for (; i < lv.size() && cur_guess != end; i++, cur_guess++) { + libtensor::btensor<3, double>& bt = *(cur_guess->first); + { // Setup up the symmetry + libtensor::block_tensor_wr_ctrl<3, double> ctrl(bt); + ctrl.req_zero_all_blocks(); + libtensor::symmetry<3, double>& sym_to = ctrl.req_symmetry(); + libtensor::so_copy<3, double>(sym).perform(sym_to); + } + + // Set the elements + libtensor::btod_set_elem<3> set_op; + for (auto it = lv[i].begin(); it != lv[i].end(); it++) { + set_op.perform(bt, it->bidx, it->idx, it->coeff); + } + + // Normalise + double norm = libtensor::btod_dotprod<3>(bt, bt).calculate(); + if (norm != 1.0) { + libtensor::btod_scale<3>(bt, 1.0 / sqrt(norm)).perform(); + } + cur_guess->second = value; + } + return i; +} + +} // namespace + +size_t ea_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, + bool a_spin, bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, + int dm_s, double degeneracy_tolerance) { + + size_t nguesses = va.size(); + if (nguesses == 0) return 0; + + // TODO sym_v should be stored in an adc_guess_base-like object + // Determine symmetry and spin + bool sym_v; // Are the two occupied indices identical + determine_sym(sym, sym_v); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + size_t ns = nguesses; + index_group_map_h2p igm(degeneracy_tolerance, sym_v); + + bool max_reached = false; + // Create empty 1d symmetry to use with btod_select + symmetry<1, double> sym1(d_o.get_bis()), sym2(d_v.get_bis()); + + // Search for smallest elements until we have found enough. + size_t size = 0; + while (size < nguesses && !max_reached) { + + igm.clear(); + size = 0; + + ns *= 2; + list1d_t ilx_o, ilx_v; + btod_select<1, compare_t>(d_o, sym1).perform(ilx_o, ns); + btod_select<1, compare_t>(d_v, sym2).perform(ilx_v, ns); + + max_reached = ilx_o.size() < ns; + + index_handler<3> base(ab); + transfer_elements(ilx_o, ilx_v, igm, sym, base, dm_s); + ilx_o.clear(); + ilx_v.clear(); + + //size++; // we want to have the real size of the index group guesses + // Count the number of elements + if (spin == 0) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + size += igm.get_group(it).size(); + } + } else if (spin == 2) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 2; // two doublets, one quartet + } else if (igm.get_group(it).size() == 1) { + size += 1; // only a doublet in this case + } + } + } else if (spin == 4) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 1; // one quartet + } + } + } + } // while + + // Now form the guess vectors + nguesses = 0; + + list_t::iterator guess = va.begin(); + // Loop until list is empty or we have constructed all guesses + index_group_map_h2p::iterator it = igm.begin(); + while (it != igm.end() && guess != va.end()) { + index_handler<3> base(ab); + nguesses += + build_guesses(guess, va.end(), igm.get_group(it), igm.get_value(it), + sym, a_spin, restricted, doublet, base); + it++; + } + + return nguesses; +} +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/ea_adc_guess_d.hh b/libadcc_src/guess/ea_adc_guess_d.hh new file mode 100644 index 000000000..9e943696e --- /dev/null +++ b/libadcc_src/guess/ea_adc_guess_d.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "index_group_h2p.hh" + +namespace libadcc { + +/** \brief Forms a list of doubles guess vectors. + Selects the smallest elements from the provided OV matrices (for Koopman's + guess this should be the delta Fock matrix) and combines two of these + elements to form the doubles guesses. + \param va List of doubles-value pairs to initialize. + \param d_o Fock matrix to construct guesses from (occ.) + \param d_v Fock matrix to construct guesses from. (virt.) + \param sym Symmetry of guess vectors. + \param a_spin If alpha ionization (false: beta) + \param restricted Is this a restricted calculation + \param doublet Doublet or quartet states (only in case of restricted + calculation) + \param ab Alpha/beta spin blocks of occupied orbitals. + \param dm_s Delta m_s, spin-change twice + \return Number of guess vectors created + **/ +size_t ea_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, + bool a_spin, bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, + int dm_s, double degeneracy_tolerance); + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_h2p.cc b/libadcc_src/guess/index_group_h2p.cc new file mode 100644 index 000000000..1376aee6f --- /dev/null +++ b/libadcc_src/guess/index_group_h2p.cc @@ -0,0 +1,84 @@ +#include "index_group_h2p.hh" +#include "../exceptions.hh" +#include + +namespace libadcc { + +using namespace libtensor; +using libtensor::index; + +libtensor::mask<3> index_group_h2p::get_spin_mask(size_t sp) const { + if (m_s.count(sp) == 0) { + throw runtime_error("Could not find spin state sp ==" + std::to_string(sp) + "."); + } + return compute_spin_mask(sp); +} + +size_t index_group_h2p::compute_spin(const mask<3>& spm) { + + size_t s = 0; + for (size_t i = 0; i < 3; i++) s = s * 2 + (spm[i] ? 1 : 0); + + return s; +} + +mask<3> index_group_h2p::compute_spin_mask(size_t sp) { + + mask<3> m; + size_t i = 0, curbit = 1 << 2; + while (sp != 0 && i < 3) { + m[i++] = (sp & curbit); + curbit >>= 1; + } + return m; +} + +void index_group_map_h2p::add_index(double val, mask<3> spm, index<3> spidx, index<3> idx) { + + find_canonical_index(spm, spidx, idx); + + // Loop over group map and look for similar value + std::multimap::iterator it = m_idxmap.begin(); + for (; it != m_idxmap.end(); it++) { + if (fabs(val - it->first) < m_thresh) break; + } + + // Try to add element to index groups which belong to similar + // values + bool added = false; + while (it != m_idxmap.end() && fabs(val - it->first) < m_thresh && !added) { + + index_group_h2p& grp = it->second; + if (spidx == grp.get_spatial_bidx() && idx == grp.get_idx()) { + grp.add(spm); + added = true; + } + it++; + } + + // If no index group found start a new one. + if (!added) { + std::multimap::iterator ic = m_idxmap.insert( + std::pair(val, index_group_h2p(spidx, idx))); + ic->second.add(spm); + } +} + +void index_group_map_h2p::find_canonical_index(mask<3>& m, index<3>& spidx, + index<3>& idx) const { + + if (m_sym_v) { + if (spidx[1] == spidx[2]) { + if (idx[1] > idx[2]) { + std::swap(idx[1], idx[2]); + std::swap(m[1], m[2]); + } + } else if (spidx[1] > spidx[2]) { + std::swap(spidx[1], spidx[2]); + std::swap(idx[1], idx[2]); + std::swap(m[1], m[2]); + } + } +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_h2p.hh b/libadcc_src/guess/index_group_h2p.hh new file mode 100644 index 000000000..47b353581 --- /dev/null +++ b/libadcc_src/guess/index_group_h2p.hh @@ -0,0 +1,164 @@ +#pragma once +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#pragma GCC visibility pop +#include +#include + +namespace libadcc { + +/** \brief Group of 3D block tensor elements with common spatial index + An index group is constructed by passing the spatial block index and + in-block index of a 3D block tensor element. Those two indices define + the index group. + The spin states belonging to the index group can be added using the + functions + \code + void add(const libtensor::mask<3> &); + \endcode + \code + void add(size_t); + \endcode + **/ +class index_group_h2p { + public: + typedef std::set::const_iterator iterator; + + enum { + aaa = 0, + aab = 1, + aba = 2, + abb = 3, + baa = 4, + bab = 5, + bba = 6, + bbb = 7, + }; + + private: + libtensor::index<3> m_spidx; + libtensor::index<3> m_idx; + std::set m_s; + + public: + /** \brief Constructor + \param spidx Spatial block index. + \param idx In-block index + **/ + index_group_h2p(const libtensor::index<3>& spidx, const libtensor::index<3>& idx) + : m_spidx(spidx), m_idx(idx) {} + + /** \brief Add spin state to index group + \param s Spin states index (see enum) + **/ + void add(size_t s) { m_s.insert(s); } + + /** \brief Add spin state to index group + \param spm Mask representing the spin states (beta == true) + **/ + void add(const libtensor::mask<3>& spm) { add(compute_spin(spm)); } + + /** \brief Return in-block index of index group + **/ + const libtensor::index<3>& get_idx() const { return m_idx; } + + /** \brief Return spatial block index of index group + **/ + const libtensor::index<3>& get_spatial_bidx() const { return m_spidx; } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(size_t sp) const { return m_s.find(sp) != m_s.end(); } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(const libtensor::mask<3>& spm) const { + return has_spin_state(compute_spin(spm)); + } + + /** \brief Return the number of spin states + **/ + size_t size() const { return m_s.size(); } + + /** \brief STL-style iterator to the start of the list of spin states + **/ + iterator begin() const { return m_s.begin(); } + + /** \brief STL-style iterator to the end of the list of spin states + **/ + iterator end() const { return m_s.end(); } + + /** \brief Get current spin state + **/ + size_t get_spin_state(iterator it) const { return *it; } + + /** \brief Get spin state as mask + **/ + libtensor::mask<3> get_spin_mask(size_t sp) const; + + /** \brief Get current spin state as mask + **/ + libtensor::mask<3> get_spin_mask(iterator it) const { return get_spin_mask(*it); } + + private: + static size_t compute_spin(const libtensor::mask<3>& spm); + static libtensor::mask<3> compute_spin_mask(size_t sp); +}; + +/** \brief Map of (value, index group) pairs + \sa ea_adc_guess_d, ea_adc_guess_d + **/ +class index_group_map_h2p { + public: + typedef std::multimap::const_iterator iterator; + + private: + bool m_sym_v; //!< Permutational anti-symmetry of virt indices + double m_thresh; //!< Threshold for identical values + + std::multimap m_idxmap; + + public: + /** \brief Constructor + \param thresh Threshold for identical values + \param sym_v Virt. indices have perm. anti-symmetry + */ + index_group_map_h2p(double thresh, bool sym_v = true) + : m_sym_v(sym_v), m_thresh(thresh) {} + + /** \brief Remove all elements from list + **/ + void clear() { m_idxmap.clear(); } + + /** \brief Add an index to the map + \param val Value assigned to the index + \param spm Spin state mask + \param spidx Spatial block index + \param idx In-block index + **/ + void add_index(double val, libtensor::mask<3> spm, libtensor::index<3> spidx, + libtensor::index<3> idx); + + /** \brief STL-style iterator to first element + **/ + iterator begin() const { return m_idxmap.begin(); } + + /** \brief STL-style iterator to end + **/ + iterator end() const { return m_idxmap.end(); } + + /** \brief Return the value at the current position + **/ + double get_value(iterator it) const { return it->first; } + + /** \brief Return the index group at the current position + **/ + const index_group_h2p& get_group(iterator it) const { return it->second; } + + private: + void find_canonical_index(libtensor::mask<3>& m, libtensor::index<3>& spidx, + libtensor::index<3>& idx) const; +}; + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_p2h.cc b/libadcc_src/guess/index_group_p2h.cc new file mode 100644 index 000000000..facc992c4 --- /dev/null +++ b/libadcc_src/guess/index_group_p2h.cc @@ -0,0 +1,84 @@ +#include "index_group_p2h.hh" +#include "../exceptions.hh" +#include + +namespace libadcc { + +using namespace libtensor; +using libtensor::index; + +libtensor::mask<3> index_group_p2h::get_spin_mask(size_t sp) const { + if (m_s.count(sp) == 0) { + throw runtime_error("Could not find spin state sp ==" + std::to_string(sp) + "."); + } + return compute_spin_mask(sp); +} + +size_t index_group_p2h::compute_spin(const mask<3>& spm) { + + size_t s = 0; + for (size_t i = 0; i < 3; i++) s = s * 2 + (spm[i] ? 1 : 0); + + return s; +} + +mask<3> index_group_p2h::compute_spin_mask(size_t sp) { + + mask<3> m; + size_t i = 0, curbit = 1 << 2; + while (sp != 0 && i < 3) { + m[i++] = (sp & curbit); + curbit >>= 1; + } + return m; +} + +void index_group_map_p2h::add_index(double val, mask<3> spm, index<3> spidx, index<3> idx) { + + find_canonical_index(spm, spidx, idx); + + // Loop over group map and look for similar value + std::multimap::iterator it = m_idxmap.begin(); + for (; it != m_idxmap.end(); it++) { + if (fabs(val - it->first) < m_thresh) break; + } + + // Try to add element to index groups which belong to similar + // values + bool added = false; + while (it != m_idxmap.end() && fabs(val - it->first) < m_thresh && !added) { + + index_group_p2h& grp = it->second; + if (spidx == grp.get_spatial_bidx() && idx == grp.get_idx()) { + grp.add(spm); + added = true; + } + it++; + } + + // If no index group found start a new one. + if (!added) { + std::multimap::iterator ic = m_idxmap.insert( + std::pair(val, index_group_p2h(spidx, idx))); + ic->second.add(spm); + } +} + +void index_group_map_p2h::find_canonical_index(mask<3>& m, index<3>& spidx, + index<3>& idx) const { + + if (m_sym_o) { + if (spidx[0] == spidx[1]) { + if (idx[0] > idx[1]) { + std::swap(idx[0], idx[1]); + std::swap(m[0], m[1]); + } + } else if (spidx[0] > spidx[1]) { + std::swap(spidx[0], spidx[1]); + std::swap(idx[0], idx[1]); + std::swap(m[0], m[1]); + } + } +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_p2h.hh b/libadcc_src/guess/index_group_p2h.hh new file mode 100644 index 000000000..eb5447421 --- /dev/null +++ b/libadcc_src/guess/index_group_p2h.hh @@ -0,0 +1,164 @@ +#pragma once +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#pragma GCC visibility pop +#include +#include + +namespace libadcc { + +/** \brief Group of 3D block tensor elements with common spatial index + An index group is constructed by passing the spatial block index and + in-block index of a 3D block tensor element. Those two indices define + the index group. + The spin states belonging to the index group can be added using the + functions + \code + void add(const libtensor::mask<3> &); + \endcode + \code + void add(size_t); + \endcode + **/ +class index_group_p2h { + public: + typedef std::set::const_iterator iterator; + + enum { + aaa = 0, + aab = 1, + aba = 2, + abb = 3, + baa = 4, + bab = 5, + bba = 6, + bbb = 7, + }; + + private: + libtensor::index<3> m_spidx; + libtensor::index<3> m_idx; + std::set m_s; + + public: + /** \brief Constructor + \param spidx Spatial block index. + \param idx In-block index + **/ + index_group_p2h(const libtensor::index<3>& spidx, const libtensor::index<3>& idx) + : m_spidx(spidx), m_idx(idx) {} + + /** \brief Add spin state to index group + \param s Spin states index (see enum) + **/ + void add(size_t s) { m_s.insert(s); } + + /** \brief Add spin state to index group + \param spm Mask representing the spin states (beta == true) + **/ + void add(const libtensor::mask<3>& spm) { add(compute_spin(spm)); } + + /** \brief Return in-block index of index group + **/ + const libtensor::index<3>& get_idx() const { return m_idx; } + + /** \brief Return spatial block index of index group + **/ + const libtensor::index<3>& get_spatial_bidx() const { return m_spidx; } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(size_t sp) const { return m_s.find(sp) != m_s.end(); } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(const libtensor::mask<3>& spm) const { + return has_spin_state(compute_spin(spm)); + } + + /** \brief Return the number of spin states + **/ + size_t size() const { return m_s.size(); } + + /** \brief STL-style iterator to the start of the list of spin states + **/ + iterator begin() const { return m_s.begin(); } + + /** \brief STL-style iterator to the end of the list of spin states + **/ + iterator end() const { return m_s.end(); } + + /** \brief Get current spin state + **/ + size_t get_spin_state(iterator it) const { return *it; } + + /** \brief Get spin state as mask + **/ + libtensor::mask<3> get_spin_mask(size_t sp) const; + + /** \brief Get current spin state as mask + **/ + libtensor::mask<3> get_spin_mask(iterator it) const { return get_spin_mask(*it); } + + private: + static size_t compute_spin(const libtensor::mask<3>& spm); + static libtensor::mask<3> compute_spin_mask(size_t sp); +}; + +/** \brief Map of (value, index group) pairs + \sa ip_adc_guess_d, ip_adc_guess_d + **/ +class index_group_map_p2h { + public: + typedef std::multimap::const_iterator iterator; + + private: + bool m_sym_o; //!< Permutational anti-symmetry of occ indices + double m_thresh; //!< Threshold for identical values + + std::multimap m_idxmap; + + public: + /** \brief Constructor + \param thresh Threshold for identical values + \param sym_o Occ. indices have perm. anti-symmetry + */ + index_group_map_p2h(double thresh, bool sym_o = true) + : m_sym_o(sym_o), m_thresh(thresh) {} + + /** \brief Remove all elements from list + **/ + void clear() { m_idxmap.clear(); } + + /** \brief Add an index to the map + \param val Value assigned to the index + \param spm Spin state mask + \param spidx Spatial block index + \param idx In-block index + **/ + void add_index(double val, libtensor::mask<3> spm, libtensor::index<3> spidx, + libtensor::index<3> idx); + + /** \brief STL-style iterator to first element + **/ + iterator begin() const { return m_idxmap.begin(); } + + /** \brief STL-style iterator to end + **/ + iterator end() const { return m_idxmap.end(); } + + /** \brief Return the value at the current position + **/ + double get_value(iterator it) const { return it->first; } + + /** \brief Return the index group at the current position + **/ + const index_group_p2h& get_group(iterator it) const { return it->second; } + + private: + void find_canonical_index(libtensor::mask<3>& m, libtensor::index<3>& spidx, + libtensor::index<3>& idx) const; +}; + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/ip_adc_guess_d.cc b/libadcc_src/guess/ip_adc_guess_d.cc new file mode 100644 index 000000000..328b06d46 --- /dev/null +++ b/libadcc_src/guess/ip_adc_guess_d.cc @@ -0,0 +1,485 @@ +#include "ip_adc_guess_d.hh" +#include "../exceptions.hh" + +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#include +#include +#include +#include +#include +#pragma GCC visibility pop + +namespace libadcc { + +// TODO This file definitely needs a cleanup. + +using namespace libtensor; +using libtensor::index; + +/** \brief Element type for guess vectors + **/ +template +struct guess_element { + libtensor::index bidx; //!< Block index + libtensor::index idx; //!< In block index + double coeff; //!< Coefficient; + + guess_element(const libtensor::index& bidx_, const libtensor::index& idx_, + const double& coeff_) + : bidx(bidx_), idx(idx_), coeff(coeff_) {} +}; + +/** \brief Base class for guess formation **/ +template +class index_handler { + public: + protected: + libtensor::sequence*> m_ab; //!< Alpha-beta block markers + + private: + libtensor::sequence m_na; //!< Number of alpha spin blocks + + public: + /** \brief Constructor + \param ab Alpha-beta block markers (for N orbital spaces) + \param sym Symmetry of guess vectors + \param ms Spin multiplicity + **/ + index_handler(const libtensor::sequence*>& ab) + : m_ab(ab), m_na(0) { + for (size_t i = 0; i < N; i++) { + for (size_t j = 0; j < m_ab[i]->size(); j++) { + if (!m_ab[i]->at(j)) m_na[i]++; + } + } + } + + /** \brief Calculates the spin projection \f$ m_s \f$ of the block. + \param bidx Block index + \param orb_type Orbital type per dim (true = occupied) + \return -1 or +1 for ionization of an alpha or beta electron + */ + int get_spin_proj(const libtensor::mask& orb_type, + const libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + if (bidx[i] > m_ab[i]->size()) { + throw runtime_error("Block index exceeds dim"); + } + } + + int ms = 0; + for (size_t i = 0; i < N; i++) { + // Left side is true if orbital is occ. + // Right side is true if orbital has beta spin + // Hence it is true for occ. beta orbitals and virt. alpha orbitals + if (orb_type[i] == m_ab[i]->at(bidx[i])) + ms += 1; + else + ms -= 1; + } + return ms; + } + + /** \brief Split block index into spatial part and spin part + \param bidx Input block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + **/ + void split_block_index(const libtensor::index& bidx, libtensor::mask& sp, + libtensor::index& sbidx) const { + for (size_t i = 0; i < N; i++) { + sp[i] = m_ab[i]->at(bidx[i]); + sbidx[i] = (sp[i] ? bidx[i] - m_na[i] : bidx[i]); + } + } + + /** \brief Merge spatial part and spin part of block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + \param bidx Input block index + **/ + void merge_block_index(const libtensor::mask& sp, const libtensor::index& sbidx, + libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + bidx[i] = (sp[i] ? sbidx[i] + m_na[i] : sbidx[i]); + } + } +}; + +namespace { +typedef libtensor::compare4min compare_t; +typedef libtensor::btod_select<1, compare_t>::list_type list1d_t; +typedef libtensor::btod_select<3, compare_t>::list_type list3d_t; +typedef std::list*, double>> list_t; + +/** Determine if occupied indices should be symmetrized */ +void determine_sym(const symmetry<3, double>& sym, bool& sym_o) { + + sym_o = false; + for (symmetry<3, double>::iterator it1 = sym.begin(); it1 != sym.end(); it1++) { + + const symmetry_element_set<3, double>& set = sym.get_subset(it1); + const std::string& id = set.get_id(); + + if (id.compare(se_perm<3, double>::k_sym_type) != 0) continue; + if (set.is_empty()) return; + + typedef symmetry_element_set_adapter<3, double, se_perm<3, double>> adapter_t; + + adapter_t ad(set); + for (adapter_t::iterator it2 = ad.begin(); it2 != ad.end(); it2++) { + + const se_perm<3, double>& el = ad.get_elem(it2); + + const permutation<3>& p = el.get_perm(); + sym_o |= (p[0] == 1 && p[1] == 0); + } + } +} + +/** Determine the spin of the guess vectors */ +unsigned determine_spin(bool restricted, bool doublet) { + + if (restricted) { + if (doublet) return 2; + else return 4; + } else { + return 0; + } +} + +/** Transfers the elements of a 1D list to a 3D list */ +void transfer_elements(const list1d_t& o, const list1d_t& v, + index_group_map_p2h& to, const libtensor::symmetry<3, + double>& sym, const index_handler<3>& base, int dm_s) { + + // Determine symmetry + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + + to.clear(); + + dimensions<3> bidims = sym.get_bis().get_block_index_dims(); + + for (list1d_t::const_iterator ita = o.begin(); ita != o.end(); ita++) { + + for (list1d_t::const_iterator itb = o.begin(); itb != o.end(); itb++) { + + for (list1d_t::const_iterator itc = v.begin(); itc != v.end(); itc++) { + + // Discard element combinations which are not allowed due to the + // permutational symmetry!!! + const index<1>& bidxa = ita->get_block_index(); + const index<1>& idxa = ita->get_in_block_index(); + const index<1>& bidxb = itb->get_block_index(); + const index<1>& idxb = itb->get_in_block_index(); + const index<1>& bidxc = itc->get_block_index(); + const index<1>& idxc = itc->get_in_block_index(); + + if (sym_o && bidxa[0] == bidxb[0] && idxa[0] == idxb[0]) continue; + + double value = ita->get_value() + itb->get_value() + itc->get_value(); + + libtensor::index<3> bidx, idx; + bidx[0] = bidxa[0]; + bidx[1] = bidxb[0]; + bidx[2] = bidxc[0]; + idx[0] = idxa[0]; + idx[1] = idxb[0]; + idx[2] = idxc[0]; + + if (sym_o && bidx[0] > bidx[1]) { + std::swap(bidx[0], bidx[1]); + std::swap(idx[0], idx[1]); + } else if (sym_o && bidx[0] == bidx[1] && idx[0] > idx[1]) { + std::swap(idx[0], idx[1]); + } + + // Ignore blocks where the targeted spin_change is not achieved + mask<3> orb_type; + orb_type[0] = true; + orb_type[1] = true; + if (base.get_spin_proj(orb_type, bidx) != dm_s) continue; + + // Check if the block is allowed in the symmetry of the guess + orbit<3, double> orb(sym, bidx); + if (!orb.is_allowed()) continue; + + // Find canonical index + abs_index<3> abi(orb.get_acindex(), bidims); + const tensor_transf<3, double>& tr = orb.get_transf(bidx); + bidx = abi.get_index(); + permutation<3> pinv(tr.get_perm(), true); + idx.permute(pinv); + + // Split block index into spin part and spatial part + mask<3> spm; + index<3> spi; + base.split_block_index(bidx, spm, spi); + + to.add_index(value, spm, spi, idx); + } // for itc + } // for itb + } // for ita +} + +size_t build_guesses(list_t::iterator& cur_guess, list_t::iterator end, + const index_group_p2h& ig, double value, + const symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, index_handler<3>& base) { + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + if (cur_guess == end) return 0; + + int ms = a_spin ? -1 : 1; + + const index<3>& spidx = ig.get_spatial_bidx(); + const index<3>& idx = ig.get_idx(); + + std::vector>> lv; + + // No specific spin create as many guesses as there are available in the + // index group + if (spin == 0) { + lv.resize(ig.size()); + + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + static const double coeff[3][3] = { + // in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + { 1.0, -1.0, -1.0}, // quartet + { 0.0, -1.0, 1.0}, // doublet 1 + { -2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + for (size_t i = 0; i < ig.size(); i++) + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + else if (spin == 2) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + lv.resize(1); + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + lv.resize(2); + + static const double coeff[2][3] = { + // in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + { 0.0, -1.0, 1.0}, // doublet 1 + { -2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } else if (spin == 4) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 3) { + lv.resize(1); + + static const double coeff[1][3] = { + // in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + { 1.0, -1.0, -1.0}}; // quartet + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } + + + size_t i = 0; + for (; i < lv.size() && cur_guess != end; i++, cur_guess++) { + libtensor::btensor<3, double>& bt = *(cur_guess->first); + { // Setup up the symmetry + libtensor::block_tensor_wr_ctrl<3, double> ctrl(bt); + ctrl.req_zero_all_blocks(); + libtensor::symmetry<3, double>& sym_to = ctrl.req_symmetry(); + libtensor::so_copy<3, double>(sym).perform(sym_to); + } + + // Set the elements + libtensor::btod_set_elem<3> set_op; + for (auto it = lv[i].begin(); it != lv[i].end(); it++) { + set_op.perform(bt, it->bidx, it->idx, it->coeff); + } + + // Normalise + double norm = libtensor::btod_dotprod<3>(bt, bt).calculate(); + if (norm != 1.0) { + libtensor::btod_scale<3>(bt, 1.0 / sqrt(norm)).perform(); + } + cur_guess->second = value; + } + return i; +} + +} // namespace + +size_t ip_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, + bool a_spin, bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, + int dm_s, double degeneracy_tolerance) { + + size_t nguesses = va.size(); + if (nguesses == 0) return 0; + + // TODO sym_o should be stored in an adc_guess_base-like object + // Determine symmetry and spin + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + size_t ns = nguesses; + index_group_map_p2h igm(degeneracy_tolerance, sym_o); + + bool max_reached = false; + // Create empty 1d symmetry to use with btod_select + symmetry<1, double> sym1(d_o.get_bis()), sym2(d_v.get_bis()); + + // Search for smallest elements until we have found enough. + size_t size = 0; + while (size < nguesses && !max_reached) { + + igm.clear(); + size = 0; + + ns *= 2; + list1d_t ilx_o, ilx_v; + btod_select<1, compare_t>(d_o, sym1).perform(ilx_o, ns); + btod_select<1, compare_t>(d_v, sym2).perform(ilx_v, ns); + + max_reached = ilx_o.size() < ns; + + index_handler<3> base(ab); + transfer_elements(ilx_o, ilx_v, igm, sym, base, dm_s); + ilx_o.clear(); + ilx_v.clear(); + + //size++; // we want to have the real size of the index group guesses + // Count the number of elements + if (spin == 0) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + size += igm.get_group(it).size(); + } + } else if (spin == 2) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 2; // two doublets, one quartet + } else if (igm.get_group(it).size() == 1) { + size += 1; // only a doublet in this case + } + } + } else if (spin == 4) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 1; // one quartet + } + } + } + } // while + + // Now form the guess vectors + nguesses = 0; + + list_t::iterator guess = va.begin(); + // Loop until list is empty or we have constructed all guesses + index_group_map_p2h::iterator it = igm.begin(); + while (it != igm.end() && guess != va.end()) { + index_handler<3> base(ab); + nguesses += + build_guesses(guess, va.end(), igm.get_group(it), igm.get_value(it), + sym, a_spin, restricted, doublet, base); + it++; + } + + return nguesses; +} +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/ip_adc_guess_d.hh b/libadcc_src/guess/ip_adc_guess_d.hh new file mode 100644 index 000000000..528d5e3e1 --- /dev/null +++ b/libadcc_src/guess/ip_adc_guess_d.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "index_group_p2h.hh" + +namespace libadcc { + +/** \brief Forms a list of doubles guess vectors. + Selects the smallest elements from the provided OV matrices (for Koopman's + guess this should be the delta Fock matrix) and combines two of these + elements to form the doubles guesses. + \param va List of doubles-value pairs to initialize. + \param d_o Fock matrix to construct guesses from (occ.) + \param d_v Fock matrix to construct guesses from. (virt.) + \param sym Symmetry of guess vectors. + \param a_spin If alpha ionization (false: beta) + \param restricted Is this a restricted calculation + \param doublet Doublet or quartet states (only in case of restricted + calculation) + \param ab Alpha/beta spin blocks of occupied orbitals. + \param dm_s Delta m_s, spin-change twice + \return Number of guess vectors created + **/ +size_t ip_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, + bool a_spin, bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, + int dm_s, double degeneracy_tolerance); + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/pyiface/ExportAdcc.cc b/libadcc_src/pyiface/ExportAdcc.cc index 274ab79ee..69fbf6185 100644 --- a/libadcc_src/pyiface/ExportAdcc.cc +++ b/libadcc_src/pyiface/ExportAdcc.cc @@ -28,6 +28,8 @@ namespace libadcc { void export_AdcMemory(py::module& m); void export_adc_pp(py::module& m); +void export_adc_ip(py::module& m); +void export_adc_ea(py::module& m); void export_HartreeFockProvider(py::module& m); void export_MoIndexTranslation(py::module& m); void export_MoSpaces(py::module& m); @@ -41,6 +43,8 @@ void export_threading(py::module& m); PYBIND11_MODULE(libadcc, m) { libadcc::export_AdcMemory(m); libadcc::export_adc_pp(m); + libadcc::export_adc_ip(m); + libadcc::export_adc_ea(m); libadcc::export_HartreeFockProvider(m); libadcc::export_MoIndexTranslation(m); libadcc::export_MoSpaces(m); diff --git a/libadcc_src/pyiface/export_adc_ea.cc b/libadcc_src/pyiface/export_adc_ea.cc new file mode 100644 index 000000000..83929eb75 --- /dev/null +++ b/libadcc_src/pyiface/export_adc_ea.cc @@ -0,0 +1,56 @@ +// +// Copyright (C) 2019 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "../amplitude_vector_enforce_spin_kind.hh" +#include "../fill_ea_doubles_guesses.hh" +#include +#include + +namespace libadcc { + +namespace py = pybind11; +using namespace pybind11::literals; + +void export_adc_ea(py::module& m) { + m.def("amplitude_vector_enforce_spin_kind", &litude_vector_enforce_spin_kind, + "Apply the spin symmetrisation required to make the doubles and higher parts of " + "an amplitude vector consist of components for a particular spin kind only."); + + m.def("fill_ea_doubles_guesses", &fill_ea_doubles_guesses, "guesses_d"_a, + "mospaces"_a, "d_o"_a, "d_v"_a, "a_spin"_a, "restricted"_a, + "doublet"_a, "spin_change_twice"_a, "degeneracy_tolerance"_a, + "Fill the passed vector of doubles blocks with doubles guesses using " + "the O and V matrices, which are the two Fock matrices " + "involved in the doubles block.\n\nguesses_d Vectors of guesses, " + "all elements are assumed to be initialised to zero and the symmetry " + "is assumed to be properly set up.\nmospaces Mospaces object" + "\nd_o Matrix to construct guesses from (occ.)" + "\nd_v Matrix to construct guesses from (virt.)" + "\na_spin If alpha ionization (false: beta)" + "\nrestricted Is this a restricted calculation" + "\ndoublet Doublet or quartet states (only in case of" + "restricted calculation)" + "\nspin_change_twice Twice the value of the spin change to enforce " + "in an excitation.\ndegeneracy_tolerance Tolerance for two entries of " + "the diagonal to be considered degenerate, i.e. identical." + "\nReturns The number of guess vectors which have been properly " + "initialised (the others are invalid and should be discarded)."); +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/pyiface/export_adc_ip.cc b/libadcc_src/pyiface/export_adc_ip.cc new file mode 100644 index 000000000..d3454a69c --- /dev/null +++ b/libadcc_src/pyiface/export_adc_ip.cc @@ -0,0 +1,56 @@ +// +// Copyright (C) 2019 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "../amplitude_vector_enforce_spin_kind.hh" +#include "../fill_ip_doubles_guesses.hh" +#include +#include + +namespace libadcc { + +namespace py = pybind11; +using namespace pybind11::literals; + +void export_adc_ip(py::module& m) { + m.def("amplitude_vector_enforce_spin_kind", &litude_vector_enforce_spin_kind, + "Apply the spin symmetrisation required to make the doubles and higher parts of " + "an amplitude vector consist of components for a particular spin kind only."); + + m.def("fill_ip_doubles_guesses", &fill_ip_doubles_guesses, "guesses_d"_a, + "mospaces"_a, "d_o"_a, "d_v"_a, "a_spin"_a, "restricted"_a, + "doublet"_a, "spin_change_twice"_a, "degeneracy_tolerance"_a, + "Fill the passed vector of doubles blocks with doubles guesses using " + "the O and V matrices., which are the two Fock matrices " + "involved in the doubles block.\n\nguesses_d Vectors of guesses, " + "all elements are assumed to be initialised to zero and the symmetry " + "is assumed to be properly set up.\nmospaces Mospaces object" + "\nd_o Matrix to construct guesses from (occ.)" + "\nd_v Matrix to construct guesses from (virt.)" + "\na_spin If alpha ionization (false: beta)" + "\nrestricted Is this a restricted calculation" + "\ndoublet Doublet or quartet states (only in case of" + "restricted calculation)" + "\nspin_change_twice Twice the value of the spin change to enforce " + "in an excitation.\ndegeneracy_tolerance Tolerance for two entries of " + "the diagonal to be considered degenerate, i.e. identical." + "\nReturns The number of guess vectors which have been properly " + "initialised (the others are invalid and should be discarded)."); +} + +} // namespace libadcc \ No newline at end of file From 11b32ea92ce95f7a9024b78fcaf35970bba151df Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 21 Jan 2026 10:26:28 +0100 Subject: [PATCH 03/28] IP/EA workflow running properly, ExcitedState implementation left to do --- adcc/__init__.py | 11 ++++++----- adcc/guess/Guesses.py | 10 +++++----- adcc/workflow.py | 43 +++++++++++++++++++++---------------------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/adcc/__init__.py b/adcc/__init__.py index 07c933a1c..075bb4205 100644 --- a/adcc/__init__.py +++ b/adcc/__init__.py @@ -45,8 +45,8 @@ from .opt_einsum_integration import register_with_opt_einsum # This has to be the last set of import -from .guess import (guess_symmetries, guess_zero, guesses_any, guesses_singlet, - guesses_spin_flip, guesses_triplet) +from .guess.Guesses import Guesses +from .guess.guess_zero import guess_symmetries, guess_zero from .workflow import run_adc from .exceptions import InputError @@ -59,10 +59,11 @@ "HartreeFockProvider", "ExcitedStates", "State2States", "Excitation", "ElectronicTransition", "Tensor", "DictHfProvider", "DataHfProvider", "OneParticleOperator", - "guesses_singlet", "guesses_triplet", "guesses_any", - "guess_symmetries", "guesses_spin_flip", "guess_zero", "LazyMp", + "Guesses", "guess_symmetries", "guess_zero", "LazyMp", "adc0", "cis", "adc1", "adc2", "adc2x", "adc3", "cvs_adc0", "cvs_adc1", "cvs_adc2", "cvs_adc2x", "cvs_adc3", + "ip_adc0", "ip_adc1", "ip_adc2", "ip_adc2x", "ip_adc3", + "ea_adc0", "ea_adc1", "ea_adc2", "ea_adc2x", "ea_adc3", "banner"] __version__ = "0.16.1" @@ -70,7 +71,7 @@ __url__ = "https://adc-connect.org" __authors__ = ["Michael F. Herbst", "Maximilian Scheurer", "Jonas Leitner", "Antonia Papapostolou", "Friederike Schneider", - "Adrian L. Dempwolff"] + "Adrian L. Dempwolff", "Adrian J. Müller"] __email__ = "developers@adc-connect.org" __contributors__ = [] diff --git a/adcc/guess/Guesses.py b/adcc/guess/Guesses.py index 022b8ef04..279b38676 100644 --- a/adcc/guess/Guesses.py +++ b/adcc/guess/Guesses.py @@ -21,12 +21,12 @@ ## ## --------------------------------------------------------------------- -from .guess import guesses_from_diagonal -from .exceptions import InputError +from .guesses_from_diagonal import guesses_from_diagonal +from adcc.exceptions import InputError import warnings -def determine_spin_change(self, adc_type: str, kind: str, is_alpha: bool): +def determine_spin_change(adc_type: str, kind: str, is_alpha: bool): if adc_type == "pp": if kind == "spin_flip": spin_change = -1 @@ -46,7 +46,7 @@ def determine_spin_change(self, adc_type: str, kind: str, is_alpha: bool): class Guesses: - def __init__(self, guesses: list|None, kind: str, + def __init__(self, guesses: list, kind: str, adc_type: str, is_alpha: bool): if guesses is None: self.guesses = [] @@ -54,7 +54,7 @@ def __init__(self, guesses: list|None, kind: str, self.guesses = guesses self.n_guesses = len(self.guesses) self.kind = kind - self.is_alpha = self.is_alpha + self.is_alpha = is_alpha symmetrisation = { "singlet": "symmetric", diff --git a/adcc/workflow.py b/adcc/workflow.py index a23063384..31bf5d824 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -26,9 +26,7 @@ from libadcc import ReferenceState from . import solver -from .guess import (guesses_any, guesses_singlet, guesses_doublet, - guesses_spin_flip, guesses_triplet) -from .guess import Guesses +from .guess.Guesses import Guesses from .LazyMp import LazyMp from .AdcMatrix import AdcMatrix, AdcMatrixlike, AdcExtraTerm from .AdcMethod import AdcMethod @@ -227,13 +225,9 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, if env_matrix_term: matrix += env_matrix_term diagres = diagonalise_adcmatrix( - matrix, n_states, guesses=guesses, conv_tol=conv_tol, output=output, - eigensolver=eigensolver, **solverargs) - # diagres = diagonalise_adcmatrix( - # matrix, n_states, kind, guesses=guesses, n_guesses=n_guesses, - # n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output, - # eigensolver=eigensolver, is_alpha=is_alpha, - # spin_change=spin_change, **solverargs) + matrix, n_states, guesses=solver_guesses, conv_tol=conv_tol, + output=output, eigensolver=eigensolver, **solverargs) + exstates = ExcitedStates(diagres) exstates.kind = kind exstates.spin_change = solver_guesses.spin_change @@ -445,14 +439,18 @@ def construct_guesses(matrix, n_states, guesses=None, n_guesses=None, return solver_guesses -def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", - guesses=None, n_guesses=None, n_guesses_doubles=None, - conv_tol=None, output=sys.stdout, is_alpha=None, - spin_change=None, **solverargs): +def diagonalise_adcmatrix(matrix, n_states, guesses=None, conv_tol=None, + eigensolver="davidson", output=sys.stdout, + **solverargs): """ This function seeks appropriate guesses and afterwards proceeds to diagonalise the ADC matrix using the specified eigensolver. Internal function called from run_adc. + + guesses : + Provide an instance of the 'Guesses' class containing a list of + guess vectors and all additional, relevant information as attributes: + 'n_guesses', 'kind', 'is_alpha', 'spin_block_symmetrisation' """ reference_state = matrix.reference_state @@ -468,28 +466,29 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", # Determine explicit_symmetrisation explicit_symmetrisation = IndexSymmetrisation - if solver_guesses.kind in ["singlet", "doublet", "triplet"]: + if guesses.kind in ["singlet", "doublet", "triplet"]: explicit_symmetrisation = IndexSpinSymmetrisation( - matrix, enforce_spin_kind=solver_guesses.kind + matrix, enforce_spin_kind=guesses.kind ) # Set some solver-specific parameters if eigensolver == "davidson": callback = setup_solver_printing( - "Jacobi-Davidson", matrix, kind, solver.davidson.default_print, - is_alpha=is_alpha, output=output) + "Jacobi-Davidson", matrix, guesses.kind, + solver.davidson.default_print, is_alpha=guesses.is_alpha, + output=output) run_eigensolver = jacobi_davidson elif eigensolver == "lanczos": callback = setup_solver_printing( - "Lanczos", matrix, kind, solver.lanczos.default_print, - is_alpha=is_alpha, output=output) + "Lanczos", matrix, guesses.kind, solver.lanczos.default_print, + is_alpha=guesses.is_alpha, output=output) run_eigensolver = lanczos else: raise InputError(f"Solver {eigensolver} unknown, try 'davidson'.") solverargs.setdefault("which", "SA") - return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol, - callback=callback, + return run_eigensolver(matrix, guesses.guesses, n_ep=n_states, + conv_tol=conv_tol, callback=callback, explicit_symmetrisation=explicit_symmetrisation, **solverargs) From 34800a79bd1c88634faef2d8c00fbbe781cdaec0 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 21 Jan 2026 10:37:20 +0100 Subject: [PATCH 04/28] simplified adc0 pole strengths --- adcc/adc_ea/pole_strength.py | 9 ++------- adcc/adc_ip/pole_strength.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/adcc/adc_ea/pole_strength.py b/adcc/adc_ea/pole_strength.py index 3ee1f0220..fe3720f2c 100644 --- a/adcc/adc_ea/pole_strength.py +++ b/adcc/adc_ea/pole_strength.py @@ -33,14 +33,9 @@ def pole_strength_ea_adc0(mp, amplitude, intermediates): check_singles_amplitudes([b.v], amplitude) - # Build Kronecker delta - hf = mp.reference_state - d_vv = zeros_like(hf.fvv) - d_vv.set_mask("aa", 1.0) - f11 = d_vv - # Calculate the spectroscopic amplitude x - xa = einsum("b,ba->a", amplitude.p, f11) + # "Calculate" the spectroscopic amplitude x + xa = amplitude.p return dot(xa, xa) diff --git a/adcc/adc_ip/pole_strength.py b/adcc/adc_ip/pole_strength.py index 0d8ad94eb..b0312c187 100644 --- a/adcc/adc_ip/pole_strength.py +++ b/adcc/adc_ip/pole_strength.py @@ -33,14 +33,9 @@ def pole_strength_ip_adc0(mp, amplitude, intermediates): check_singles_amplitudes([b.o], amplitude) - # Build Kronecker delta - hf = mp.reference_state - d_oo = zeros_like(hf.foo) - d_oo.set_mask("ii", 1.0) - f11 = d_oo - # Calculate the spectroscopic amplitude x - xi = einsum("j,ji->i", amplitude.h, f11) + # "Calculate" the spectroscopic amplitude x + xi = amplitude.h return dot(xi, xi) From bc36c4d0a9fc46056b0a3d2bb41d47b7d675a0a2 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 21 Jan 2026 10:38:59 +0100 Subject: [PATCH 05/28] cleanup --- adcc/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adcc/workflow.py b/adcc/workflow.py index 31bf5d824..aa9796852 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -224,10 +224,10 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, # add terms to matrix if env_matrix_term: matrix += env_matrix_term + diagres = diagonalise_adcmatrix( matrix, n_states, guesses=solver_guesses, conv_tol=conv_tol, output=output, eigensolver=eigensolver, **solverargs) - exstates = ExcitedStates(diagres) exstates.kind = kind exstates.spin_change = solver_guesses.spin_change From 1eade6e3e0d18fc78c4d62734799a9cfc7fe81b0 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Thu, 22 Jan 2026 16:45:13 +0100 Subject: [PATCH 06/28] extended ExcitedStates class with Child classes for IP/EA-ADC --- adcc/AdcMethod.py | 7 +- adcc/ChargedExcitations.py | 290 +++++++++++++++++++++++++++++++++++++ adcc/ElectronicStates.py | 55 ++++--- adcc/ExcitedStates.py | 20 ++- adcc/State2States.py | 6 +- 5 files changed, 355 insertions(+), 23 deletions(-) create mode 100644 adcc/ChargedExcitations.py diff --git a/adcc/AdcMethod.py b/adcc/AdcMethod.py index 5e250e99c..347ed7c4e 100644 --- a/adcc/AdcMethod.py +++ b/adcc/AdcMethod.py @@ -67,10 +67,13 @@ def at_level(self, newlevel): Return an equivalent method, where only the level is changed (e.g. calling this on a CVS method returns a CVS method) """ + name_str = "adc" + if self.adc_type != "pp": + name_str = self.adc_type + "-" + "adc" if self.is_core_valence_separated: - return AdcMethod("cvs-adc" + str(newlevel)) + return AdcMethod("cvs-" + name_str + str(newlevel)) else: - return AdcMethod("adc" + str(newlevel)) + return AdcMethod(name_str + str(newlevel)) @property def name(self): diff --git a/adcc/ChargedExcitations.py b/adcc/ChargedExcitations.py new file mode 100644 index 000000000..39ad211a9 --- /dev/null +++ b/adcc/ChargedExcitations.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2019 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +import numpy as np +from scipy import constants +import warnings + +from .import adc_ip, adc_ea +from .ElectronicStates import TableColumn, ElectronicStates, _timer_name +from .functions import dot +from .misc import cached_member_function + + +class ChargedExcitation(ElectronicStates): + + @property + def pole_strength(self) -> np.ndarray: + """Array of pole strengths of all computed states""" + return np.array([ + self._pole_strength(i) for i in range(self.size) + ]) + + + @cached_member_function(timer=_timer_name, separate_timings_by_args=False) + def _pole_strength(self, state_n: int) -> np.ndarray: + """Computes the pole strength for a single state""" + evec = self.excitation_vector[state_n] + return self._module.pole_strength( + self.property_method, self.ground_state, evec, + self.matrix.intermediates) + + def describe_helper(self, pole_strengths=True, state_dipole_moments=False, + block_norms=True, excitation_type="energy",): + """ + Creates and returns the to be printed columns + + Parameters + ---------- + pole_strengths : bool optional + Show oscillator strengths, by default ``True``. + + state_dipole_moments : bool, optional + Show state dipole moments, by default ``False``. + + block_norms : bool, optional + Show the norms of the n particle (n-1) hole blocks of the charged + excited states, by default ``True``. + + excitation_type : str, optional + Defines the name of the energy property. + 'ionization potential'/'electron affinity' for IP/EA + """ + has_dipole = "electric_dipole" in self.operators.available + # Collect the columns to print + columns: list[TableColumn] = [] + values: list[str] = [] + # count the number of states + values.extend(str(i) for i in range(self.size)) + columns.append(TableColumn(header="#", values=values.copy(), unit="")) + values.clear() + # excitation energy in a.u. and eV + eV = constants.value("Hartree energy in eV") + values.extend(f"{e:^13.7g} {e * eV:^13.7g}" for e in self.excitation_energy) + columns.append(TableColumn( + header=excitation_type, values=values.copy(), + unit="(au) (eV)" + )) + values.clear() + # the pole strengths + if pole_strengths: + values.extend(f"{pstr:^8.4f}" for pstr in self.pole_strength) + columns.append(TableColumn( + header="pole str", values=values.copy(), unit="(au)" + )) + values.clear() + # vector norm + blocks = self.matrix.axis_blocks + if block_norms and len(blocks) > 0: + values.extend(f"{dot(vec.get(blocks[0]), vec.get(blocks[0])):^9.4f}" + for vec in self.excitation_vector) + columns.append(TableColumn( + header="|v1|^2", values=values.copy(), unit="" + )) + values.clear() + if block_norms and len(blocks) > 1: + values.extend(f"{dot(vec.get(blocks[1]), vec.get(blocks[1])):^9.4f}" + for vec in self.excitation_vector) + columns.append(TableColumn( + header="|v2|^2", values=values.copy(), unit="" + )) + values.clear() + # the state dipole moment + if state_dipole_moments and has_dipole: + warnings.warn("Dipole moments of charged species are gauge " + "dependent.") + for dm in self.state_dipole_moment: + dmx, dmy, dmz = dm + values.append( + f"{dmx:^8.4f} {dmy:^8.4f} {dmz:^8.4f}" + f"{np.linalg.norm(dm):^8.4f}" + ) + columns.append(TableColumn( + header="state dipole moment", values=values.copy(), + unit="x(au) y(au) z(au) abs(au)" + )) + values.clear() + return columns + + +class DetachedStates(ChargedExcitation): + _module = adc_ip + + def __init__(self, data, is_alpha: bool, method: str = None, + property_method: str = None): + self.is_alpha = is_alpha + super().__init__(data, method, property_method) + + if self.method.adc_type != "ip": + raise ValueError("DetachedStates computes excited state properties" + " for IP-ADC. Got the non-IP-ADC method " + f"{self.method.name}") + + def describe(self, pole_strengths=True, state_dipole_moments=False, + block_norms=True): + """ + Return a string providing a human-readable description of the class + + Parameters + ---------- + pole_strengths : bool optional + Show oscillator strengths, by default ``True``. + + state_dipole_moments : bool, optional + Show state dipole moments, by default ``False``. + + block_norms : bool, optional + Show the norms of the n particle (n+1) hole blocks of the charged + excited states, by default ``True``. + """ + assert (self.matrix.axis_blocks == ["h"] + or self.matrix.axis_blocks == ["h", "phh"]) + columns = self.describe_helper( + pole_strengths, state_dipole_moments, block_norms, + "ionization potential") + + # Format the state information: kind, spin_change, + # alpha/beta detachment, and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + spin_type = "alpha" if self.is_alpha else "beta" + state_info.append(f"(ΔMS={self.spin_change:+.1f}), " + f"{spin_type} detachment") + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + return self._describe(columns, state_info) + + def to_qcvars(self, properties=False, recurse=False): + """ + Return a dictionary with property keys compatible to a Psi4 wavefunction + or a QCEngine Atomicresults object. + """ + name = self.method.name.upper() + + qcvars = { + "EXCITATION KIND": self.kind.upper(), + "NUMBER OF IONIZED STATES": len(self.excitation_energy), + f"{name} ITERATIONS": self.n_iter, + f"{name} IONIZATION POTENTIALS": self.excitation_energy, + } + + if properties: + qcvars.update({ + # Transition properties + f"{name} POLE STRENGTHS": self.pole_strength, + # + # State properties + f"{name} STATE DIPOLES": self.state_dipole_moment + }) + + if recurse: + mpvars = self.ground_state.to_qcvars(properties, recurse=True, + maxlevel=self.method.level) + qcvars.update(mpvars) + return qcvars + + +class AttachedStates(ChargedExcitation): + _module = adc_ea + + def __init__(self, data, is_alpha: bool, method: str = None, + property_method: str = None): + self.is_alpha = is_alpha + super().__init__(data, method, property_method) + + if self.method.adc_type != "ea": + raise ValueError("DetachedStates computes excited state properties" + " for EA-ADC. Got the non-EA-ADC method " + f"{self.method.name}") + + def describe(self, pole_strengths=True, state_dipole_moments=False, + block_norms=True): + """ + Return a string providing a human-readable description of the class + + Parameters + ---------- + pole_strengths : bool optional + Show oscillator strengths, by default ``True``. + + state_dipole_moments : bool, optional + Show state dipole moments, by default ``False``. + + block_norms : bool, optional + Show the norms of the n particle (n+1) hole blocks of the charged + excited states, by default ``True``. + """ + assert (self.matrix.axis_blocks == ["p"] + or self.matrix.axis_blocks == ["p", "pph"]) + columns = self.describe_helper( + pole_strengths, state_dipole_moments, block_norms, + "electron affinity") + + # Format the state information: kind, spin_change, + # alpha/beta detachment, and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + spin_type = "alpha" if self.is_alpha else "beta" + state_info.append(f"(ΔMS={self.spin_change:+.1f}), " + f"{spin_type} attachment") + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + return self._describe(columns, state_info) + + def to_qcvars(self, properties=False, recurse=False): + """ + Return a dictionary with property keys compatible to a Psi4 wavefunction + or a QCEngine Atomicresults object. + """ + name = self.method.name.upper() + + qcvars = { + "EXCITATION KIND": self.kind.upper(), + "NUMBER OF ELECTRON ATTACHED STATES": len(self.excitation_energy), + f"{name} ITERATIONS": self.n_iter, + f"{name} ELECTRON AFFINITIES": self.excitation_energy, + } + + if properties: + qcvars.update({ + # Transition properties + f"{name} POLE STRENGTHS": self.pole_strength, + # + # State properties + f"{name} STATE DIPOLES": self.state_dipole_moment + }) + + if recurse: + mpvars = self.ground_state.to_qcvars(properties, recurse=True, + maxlevel=self.method.level) + qcvars.update(mpvars) + return qcvars \ No newline at end of file diff --git a/adcc/ElectronicStates.py b/adcc/ElectronicStates.py index ea4383460..17a6e440d 100644 --- a/adcc/ElectronicStates.py +++ b/adcc/ElectronicStates.py @@ -70,7 +70,8 @@ def __init__(self, data, method: str = None, self._timed_objects.append((datakey, data)) # Copy some optional attributes - for optattr in ["converged", "spin_change", "kind", "n_iter"]: + for optattr in ["converged", "spin_change", "kind", "n_iter", + "is_alpha"]: if hasattr(data, optattr): setattr(self, optattr, getattr(data, optattr)) @@ -107,7 +108,7 @@ def __init__(self, data, method: str = None, self._excitation_energy_uncorrected = \ data.excitation_energy.copy() if hasattr(data, "excitation_energy_uncorrected"): - self._excitation_energy_uncorrected =\ + self._excitation_energy_uncorrected = \ data.excitation_energy_uncorrected.copy() if hasattr(data, "excitation_vector"): self._excitation_vector = data.excitation_vector @@ -405,7 +406,7 @@ def convert_x_units(spectrum: Spectrum): plots = spectrum.plot(style="discrete", **kwargs) return plots - def _describe(self, columns: list["TableColumn"]): + def _describe(self, columns: list["TableColumn"], state_info: list[str]): """ Return a string providing a human-readable description of the class @@ -431,25 +432,11 @@ def _describe(self, columns: list["TableColumn"]): columns[-1] = columns[-1].with_width(new_width) table_width = corr_width - # - Format the header # Format the method method = self.method.name if self.property_method != self.method: method += f" ({self.property_method.name})" - # Format the state information: kind, spin_change and convergence - state_info = [] - if hasattr(self, "kind") and self.kind: - state_info.append(self.kind) - if hasattr(self, "spin_change") and self.spin_change is not None and \ - self.spin_change != 0: - state_info.append(f"(ΔMS={self.spin_change:+2d})") - if hasattr(self, "converged"): - conv = "converged" if self.converged else "NOT CONVERGED" - if state_info: # add separator to previous entry - state_info[-1] += "," - state_info.append(conv) - state_info = " ".join(state_info) - # actually format the header + # Format the header if table_width > len(method) + 4: header = ( # -4 for the spaces f"{method:s} {state_info:>{str(table_width - len(method) - 4)}s}" @@ -694,6 +681,38 @@ def format(self, amplitude: AmplitudeVector) -> list[str]: + spin_coeff_gap + self.value_format ) } + elif self.matrix.axis_blocks == ["h"]: + formats = {"o": ( + "{} -> " + idx_spin_gap + "{}->" + + spin_coeff_gap + self.value_format + )} + elif self.matrix.axis_blocks == ["h", "phh"]: + formats = { + "o": ( + empty_idx + " {} -> " + empty_idx + idx_spin_gap + + " {}-> " + spin_coeff_gap + self.value_format + ), + "oov": ( + "{} {} -> {}" + idx_spin_gap + "{}{}->{}" + + spin_coeff_gap + self.value_format + ) + } + elif self.matrix.axis_blocks == ["p"]: + formats = {"v": ( + " -> {}" + idx_spin_gap + "->{}" + + spin_coeff_gap + self.value_format + )} + elif self.matrix.axis_blocks == ["p", "pph"]: + formats = { + "v": ( + empty_idx + " -> {} " + empty_idx + idx_spin_gap + + " ->{} " + spin_coeff_gap + self.value_format + ), + "ovv": ( + "{} -> {} {}" + idx_spin_gap + + "{}->{}{}" + spin_coeff_gap + self.value_format + ) + } else: raise NotImplementedError("Unknown ADC matrix structure") diff --git a/adcc/ExcitedStates.py b/adcc/ExcitedStates.py index 1e0803da2..3b9438dc1 100644 --- a/adcc/ExcitedStates.py +++ b/adcc/ExcitedStates.py @@ -144,8 +144,24 @@ def describe(self, oscillator_strengths=True, rotatory_strengths=False, unit="x(au) y(au) z(au) abs(au)" )) values.clear() - values.clear() - return self._describe(columns) + + # Format the state information: kind, spin_change and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + if hasattr(self, "spin_change") and self.spin_change is not None and \ + self.spin_change != 0: + # For PP, spin_change can only be integer values + spin_change = int(self.spin_change) + state_info.append(f"(ΔMS={spin_change:+2d})") + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + + return self._describe(columns, state_info) def to_qcvars(self, properties=False, recurse=False): """ diff --git a/adcc/State2States.py b/adcc/State2States.py index 257c2e5a7..b65b0896d 100644 --- a/adcc/State2States.py +++ b/adcc/State2States.py @@ -22,7 +22,7 @@ ## --------------------------------------------------------------------- import numpy as np -from . import adc_pp +from . import adc_pp, adc_ip, adc_ea from .ElectronicStates import _timer_name from .ElectronicTransition import ElectronicTransition from .misc import cached_member_function @@ -65,6 +65,10 @@ def __init__(self, data, method=None, property_method=None, initial=0): # the module according to the method. if self.method.adc_type == "pp": self._module = adc_pp + elif self.method.adc_type == "ip": + self._module = adc_ip + elif self.method.adc_type == "ea": + self._module = adc_ea else: raise ValueError(f"Unknown adc_type {self.method.adc_type}.") From 6ad844d39be11e569ea330d3a44dbd66306140d9 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Thu, 22 Jan 2026 16:46:18 +0100 Subject: [PATCH 07/28] slight refactorization of guess setup for simplicity --- adcc/__init__.py | 14 +++- adcc/guess/Guesses.py | 159 ------------------------------------ adcc/guess/__init__.py | 127 +++++++++++++++++++++++++++++ adcc/guess/util.py | 87 ++++++++++++++++++++ adcc/workflow.py | 180 ++++++++++++++++++++++++++++------------- 5 files changed, 350 insertions(+), 217 deletions(-) delete mode 100644 adcc/guess/Guesses.py create mode 100644 adcc/guess/__init__.py create mode 100644 adcc/guess/util.py diff --git a/adcc/__init__.py b/adcc/__init__.py index 075bb4205..61c69f154 100644 --- a/adcc/__init__.py +++ b/adcc/__init__.py @@ -36,6 +36,8 @@ from .memory_pool import memory_pool from .State2States import State2States from .ExcitedStates import ExcitedStates +from .ChargedExcitations import (ChargedExcitation, DetachedStates, + AttachedStates) from .Excitation import Excitation from .ElectronicTransition import ElectronicTransition from .DataHfProvider import DataHfProvider, DictHfProvider @@ -45,8 +47,9 @@ from .opt_einsum_integration import register_with_opt_einsum # This has to be the last set of import -from .guess.Guesses import Guesses -from .guess.guess_zero import guess_symmetries, guess_zero +from .guess import (guess_symmetries, guess_zero, guesses_any, + guesses_singlet, guesses_spin_flip, guesses_triplet, + guesses_doublet) from .workflow import run_adc from .exceptions import InputError @@ -57,9 +60,12 @@ "linear_combination", "zeros_like", "direct_sum", "memory_pool", "set_n_threads", "get_n_threads", "AmplitudeVector", "HartreeFockProvider", "ExcitedStates", "State2States", + "ChargedExcitation", "DetachedStates", "AttachedStates" "Excitation", "ElectronicTransition", "Tensor", "DictHfProvider", - "DataHfProvider", "OneParticleOperator", - "Guesses", "guess_symmetries", "guess_zero", "LazyMp", + "DataHfProvider", "OneParticleOperator", "LazyMp", + "guesses_singlet", "guesses_triplet", "guesses_doublet", + "guesses_any", "guesses_spin_flip", "get_spin_block_symmetrisation", + "guess_symmetries", "guess_zero", "guesses_from_diagonal", "adc0", "cis", "adc1", "adc2", "adc2x", "adc3", "cvs_adc0", "cvs_adc1", "cvs_adc2", "cvs_adc2x", "cvs_adc3", "ip_adc0", "ip_adc1", "ip_adc2", "ip_adc2x", "ip_adc3", diff --git a/adcc/guess/Guesses.py b/adcc/guess/Guesses.py deleted file mode 100644 index 279b38676..000000000 --- a/adcc/guess/Guesses.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab -## --------------------------------------------------------------------- -## -## Copyright (C) 2018 by the adcc authors -## -## This file is part of adcc. -## -## adcc is free software: you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## adcc is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with adcc. If not, see . -## -## --------------------------------------------------------------------- - -from .guesses_from_diagonal import guesses_from_diagonal -from adcc.exceptions import InputError -import warnings - - -def determine_spin_change(adc_type: str, kind: str, is_alpha: bool): - if adc_type == "pp": - if kind == "spin_flip": - spin_change = -1 - else: - spin_change = 0 - elif adc_type == "ip": - if kind == "spin_flip": - spin_change = 0 - else: - spin_change = +0.5 - int(is_alpha) - elif adc_type == "ea": - if kind == "spin_flip": - spin_change = 0 - else: - spin_change = -0.5 + int(is_alpha) - return spin_change - - -class Guesses: - def __init__(self, guesses: list, kind: str, - adc_type: str, is_alpha: bool): - if guesses is None: - self.guesses = [] - else: - self.guesses = guesses - self.n_guesses = len(self.guesses) - self.kind = kind - self.is_alpha = is_alpha - - symmetrisation = { - "singlet": "symmetric", - "doublet": "none", - "triplet": "antisymmetric", - "spin_flip":"none", - "any": "none" - } - self.spin_block_symmetrisation = symmetrisation.get(kind, 'none') - - if guesses is not None: - self.spin_change = None - else: - self.spin_change = determine_spin_change(adc_type, kind, is_alpha) - - - def estimate_n_guesses(self, matrix, n_states, n_guesses_per_state=2, - singles_only=True): - """ - Implementation of a basic heuristic to find a good number of guess - vectors to be searched for using the find_guesses function. - Internal function called from run_adc. - - matrix ADC matrix - n_states Number of states to be computed - singles_only Try to stay withing the singles excitation space - with the number of guess vectors. - n_guesses_per_state Number of guesses to search for for each state - """ - # Try to use at least 4 or twice the number of states - # to be computed as guesses - n_guesses = n_guesses_per_state * max(2, n_states) - - if singles_only: - # Compute the maximal number of sensible singles block guesses. - # This is roughly the number of occupied alpha orbitals - # times the number of virtual alpha orbitals - # - # If the system is core valence separated, then only the - # core electrons count as "occupied". - mospaces = matrix.mospaces - sp_occ = "o2" if matrix.is_core_valence_separated else "o1" - n_virt_a = mospaces.n_orbs_alpha("v1") - n_occ_a = mospaces.n_orbs_alpha(sp_occ) - estimate = n_occ_a * n_virt_a - if matrix.method.level < 2 and matrix.method.adc_type != "pp": - # Adjustment for IP- and EA-ADC(0/1) calculations - estimate = n_occ_a if matrix.method.adc_type == "ip" else n_virt_a - n_guesses = min(n_guesses, estimate) - - # Adjust if we overshoot the maximal number of sensible singles block - # guesses, but make sure we get at least n_states guesses - self.n_guesses = max(n_states, n_guesses) - - - def obtain_guesses_by_inspection(self, matrix, n_guesses_doubles=None, - **kwargs): - """ - Obtain guesses by inspecting the diagonal matrix elements. - If n_guesses_doubles is not None, this number is always adhered to. - Otherwise the number of doubles guesses is adjusted to fill up whatever - the singles guesses cannot provide to reach n_guesses. - - matrix The matrix for which guesses are to be constructed - is_alpha Is the detached/attached electron alpha spin for the - respective IP-/EA-ADC calculation. - kwargs Any other argument understood by guesses_from_diagonal. - """ - if n_guesses_doubles is not None and len(matrix.axis_blocks) < 2: - raise InputError("n_guesses_doubles > 0 is only sensible if the " - "ADC method has a doubles block (i.e. it is *not* " - "ADC(0), ADC(1) or a variant thereof.") - - # Determine number of singles guesses to request - if n_guesses_doubles is None: - n_guesses_doubles = 0 - else: - if len(matrix.axis_blocks) < 2: - warnings.warn("Ignoring n_guesses_doubles parameter, since no " - "double guesses exist.") - - n_guesses_singles = self.n_guesses - n_guesses_doubles - - self.guesses += guesses_from_diagonal( - matrix, n_guesses_singles, matrix.axis_blocks[0], self.kind, - self.is_alpha, self.spin_change, self.spin_block_symmetrisation, - **kwargs) - - # Determine number of doubles guesses to request if not - # explicitly specified - n_guesses_doubles = self.n_guesses - len(self.guesses) - if n_guesses_doubles > 0: - self.guesses += guesses_from_diagonal( - matrix, n_guesses_doubles, matrix.axis_blocks[1], self.kind, - self.is_alpha, self.spin_change, - self.spin_block_symmetrisation, **kwargs) - - if len(self.guesses) < self.n_guesses: - raise InputError("Less guesses found than requested: {} found, " - "{} requested".format( - len(self.guesses), self.n_guesses)) - self.n_guesses = len(self.guesses) \ No newline at end of file diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py new file mode 100644 index 000000000..c30dccf25 --- /dev/null +++ b/adcc/guess/__init__.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2020 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from .guess_zero import guess_symmetries, guess_zero +from .guesses_from_diagonal import guesses_from_diagonal +from .util import estimate_n_guesses, determine_spin_change + +__all__ = ["guess_zero", "guesses_from_diagonal", + "get_spin_block_symmetrisation", + "guesses_singlet", "guesses_triplet", "guesses_any", + "guesses_spin_flip", "guess_symmetries", + "estimate_n_guesses", "determine_spin_change"] + + +def get_spin_block_symmetrisation(kind: str) -> str: + """ + Return the kwargs required to be passed to `guesses_from_diagonal` to + computed states of the passed excitation `kind`. + """ + kwargsmap = { + "singlet": "symmetric", + "doublet": "none", + "triplet": "antisymmetric", + "spin_flip":"none", + "any": "none" + } + try: + return kwargsmap[kind] + except KeyError: + raise ValueError(f"Kind not known: {kind}") + + +def guesses_singlet(matrix, n_guesses, block="ph", **kwargs): + """ + Obtain guesses for computing singlet states by inspecting the passed + ADC matrix. + + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + return guesses_from_diagonal(matrix, n_guesses, block=block, + spin_block_symmetrisation=guess_kwargs_kind("singlet"), **kwargs) + + +def guesses_doublet(matrix, n_guesses, block="h", is_alpha=None, **kwargs): + """ + Obtain guesses for computing doublet states by inspecting the passed + ADC matrix. + + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + return guesses_from_diagonal(matrix, n_guesses, block=block, + is_alpha=is_alpha, + **guess_kwargs_kind("doublet"), + **kwargs) + + +def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): + """ + Obtain guesses for computing triplet states by inspecting the passed + ADC matrix. + + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + return guesses_from_diagonal(matrix, n_guesses, block=block, + **guess_kwargs_kind("triplet"), **kwargs) + + +# guesses for computing any state (excluding spin-flip states) +guesses_any = guesses_from_diagonal + + +def guesses_spin_flip(matrix, n_guesses, block="ph", **kwargs): + """ + Obtain guesses for computing spin-flip states by inspecting the passed + ADC matrix. + + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + return guesses_from_diagonal(matrix, n_guesses, block=block, + **guess_kwargs_kind("spin_flip"), **kwargs) \ No newline at end of file diff --git a/adcc/guess/util.py b/adcc/guess/util.py new file mode 100644 index 000000000..8840c16db --- /dev/null +++ b/adcc/guess/util.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2018 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + +from ..AdcMethod import AdcMethod +from typing import Optional + + +def determine_spin_change(method: AdcMethod, kind: str, + is_alpha: Optional[bool]): + if method.adc_type == "pp": + if kind == "spin_flip": + spin_change = -1.0 + else: + spin_change = 0.0 + elif method.adc_type == "ip": + if kind == "spin_flip": + raise ValueError("Spin flip is only valid for PP-ADC") + else: + spin_change = +0.5 - int(is_alpha) + elif method.adc_type == "ea": + if kind == "spin_flip": + raise ValueError("Spin flip is only valid for PP-ADC") + else: + spin_change = -0.5 + int(is_alpha) + else: + raise ValueError(f"Unknown ADC method: {method.name}") + return spin_change + + +def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2, + singles_only=True): + """ + Implementation of a basic heuristic to find a good number of guess + vectors to be searched for using the find_guesses function. + Internal function called from run_adc. + + matrix ADC matrix + n_states Number of states to be computed + singles_only Try to stay withing the singles excitation space + with the number of guess vectors. + n_guesses_per_state Number of guesses to search for for each state + """ + # Try to use at least 4 or twice the number of states + # to be computed as guesses + n_guesses = n_guesses_per_state * max(2, n_states) + + if singles_only: + # Compute the maximal number of sensible singles block guesses. + # This is roughly the number of occupied alpha orbitals + # times the number of virtual alpha orbitals + # + # If the system is core valence separated, then only the + # core electrons count as "occupied". + mospaces = matrix.mospaces + sp_occ = "o2" if matrix.is_core_valence_separated else "o1" + n_virt_a = mospaces.n_orbs_alpha("v1") + n_occ_a = mospaces.n_orbs_alpha(sp_occ) + estimate = n_occ_a * n_virt_a + if matrix.method.level < 2 and matrix.method.adc_type != "pp": + # Adjustment for IP- and EA-ADC(0/1) calculations + estimate = (n_occ_a if matrix.method.adc_type == "ip" + else n_virt_a) + n_guesses = min(n_guesses, estimate) + + # Adjust if we overshoot the maximal number of sensible singles block + # guesses, but make sure we get at least n_states guesses + return max(n_states, n_guesses) \ No newline at end of file diff --git a/adcc/workflow.py b/adcc/workflow.py index aa9796852..0737230be 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -25,13 +25,18 @@ from libadcc import ReferenceState +from typing import Optional + from . import solver -from .guess.Guesses import Guesses +from .guess import (determine_spin_change, estimate_n_guesses, + guesses_from_diagonal, get_spin_block_symmetrisation) from .LazyMp import LazyMp from .AdcMatrix import AdcMatrix, AdcMatrixlike, AdcExtraTerm from .AdcMethod import AdcMethod +from .AmplitudeVector import AmplitudeVector from .exceptions import InputError from .ExcitedStates import ExcitedStates +from .ChargedExcitations import DetachedStates, AttachedStates from .ReferenceState import ReferenceState as adcc_ReferenceState from .solver.lanczos import lanczos from .solver.davidson import jacobi_davidson @@ -214,10 +219,6 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, kind=kind, is_alpha=is_alpha) - # Construct guesses - solver_guesses = construct_guesses(matrix, n_states, guesses, n_guesses, - n_guesses_doubles, kind, is_alpha, eigensolver) - # Setup environment coupling terms and energy corrections env_matrix_term, env_energy_corrections = setup_environment(matrix, environment) @@ -225,13 +226,44 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, if env_matrix_term: matrix += env_matrix_term + # Construct guesses and determine spin_change + if guesses is None: + spin_change = determine_spin_change(matrix.method, kind, is_alpha) + guesses = construct_guesses( + matrix, n_states, kind, spin_change, n_guesses, n_guesses_doubles, + is_alpha, eigensolver + ) + else: + if len(guesses) < n_states: + raise InputError("Less guesses provided via guesses (== {}) " + "than states to be computed (== {})" + "".format(len(guesses), n_states)) + if n_guesses is not None: + warnings.warn("Ignoring n_guesses parameter, since guesses are " + "explicitly provided.") + if n_guesses_doubles is not None: + warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" + " are explicitly provided.") + # Set spin_change to None since we don't know if guesses are provided + spin_change = None + + diagres = diagonalise_adcmatrix( - matrix, n_states, guesses=solver_guesses, conv_tol=conv_tol, - output=output, eigensolver=eigensolver, **solverargs) - exstates = ExcitedStates(diagres) + matrix, n_states, guesses, kind, conv_tol=conv_tol, + output=output, eigensolver=eigensolver, is_alpha=is_alpha, + **solverargs) + + if matrix.method.adc_type == "pp": + exstates = ExcitedStates(diagres) + elif matrix.method.adc_type == "ip": + exstates = DetachedStates(diagres, is_alpha) + elif matrix.method.adc_type == "ea": + exstates = AttachedStates(diagres, is_alpha) + else: + raise ValueError(f"Unknown ADC method: {matrix.method.name}") + exstates.kind = kind - exstates.spin_change = solver_guesses.spin_change - exstates.is_alpha = is_alpha + exstates.spin_change = spin_change # add environment corrections to excited states exstates += env_energy_corrections @@ -402,55 +434,95 @@ def validate_state_parameters(matrix, n_states=None, n_singlets=None, return n_states, kind, is_alpha -def construct_guesses(matrix, n_states, guesses=None, n_guesses=None, - n_guesses_doubles=None, kind="any", is_alpha=None, - eigensolver="davidson"): +def obtain_guesses_by_inspection(matrix, n_guesses, kind, + n_guesses_doubles=None, is_alpha=None, + spin_change=None): + """ + Obtain guesses by inspecting the diagonal matrix elements. + If n_guesses_doubles is not None, this number is always adhered to. + Otherwise the number of doubles guesses is adjusted to fill up whatever + the singles guesses cannot provide to reach n_guesses. + + matrix The matrix for which guesses are to be constructed + is_alpha Is the detached/attached electron alpha spin for the + respective IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + spin_block_symmetrisation = get_spin_block_symmetrisation(kind) + + if n_guesses_doubles is not None and len(matrix.axis_blocks) < 2: + raise InputError("n_guesses_doubles > 0 is only sensible if the " + "ADC method has a doubles block (i.e. it is *not*" + " ADC(0), ADC(1) or a variant thereof.") + + # Determine number of singles guesses to request + if n_guesses_doubles is None: + n_guesses_doubles = 0 + + n_guesses_singles = n_guesses - n_guesses_doubles + + guesses = guesses_from_diagonal( + matrix, n_guesses_singles, matrix.axis_blocks[0], kind, + is_alpha, spin_change, spin_block_symmetrisation) + + # Determine number of doubles guesses to request if not + # explicitly specified + n_guesses_doubles = n_guesses - len(guesses) + if n_guesses_doubles > 0: + guesses += guesses_from_diagonal( + matrix, n_guesses_doubles, matrix.axis_blocks[1], kind, + is_alpha, spin_change, spin_block_symmetrisation) + + if len(guesses) < n_guesses: + raise InputError("Less guesses found than requested: {} found, " + "{} requested".format( + len(guesses), n_guesses)) + return guesses + + +def construct_guesses( + matrix: AdcMatrix, + n_states: int, + kind: str, + spin_change: float, + n_guesses: int, + n_guesses_doubles: Optional[int] = None, + is_alpha: Optional[bool] = None, + eigensolver: Optional[str] = "davidson" + ) -> list[AmplitudeVector]: """ This function constructs appropriate guesses if not given. - Returns a class object containing all crucial guess information. + Returns a :class:`Guesses` object containing all crucial guess information. Internal function called from run_adc. """ - solver_guesses = Guesses(guesses, kind, matrix.method.adc_type, is_alpha) - - # Obtain or check guesses - if guesses is None: - if n_guesses is None: - # Set solver-specific parameters - if eigensolver == "davidson": - n_guesses_per_state = 2 - else: - n_guesses_per_state = 1 - solver_guesses.estimate_n_guesses(matrix, n_states, - n_guesses_per_state) - solver_guesses.obtain_guesses_by_inspection(matrix, n_guesses_doubles) - else: - if len(guesses) < n_states: - raise InputError("Less guesses provided via guesses (== {}) " - "than states to be computed (== {})" - "".format(len(guesses), n_states)) - if n_guesses is not None: - warnings.warn("Ignoring n_guesses parameter, since guesses are " - "explicitly provided.") - if n_guesses_doubles is not None: - warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" - " are explicitly provided.") - solver_guesses.n_guesses = len(guesses) - - return solver_guesses + if n_guesses is None: + # Set solver-specific parameters + if eigensolver == "davidson": + n_guesses_per_state = 2 + else: + n_guesses_per_state = 1 + n_guesses = estimate_n_guesses(matrix, n_states, + n_guesses_per_state) + + return obtain_guesses_by_inspection( + matrix, n_guesses, kind, n_guesses_doubles, is_alpha, spin_change + ) + -def diagonalise_adcmatrix(matrix, n_states, guesses=None, conv_tol=None, +def diagonalise_adcmatrix(matrix, n_states, guesses, kind, conv_tol=None, eigensolver="davidson", output=sys.stdout, - **solverargs): + is_alpha=None, **solverargs): """ This function seeks appropriate guesses and afterwards proceeds to diagonalise the ADC matrix using the specified eigensolver. Internal function called from run_adc. - guesses : - Provide an instance of the 'Guesses' class containing a list of - guess vectors and all additional, relevant information as attributes: - 'n_guesses', 'kind', 'is_alpha', 'spin_block_symmetrisation' + matrix : AdcMatrix + n_states : int + guesses : list[AmplitudeVector] + A list of guess vectors + kind : str """ reference_state = matrix.reference_state @@ -466,28 +538,28 @@ def diagonalise_adcmatrix(matrix, n_states, guesses=None, conv_tol=None, # Determine explicit_symmetrisation explicit_symmetrisation = IndexSymmetrisation - if guesses.kind in ["singlet", "doublet", "triplet"]: + if kind in ["singlet", "doublet", "triplet"]: explicit_symmetrisation = IndexSpinSymmetrisation( - matrix, enforce_spin_kind=guesses.kind + matrix, enforce_spin_kind=kind ) # Set some solver-specific parameters if eigensolver == "davidson": callback = setup_solver_printing( - "Jacobi-Davidson", matrix, guesses.kind, - solver.davidson.default_print, is_alpha=guesses.is_alpha, + "Jacobi-Davidson", matrix, kind, + solver.davidson.default_print, is_alpha=is_alpha, output=output) run_eigensolver = jacobi_davidson elif eigensolver == "lanczos": callback = setup_solver_printing( - "Lanczos", matrix, guesses.kind, solver.lanczos.default_print, - is_alpha=guesses.is_alpha, output=output) + "Lanczos", matrix, kind, solver.lanczos.default_print, + is_alpha=is_alpha, output=output) run_eigensolver = lanczos else: raise InputError(f"Solver {eigensolver} unknown, try 'davidson'.") solverargs.setdefault("which", "SA") - return run_eigensolver(matrix, guesses.guesses, n_ep=n_states, + return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol, callback=callback, explicit_symmetrisation=explicit_symmetrisation, **solverargs) From 6a0fb824f2f79cf0d3d38c41678b3edb35d67fec Mon Sep 17 00:00:00 2001 From: fedy9 Date: Fri, 23 Jan 2026 13:08:58 +0100 Subject: [PATCH 08/28] corrected small mistakes and adapted some tests --- adcc/guess/__init__.py | 34 +++++++++++++++++--------- adcc/projection.py | 6 ++--- adcc/solver/explicit_symmetrisation.py | 9 ++++--- adcc/tests/workflow_test.py | 11 +++------ adcc/workflow.py | 17 +++++++------ 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py index c30dccf25..89ed751c1 100644 --- a/adcc/guess/__init__.py +++ b/adcc/guess/__init__.py @@ -36,7 +36,7 @@ def get_spin_block_symmetrisation(kind: str) -> str: Return the kwargs required to be passed to `guesses_from_diagonal` to computed states of the passed excitation `kind`. """ - kwargsmap = { + symmetrisation = { "singlet": "symmetric", "doublet": "none", "triplet": "antisymmetric", @@ -44,7 +44,7 @@ def get_spin_block_symmetrisation(kind: str) -> str: "any": "none" } try: - return kwargsmap[kind] + return symmetrisation[kind] except KeyError: raise ValueError(f"Kind not known: {kind}") @@ -63,8 +63,11 @@ def guesses_singlet(matrix, n_guesses, block="ph", **kwargs): IP-/EA-ADC calculation. kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - spin_block_symmetrisation=guess_kwargs_kind("singlet"), **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, + spin_block_symmetrisation=get_spin_block_symmetrisation("singlet"), + **kwargs + ) def guesses_doublet(matrix, n_guesses, block="h", is_alpha=None, **kwargs): @@ -81,10 +84,11 @@ def guesses_doublet(matrix, n_guesses, block="h", is_alpha=None, **kwargs): IP-/EA-ADC calculation. kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - is_alpha=is_alpha, - **guess_kwargs_kind("doublet"), - **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, is_alpha=is_alpha, + spin_block_symmetrisation= get_spin_block_symmetrisation("doublet"), + **kwargs + ) def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): @@ -101,8 +105,11 @@ def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): IP-/EA-ADC calculation. kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("triplet"), **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, + spin_block_symmetrisation= get_spin_block_symmetrisation("triplet"), + **kwargs + ) # guesses for computing any state (excluding spin-flip states) @@ -123,5 +130,8 @@ def guesses_spin_flip(matrix, n_guesses, block="ph", **kwargs): IP-/EA-ADC calculation. kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("spin_flip"), **kwargs) \ No newline at end of file + return guesses_from_diagonal( + matrix, n_guesses, block=block, + spin_block_symmetrisation= get_spin_block_symmetrisation("spin_flip"), + **kwargs + ) \ No newline at end of file diff --git a/adcc/projection.py b/adcc/projection.py index d6eee2580..0bfdbc803 100644 --- a/adcc/projection.py +++ b/adcc/projection.py @@ -26,7 +26,7 @@ import numpy as np -from .guess import guess_kwargs_kind, guess_symmetries +from .guess import get_spin_block_symmetrisation, guess_symmetries from .Tensor import Tensor from .MoSpaces import expand_spaceargs from .Symmetry import Symmetry @@ -333,7 +333,7 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, "ExcitedStates object and spin symmetry setup is not " "explicitly given.") return transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector, kind, - **guess_kwargs_kind(kind)) + get_spin_block_symmetrisation(kind)) if vector is None: if hasattr(state_matrix_cvs, "excitation_vector"): @@ -343,7 +343,7 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, "ExcitedStates object.") if isinstance(vector, list): return [transfer_cvs_to_full(state_matrix_cvs, matrix_full, v, kind, - **guess_kwargs_kind(kind)) for v in vector] + get_spin_block_symmetrisation(kind)) for v in vector] if isinstance(state_matrix_cvs, AdcMatrixlike): mospaces_cvs = state_matrix_cvs.mospaces diff --git a/adcc/solver/explicit_symmetrisation.py b/adcc/solver/explicit_symmetrisation.py index d49e55bf0..01a16b031 100644 --- a/adcc/solver/explicit_symmetrisation.py +++ b/adcc/solver/explicit_symmetrisation.py @@ -76,6 +76,8 @@ class IndexSpinSymmetrisation(IndexSymmetrisation): def __init__(self, matrix, enforce_spin_kind="singlet"): super().__init__(matrix) self.enforce_spin_kind = enforce_spin_kind + # Bool to distinguish IP/EA if 'spin_kind=='doublet' + self.is_ip_adc = matrix.method.adc_type == "ip" def symmetrise(self, new_vectors): if isinstance(new_vectors, AmplitudeVector): @@ -87,12 +89,13 @@ def symmetrise(self, new_vectors): for vec in new_vectors: # Only work on the doubles part # the other blocks are not yet implemented - # or nothing needs to be done ("ph" block) - if "pphh" in vec.blocks: + # or nothing needs to be done ("ph"/"h"/"p" block) + if len(vec.blocks) > 1: # TODO: Note that the "d" is needed here because the C++ side # does not yet understand ph and pphh amplitude_vector_enforce_spin_kind( - vec.pphh, "d", self.enforce_spin_kind + vec.get(vec.blocks[1]), "d", self.enforce_spin_kind, + self.is_ip_adc ) return new_vectors diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py index 6dbc8e6d2..15dcd8693 100644 --- a/adcc/tests/workflow_test.py +++ b/adcc/tests/workflow_test.py @@ -209,11 +209,6 @@ def test_diagonalise_adcmatrix(self): matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) - res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, - eigensolver="davidson") - assert res.converged - assert res.eigenvalues[:n_states] == approx(ref_singlets[:n_states]) - guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="ph") res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, guesses=guesses) @@ -223,14 +218,14 @@ def test_diagonalise_adcmatrix(self): with pytest.raises(InputError): # Too low tolerance # SCF tolerance = 1e-14 currently res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, - eigensolver="davidson", + guesses=guesses,eigensolver="davidson", conv_tol=1e-15) with pytest.raises(InputError): # Wrong solver method res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, - eigensolver="blubber") + guesses=guesses, eigensolver="blubber") - with pytest.raises(InputError): # Too few guesses + with pytest.raises(ValueError): # Too few guesses res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, eigensolver="davidson", guesses=guesses) diff --git a/adcc/workflow.py b/adcc/workflow.py index 0737230be..fd39bab00 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -249,7 +249,7 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, diagres = diagonalise_adcmatrix( - matrix, n_states, guesses, kind, conv_tol=conv_tol, + matrix, n_states, guesses, kind=kind, conv_tol=conv_tol, output=output, eigensolver=eigensolver, is_alpha=is_alpha, **solverargs) @@ -436,7 +436,7 @@ def validate_state_parameters(matrix, n_states=None, n_singlets=None, def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_doubles=None, is_alpha=None, - spin_change=None): + spin_change=0): """ Obtain guesses by inspecting the diagonal matrix elements. If n_guesses_doubles is not None, this number is always adhered to. @@ -450,11 +450,6 @@ def obtain_guesses_by_inspection(matrix, n_guesses, kind, """ spin_block_symmetrisation = get_spin_block_symmetrisation(kind) - if n_guesses_doubles is not None and len(matrix.axis_blocks) < 2: - raise InputError("n_guesses_doubles > 0 is only sensible if the " - "ADC method has a doubles block (i.e. it is *not*" - " ADC(0), ADC(1) or a variant thereof.") - # Determine number of singles guesses to request if n_guesses_doubles is None: n_guesses_doubles = 0 @@ -468,7 +463,13 @@ def obtain_guesses_by_inspection(matrix, n_guesses, kind, # Determine number of doubles guesses to request if not # explicitly specified n_guesses_doubles = n_guesses - len(guesses) + if n_guesses_doubles > 0: + if matrix.method.level < 2: + raise InputError("n_guesses_doubles > 0 is only sensible if the " + "ADC method has a doubles block (i.e. it is *not*" + " ADC(0), ADC(1) or a variant thereof.") + guesses += guesses_from_diagonal( matrix, n_guesses_doubles, matrix.axis_blocks[1], kind, is_alpha, spin_change, spin_block_symmetrisation) @@ -510,7 +511,7 @@ def construct_guesses( -def diagonalise_adcmatrix(matrix, n_states, guesses, kind, conv_tol=None, +def diagonalise_adcmatrix(matrix, n_states, guesses, kind="any", conv_tol=None, eigensolver="davidson", output=sys.stdout, is_alpha=None, **solverargs): """ From b16612f30b4e727b7f1ac3f1f8af87e2a6f12133 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Fri, 6 Feb 2026 10:28:28 +0100 Subject: [PATCH 09/28] refactored guess setup separating 'spin_change' and 'spin_block_symmetrisation' adapted and projection_test runs again --- adcc/guess/util.py | 2 +- adcc/projection.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/adcc/guess/util.py b/adcc/guess/util.py index 8840c16db..55fde897c 100644 --- a/adcc/guess/util.py +++ b/adcc/guess/util.py @@ -26,7 +26,7 @@ def determine_spin_change(method: AdcMethod, kind: str, - is_alpha: Optional[bool]): + is_alpha: Optional[bool] = None): if method.adc_type == "pp": if kind == "spin_flip": spin_change = -1.0 diff --git a/adcc/projection.py b/adcc/projection.py index 0bfdbc803..b3b7fa749 100644 --- a/adcc/projection.py +++ b/adcc/projection.py @@ -26,7 +26,8 @@ import numpy as np -from .guess import get_spin_block_symmetrisation, guess_symmetries +from .guess import (get_spin_block_symmetrisation, guess_symmetries, + determine_spin_change) from .Tensor import Tensor from .MoSpaces import expand_spaceargs from .Symmetry import Symmetry @@ -332,8 +333,10 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, raise ValueError("kind needs to be given if first argument is not an " "ExcitedStates object and spin symmetry setup is not " "explicitly given.") - return transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector, kind, - get_spin_block_symmetrisation(kind)) + return transfer_cvs_to_full( + state_matrix_cvs, matrix_full, vector, kind, + spin_change=determine_spin_change(matrix_full.method, kind), + spin_block_symmetrisation=get_spin_block_symmetrisation(kind)) if vector is None: if hasattr(state_matrix_cvs, "excitation_vector"): @@ -342,8 +345,11 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, raise ValueError("vector needs to be given if first argument is not an " "ExcitedStates object.") if isinstance(vector, list): - return [transfer_cvs_to_full(state_matrix_cvs, matrix_full, v, kind, - get_spin_block_symmetrisation(kind)) for v in vector] + return [transfer_cvs_to_full( + state_matrix_cvs, matrix_full, v, kind, + spin_change=determine_spin_change(matrix_full.method, kind), + spin_block_symmetrisation=get_spin_block_symmetrisation(kind) + ) for v in vector] if isinstance(state_matrix_cvs, AdcMatrixlike): mospaces_cvs = state_matrix_cvs.mospaces From 7a41274f283914fe0ea8978eb469d48bc41f6a70 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 10 Feb 2026 18:11:31 +0100 Subject: [PATCH 10/28] Make Davidson subspace max_iter parameter available to user --- adcc/solver/davidson.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/adcc/solver/davidson.py b/adcc/solver/davidson.py index d0588c7cc..185e590da 100644 --- a/adcc/solver/davidson.py +++ b/adcc/solver/davidson.py @@ -78,7 +78,8 @@ def default_print(state, identifier, file=sys.stdout): def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep, n_block, is_converged, which, callback=None, preconditioner=None, preconditioning_method="Davidson", debug_checks=False, - residual_min_norm=None, explicit_symmetrisation=None): + residual_min_norm=None, explicit_symmetrisation=None, + max_subspace_iter=None): """Drive the davidson iterations Parameters @@ -119,6 +120,8 @@ def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep, n_block, Explicit symmetrisation to apply to new subspace vectors before adding them to the subspace. Allows to correct for loss of index or spin symmetries (type or instance) + max_subspace_iter : int, optional + Maximum number of iterations for diagonalizing the subspace matrix """ if preconditioning_method not in ["Davidson", "Sleijpen-van-der-Vorst"]: raise ValueError("Only 'Davidson' and 'Sleijpen-van-der-Vorst' " @@ -186,7 +189,8 @@ def callback(state, identifier): # TODO Maybe play with precision a little here # TODO Maybe use previous vectors somehow v0 = None - rvals, rvecs = sla.eigsh(Ass, k=n_block, which=which, v0=v0) + rvals, rvecs = sla.eigsh(Ass, k=n_block, which=which, v0=v0, + maxiter=max_subspace_iter) with state.timer.record("residuals"): # Form residuals, A * SS * v - λ * SS * v = Ax * v + SS * (-λ*v) @@ -350,7 +354,8 @@ def eigsh(matrix, guesses, n_ep=None, n_block=None, max_subspace=None, conv_tol=1e-9, which="SA", max_iter=70, callback=None, preconditioner=None, preconditioning_method="Davidson", debug_checks=False, - residual_min_norm=None, explicit_symmetrisation=IndexSymmetrisation): + residual_min_norm=None, explicit_symmetrisation=IndexSymmetrisation, + max_subspace_iter= None): """Davidson eigensolver for ADC problems Parameters @@ -391,6 +396,12 @@ def eigsh(matrix, guesses, n_ep=None, n_block=None, max_subspace=None, Minimal norm a residual needs to have in order to be accepted as a new subspace vector (defaults to 2 * len(matrix) * machine_expsilon) + explicit_symmetrisation + Explicit symmetrisation to apply to new subspace vectors before + adding them to the subspace. Allows to correct for loss of index + or spin symmetries (type or instance) + max_subspace_iter : int, optional + Maximum number of iterations for diagonalizing the subspace matrix """ if not isinstance(matrix, AdcMatrixlike): raise TypeError("matrix is not of type AdcMatrixlike") @@ -452,7 +463,8 @@ def convergence_test(state): preconditioning_method=preconditioning_method, debug_checks=debug_checks, residual_min_norm=residual_min_norm, - explicit_symmetrisation=explicit_symmetrisation) + explicit_symmetrisation=explicit_symmetrisation, + max_subspace_iter=max_subspace_iter) return state From ac03f0c93b6604b81922acb8e67264530c9e2faf Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 10:19:30 +0100 Subject: [PATCH 11/28] added pole strengths for ip/ea-adc2x (which is adc2) --- adcc/adc_ea/pole_strength.py | 1 + adcc/adc_ip/pole_strength.py | 1 + 2 files changed, 2 insertions(+) diff --git a/adcc/adc_ea/pole_strength.py b/adcc/adc_ea/pole_strength.py index fe3720f2c..9f5bd1654 100644 --- a/adcc/adc_ea/pole_strength.py +++ b/adcc/adc_ea/pole_strength.py @@ -149,6 +149,7 @@ def ea_adc3_f22(hf, mp, intermediates): "ea-adc0": pole_strength_ea_adc0, "ea-adc1": pole_strength_ea_adc0, "ea-adc2": pole_strength_ea_adc2, + "ea-adc2x": pole_strength_ea_adc2, "ea-adc3": pole_strength_ea_adc3, } diff --git a/adcc/adc_ip/pole_strength.py b/adcc/adc_ip/pole_strength.py index b0312c187..fc1ec6b8b 100644 --- a/adcc/adc_ip/pole_strength.py +++ b/adcc/adc_ip/pole_strength.py @@ -180,6 +180,7 @@ def m_3_minus(hf, mp, intermediates): "ip-adc0": pole_strength_ip_adc0, "ip-adc1": pole_strength_ip_adc0, "ip-adc2": pole_strength_ip_adc2, + "ip-adc2x": pole_strength_ip_adc2, "ip-adc3": pole_strength_ip_adc3, } From 1781cbe91f8cfeb4e3ae29cc6b35593bbfbdb9eb Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 10:22:19 +0100 Subject: [PATCH 12/28] prettied up --- adcc/guess/util.py | 23 ++++++++++------------- adcc/workflow.py | 10 ++++++---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/adcc/guess/util.py b/adcc/guess/util.py index 55fde897c..aa0eb7ebc 100644 --- a/adcc/guess/util.py +++ b/adcc/guess/util.py @@ -26,29 +26,26 @@ def determine_spin_change(method: AdcMethod, kind: str, - is_alpha: Optional[bool] = None): + is_alpha: Optional[bool] = None) -> int | float: if method.adc_type == "pp": if kind == "spin_flip": - spin_change = -1.0 + return -1 else: - spin_change = 0.0 + return 0 elif method.adc_type == "ip": - if kind == "spin_flip": - raise ValueError("Spin flip is only valid for PP-ADC") - else: - spin_change = +0.5 - int(is_alpha) + if is_alpha is None: + raise TypeError("'is_alpha' has to be True|False for IP-ADC") + return +0.5 - int(is_alpha) elif method.adc_type == "ea": - if kind == "spin_flip": - raise ValueError("Spin flip is only valid for PP-ADC") - else: - spin_change = -0.5 + int(is_alpha) + if is_alpha is None: + raise TypeError("'is_alpha' has to be True|False for EA-ADC") + return -0.5 + int(is_alpha) else: raise ValueError(f"Unknown ADC method: {method.name}") - return spin_change def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2, - singles_only=True): + singles_only=True) -> int: """ Implementation of a basic heuristic to find a good number of guess vectors to be searched for using the find_guesses function. diff --git a/adcc/workflow.py b/adcc/workflow.py index fd39bab00..5ab19e896 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -339,7 +339,8 @@ def construct_adcmatrix(data_or_matrix, core_orbitals=None, frozen_core=None, def validate_state_parameters(matrix, n_states=None, n_singlets=None, n_doublets=None, n_triplets=None, - n_spin_flip=None, kind="any", is_alpha=None): + n_spin_flip=None, kind="any", is_alpha=None + ) -> tuple[int, str, Optional[bool]]: """ Check the passed state parameters for consistency with itself and with the passed reference and normalise them. In the end return the number of @@ -372,6 +373,7 @@ def validate_state_parameters(matrix, n_states=None, n_singlets=None, "with n_doublets > 0") kind = "doublet" n_states = n_doublets + is_alpha = True if n_triplets is not None: if not reference_state.restricted: raise InputError("The n_triplets parameter may only be employed " @@ -457,8 +459,9 @@ def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_singles = n_guesses - n_guesses_doubles guesses = guesses_from_diagonal( - matrix, n_guesses_singles, matrix.axis_blocks[0], kind, - is_alpha, spin_change, spin_block_symmetrisation) + matrix, n_guesses_singles, block=matrix.axis_blocks[0], kind=kind, + is_alpha=is_alpha, spin_change=spin_change, + spin_block_symmetrisation=spin_block_symmetrisation) # Determine number of doubles guesses to request if not # explicitly specified @@ -510,7 +513,6 @@ def construct_guesses( ) - def diagonalise_adcmatrix(matrix, n_states, guesses, kind="any", conv_tol=None, eigensolver="davidson", output=sys.stdout, is_alpha=None, **solverargs): From 3603f1c9c6ea6ca8bb53e1502cf35708652ad8ed Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 10:24:06 +0100 Subject: [PATCH 13/28] refactored test setup to generate ip/ea reference data from adcc and adcman --- adcc/tests/generators/dump_adcc.py | 76 ++++++--- adcc/tests/generators/generate_adcc_data.py | 156 ++++++++++++++++-- adcc/tests/generators/generate_adcman_data.py | 114 +++++++++++-- adcc/tests/generators/import_qchem_data.py | 72 ++++++-- adcc/tests/generators/run_qchem.py | 37 ++++- adcc/tests/testcases.py | 40 +++-- adcc/tests/testdata_cache.py | 125 +++++++++----- 7 files changed, 505 insertions(+), 115 deletions(-) diff --git a/adcc/tests/generators/dump_adcc.py b/adcc/tests/generators/dump_adcc.py index 0fd07bf54..5f7437fa5 100644 --- a/adcc/tests/generators/dump_adcc.py +++ b/adcc/tests/generators/dump_adcc.py @@ -2,6 +2,7 @@ from adcc.AdcMatrix import AdcMatrix from adcc.AmplitudeVector import AmplitudeVector from adcc.ExcitedStates import ExcitedStates +from adcc.ChargedExcitations import DetachedStates, AttachedStates from adcc.hdf5io import emplace_dict from adcc.LazyMp import LazyMp from adcc.State2States import State2States @@ -9,6 +10,7 @@ import numpy as np import h5py +from typing import Union def dump_groundstate(ground_state: LazyMp, hdf5_file: h5py.Group) -> None: """ @@ -52,10 +54,52 @@ def dump_groundstate(ground_state: LazyMp, hdf5_file: h5py.Group) -> None: hdf5_file.attrs["adcc_version"] = adcc.__version__ -def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, - dump_nstates: int = None) -> None: +def _pp_adc_properties(states: ExcitedStates, kind_data: dict, n_states: int) -> None: + """Collects all properties that are unique for PP-ADC """ - Dump the excited states data to the given hdf5 file/group. + tdm_bb_a = [] # Ground to Excited state tdm AO basis alpha part + tdm_bb_b = [] # Ground to Excited state tdm AO basis beta part + + for n in range(n_states): + # TDMs + bb_a, bb_b = states.transition_dm[n].to_ao_basis(states.reference_state) + tdm_bb_a.append(bb_a.to_ndarray()) + tdm_bb_b.append(bb_b.to_ndarray()) + + kind_data["transition_dipole_moments"] = ( + states.transition_dipole_moment[:n_states] + ) + kind_data["transition_dipole_moments_velocity"] = ( + states.transition_dipole_moment_velocity[:n_states] + ) + + gauge_origins = ["origin", "mass_center", "charge_center"] + for g_origin in gauge_origins: + kind_data[f"transition_magnetic_dipole_moments_{g_origin}"] = ( + states.transition_magnetic_dipole_moment(g_origin)[:n_states] + ) + kind_data[f"transition_quadrupole_moments_{g_origin}"] = ( + states.transition_quadrupole_moment(g_origin)[:n_states] + ) + # ground to excited state tdm + kind_data["ground_to_excited_tdm_bb_a"] = np.asarray(tdm_bb_a) + kind_data["ground_to_excited_tdm_bb_b"] = np.asarray(tdm_bb_b) + + +def _ip_ea_adc_properties(states: Union[DetachedStates, AttachedStates], + kind_data: dict, n_states: int) -> None: + """Collects all properties that are unique for IP/EA-ADC + """ + kind_data["pole_strengths"] = ( + states.pole_strength[:n_states] + ) + + +def dump_excited_states( + states: Union[ExcitedStates,DetachedStates, AttachedStates], + hdf5_file: h5py.Group, dump_nstates: int = None) -> None: + """ + Dump the (charged) excited states data to the given hdf5 file/group. The number of states to dump can be given by dump_nstates. By default all states are dumped. """ @@ -69,8 +113,6 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, dm_bb_a = [] # State diffdm AO basis alpha part dm_bb_b = [] # State diffdm AO basis beta part. - tdm_bb_a = [] # Ground to Excited state tdm AO basis alpha part - tdm_bb_b = [] # Ground to Excited state tdm AO basis beta part # split the eigenvectors according to their excitation degree for all states eigenvectors: dict[int, list] = {} for n in range(n_states): @@ -78,9 +120,6 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, bb_a, bb_b = states.state_diffdm[n].to_ao_basis(states.reference_state) dm_bb_a.append(bb_a.to_ndarray()) dm_bb_b.append(bb_b.to_ndarray()) - bb_a, bb_b = states.transition_dm[n].to_ao_basis(states.reference_state) - tdm_bb_a.append(bb_a.to_ndarray()) - tdm_bb_b.append(bb_b.to_ndarray()) # eigenvectors for exdegree, block in enumerate(states.matrix.axis_blocks): if exdegree + 1 not in eigenvectors: @@ -89,30 +128,19 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, getattr(states.excitation_vector[n], block).to_ndarray() ) kind_data = {} + + if isinstance(states, ExcitedStates): + _pp_adc_properties(states, kind_data, n_states) + elif isinstance(states, (DetachedStates, AttachedStates)): + _ip_ea_adc_properties(states, kind_data, n_states) # eigenvalues kind_data["eigenvalues"] = states.excitation_energy[:n_states] # state and transition dipole moments kind_data["state_dipole_moments"] = states.state_dipole_moment[:n_states] - kind_data["transition_dipole_moments"] = ( - states.transition_dipole_moment[:n_states] - ) - kind_data["transition_dipole_moments_velocity"] = ( - states.transition_dipole_moment_velocity[:n_states] - ) - gauge_origins = ["origin", "mass_center", "charge_center"] - for g_origin in gauge_origins: - kind_data[f"transition_magnetic_dipole_moments_{g_origin}"] = ( - states.transition_magnetic_dipole_moment(g_origin)[:n_states] - ) - kind_data[f"transition_quadrupole_moments_{g_origin}"] = ( - states.transition_quadrupole_moment(g_origin)[:n_states] - ) # state diffdm and ground to excited state tdm kind_data["state_diffdm_bb_a"] = np.asarray(dm_bb_a) kind_data["state_diffdm_bb_b"] = np.asarray(dm_bb_b) - kind_data["ground_to_excited_tdm_bb_a"] = np.asarray(tdm_bb_a) - kind_data["ground_to_excited_tdm_bb_b"] = np.asarray(tdm_bb_b) # dump the eigenvectors kind_data["eigenvectors_singles"] = np.asarray(eigenvectors[1]) if 2 in eigenvectors: diff --git a/adcc/tests/generators/generate_adcc_data.py b/adcc/tests/generators/generate_adcc_data.py index 68fa1adb5..4b03b0af3 100644 --- a/adcc/tests/generators/generate_adcc_data.py +++ b/adcc/tests/generators/generate_adcc_data.py @@ -5,6 +5,7 @@ from adcc.tests import testcases from adcc.AdcMethod import AdcMethod +from adcc.AdcMatrix import AdcMatrixlike from adcc.LazyMp import LazyMp from adcc.workflow import run_adc, validate_state_parameters from adcc import copy as adcc_copy @@ -17,8 +18,11 @@ # the base methods for each adc_type for which to generate data # the different cases (cvs, fc, ...) are handled in the generate functions. +# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0 _methods = { - "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3") + "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"), + "ip": ("ip-adc0", "ip-adc2", "ip-adc2x", "ip-adc3"), + "ea": ("ea-adc0", "ea-adc2", "ea-adc2x", "ea-adc3"), } @@ -26,6 +30,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, gs_density_order: int = None, n_states: int = None, n_singlets: int = None, n_triplets: int = None, n_spin_flip: int = None, + n_doublets: int = None, is_alpha: bool = None, dump_nstates: int = None, **kwargs) -> None: """ Generate and dump the excited states reference data for the given reference case @@ -39,13 +44,20 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, # the kind at this point to check if we need to perform a calculation # for this purpose we need the reference state hf = testdata_cache.refstate(system=test_case, case=case) - _, kind = validate_state_parameters( - hf, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets, - n_spin_flip=n_spin_flip + matrix = AdcMatrixlike() + matrix.reference_state = hf + matrix.method = method + _, kind, is_alpha = validate_state_parameters( + matrix, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets, + n_spin_flip=n_spin_flip, n_doublets=n_doublets, is_alpha=is_alpha ) key = f"{case}/{gs_density_order}" if f"{key}/{kind}" in hdf5_file: return None + if method.adc_type in ("ip", "ea"): + spin = "alpha" if is_alpha else "beta" + if f"{key}/{spin}/{kind}" in hdf5_file: + return None print(f"Generating {method.name} data for {case} {test_case.file_name}.") # prepend cvs to the method if needed (otherwise we will get an error) if "cvs" in case and not method.is_core_valence_separated: @@ -55,7 +67,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, assert gs_density_order is None states = run_adc( hf, method=method, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, **kwargs + n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, + is_alpha=is_alpha, **kwargs ) assert states.kind == kind # maybe we predicted wrong? if f"{key}/matrix" not in hdf5_file: @@ -66,13 +79,19 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, trial_vec = adcc_copy(states.excitation_vector[0]).set_random() dump_matrix_testdata(states.matrix, trial_vec, matrix_group) # dump the excited states data - kind_group = hdf5_file.create_group(f"{key}/{states.kind}") + if method.adc_type == "pp": + kind_group = hdf5_file.create_group(f"{key}/{states.kind}") + elif method.adc_type in ("ip", "ea"): + kind_group = hdf5_file.create_group(f"{key}/{spin}/{states.kind}") + else: + raise ValueError(f"Unknown ADC method: {method.name}") dump_excited_states(states, kind_group, dump_nstates=dump_nstates) def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_states: int = None, n_singlets: int = None, n_triplets: int = None, n_spin_flip: int = None, + n_doublets: int = None, is_alpha: bool = None, dump_nstates: int = None, states_per_case: dict[str, dict[str, int]] = None, **kwargs) -> None: @@ -86,11 +105,14 @@ def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets = states_per_case[case].get("n_singlets", None) n_triplets = states_per_case[case].get("n_triplets", None) n_spin_flip = states_per_case[case].get("n_spin_flip", None) + n_doublets = states_per_case[case].get("n_doublets", None) for density_order in test_case.gs_density_orders: generate_adc( - test_case, method, case, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, - dump_nstates=dump_nstates, gs_density_order=density_order, **kwargs + test_case, method, case, n_states=n_states, + n_singlets=n_singlets, n_triplets=n_triplets, + n_spin_flip=n_spin_flip, n_doublets=n_doublets, + is_alpha=is_alpha, dump_nstates=dump_nstates, + gs_density_order=density_order, **kwargs ) @@ -135,6 +157,29 @@ def generate_h2o_sto3g(): test_case, method=method, dump_nstates=2, states_per_case=per_case, **n_states ) + + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level < 2: + n_states = {n_states: 1} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) def generate_h2o_def2tzvp(): @@ -151,6 +196,26 @@ def generate_h2o_def2tzvp(): generate_adc_all( test_case, method=method, dump_nstates=2, **n_states ) + + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) def generate_cn_sto3g(): @@ -161,11 +226,38 @@ def generate_cn_sto3g(): method = AdcMethod(method) for n_states in \ testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): - kwargs = {n_states: 3} + n_states = {n_states: 3} generate_adc_all( - test_case=test_case, method=method, dump_nstates=2, **kwargs + test_case=test_case, method=method, dump_nstates=2, **n_states ) + for is_alpha in [True, False]: + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level < 2: + n_states = {n_states: 2} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level < 2: + n_states = {n_states: 2} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + def generate_cn_ccpvdz(): # UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt @@ -180,6 +272,27 @@ def generate_cn_ccpvdz(): test_case, method=method, dump_nstates=2, **n_states ) + for is_alpha in [True, False]: + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + def generate_hf_631g(): # UHF, Triplet @@ -194,6 +307,27 @@ def generate_hf_631g(): test_case, method=method, dump_nstates=2, **n_states ) + for is_alpha in [True, False]: + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + def main(): generate_h2o_sto3g() diff --git a/adcc/tests/generators/generate_adcman_data.py b/adcc/tests/generators/generate_adcman_data.py index f5a27d1ab..030762e82 100644 --- a/adcc/tests/generators/generate_adcman_data.py +++ b/adcc/tests/generators/generate_adcman_data.py @@ -12,8 +12,12 @@ # the base methods for each adc_type for which to generate data # the different cases (cvs, fc, ...) are handled in the generate functions. +# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0 +# ip/ea-adc2x not implemented in Q-Chem _methods = { - "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3") + "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"), + "ip": ("ip-adc0", "ip-adc2", "ip-adc3"), + "ea": ("ea-adc0", "ea-adc2", "ea-adc3"), } # Since it seems not possible to only perform an adcman MPn calculation, # the ground state data has to be extracted from an adc(n) calculation. @@ -26,9 +30,10 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, - gs_density_order: int = None, - n_singlets: int = 0, n_triplets: int = 0, - n_spin_flip: int = 0, n_states: int = 0, + gs_density_order: int = None, n_singlets: int = 0, + n_triplets: int = 0, n_spin_flip: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), n_states: int = 0, dump_nstates: int = None, **kwargs) -> None: """ Generate and dump the excited state reference data for the given reference case @@ -39,7 +44,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, datadir = Path(__file__).parent.parent / _testdata_dirname datafile = datadir / test_case.adcdata_file_name("adcman", method.name) hdf5_file = h5py.File(datafile, "a") # Read/write if exists, create otherwise - if f"{case}/{gs_density_order}" in hdf5_file: + key = f"{case}/{gs_density_order}" + if key in hdf5_file: return None # skip cvs-adc(0), since it is not available in qchem. if "cvs" in case and method.level == 0: @@ -55,9 +61,10 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, method = AdcMethod(f"cvs-{method.name}") state_data, _ = run_qchem( test_case, method, case, import_states=True, import_gs=False, - n_singlets=n_singlets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, - n_states=n_states, import_nstates=dump_nstates, - gs_density_order=gs_density_order, **kwargs + n_singlets=n_singlets, n_triplets=n_triplets, n_ip_states=n_ip_states, + n_ea_states=n_ea_states, n_spin_flip=n_spin_flip, n_states=n_states, + import_nstates=dump_nstates, gs_density_order=gs_density_order, + **kwargs ) # the data returned from run_qchem should have already been imported # using the correct keys -> just dump them @@ -68,6 +75,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets: int = 0, n_triplets: int = 0, n_spin_flip: int = 0, n_states: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), dump_nstates: int = None, states_per_case: dict[str, dict[str, int]] = None, **kwargs) -> None: @@ -81,12 +90,16 @@ def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets = states_per_case[case].get("n_singlets", 0) n_triplets = states_per_case[case].get("n_triplets", 0) n_spin_flip = states_per_case[case].get("n_spin_flip", 0) + n_ea_states = states_per_case[case].get("n_ea_states", (0, 0)) + n_ip_states = states_per_case[case].get("n_ip_states", (0, 0)) n_states = states_per_case[case].get("n_states", 0) for density_order in test_case.gs_density_orders: generate_adc( test_case, method, case, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, n_states=n_states, - dump_nstates=dump_nstates, gs_density_order=density_order, + n_triplets=n_triplets, n_spin_flip=n_spin_flip, + n_states=n_states, n_ea_states=n_ea_states, + n_ip_states=n_ip_states, dump_nstates=dump_nstates, + gs_density_order=density_order, **kwargs ) @@ -161,6 +174,25 @@ def generate_h2o_sto3g(): states_per_case=states.get(method.name, None), **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, + states_per_case=states.get(method.name, None), **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + if method.level < 2: + n_states = {"n_ea_states": (1, 0)} + else: + n_states = {"n_ea_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, + states_per_case=states.get(method.name, None), **n_states + ) + def generate_h2o_def2tzvp(): # RHF, Singlet, 43 basis functions: 5 occ, 38 virt. @@ -176,6 +208,20 @@ def generate_h2o_def2tzvp(): test_case, method=method, dump_nstates=2, **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + n_states = {"n_ea_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_cn_sto3g(): # UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt @@ -189,6 +235,26 @@ def generate_cn_sto3g(): test_case, method=method, dump_nstates=2, **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + if method.level < 2: + n_states = {"n_ip_states": (2, 2)} + else: + n_states = {"n_ip_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + if method.level < 2: + n_states = {"n_ea_states": (2, 2)} + else: + n_states = {"n_ea_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_cn_ccpvdz(): # UHF, Doublet @@ -202,6 +268,20 @@ def generate_cn_ccpvdz(): test_case, method=method, dump_nstates=2, **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + n_states = {"n_ea_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_hf_631g(): # UHF, Triplet @@ -215,6 +295,20 @@ def generate_hf_631g(): test_case, method=method, dump_nstates=2, **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + n_states = {"n_ea_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_formaldehyde_pe(): for test_case in testcases.get(n_expected_cases=2, name="formaldehyde"): diff --git a/adcc/tests/generators/import_qchem_data.py b/adcc/tests/generators/import_qchem_data.py index 5170ffbe1..71ca35ae1 100644 --- a/adcc/tests/generators/import_qchem_data.py +++ b/adcc/tests/generators/import_qchem_data.py @@ -54,21 +54,35 @@ def import_excited_states(context: h5py.File, method: AdcMethod, """ # define the possible state kinds to import for each adc variant. state_kinds = { - "pp": [ - ("singlets", True), ("triplets", True), # restricted + "pp": { + True: ["singlets", "triplets"], # restricted # uhf and spin flip are located in the same ".../uhf/..." subtree - ("any_or_spinflip", False), # unrestricted - ] + False: ["any_or_spinflip"], # unrestricted + }, + "ip": { + True: [""], # restricted + False: ["alphas", "betas"], # unrestricted + }, + "ea": { + True: [""], # restricted + False: ["alphas", "betas"], # unrestricted + }, } # Of course the kinds have to have a slightly different name in adcc.... - kind_map = {"singlets": "singlet", "triplets": "triplet"} + kind_map = {"singlets": "singlet", "triplets": "triplet", + "alphas": "alpha", "betas": "beta"} # also the adcc methods have to be translated - method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn - if method_name.endswith("adc2"): # adc2 -> adc2s - method_name += "s" + if method.adc_type == "pp": + method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn + if method_name.endswith("adc2"): # adc2 -> adc2s (Only for PP) + method_name += "s" + else: + method_name: str = method.name.split('-')[-1] # No cvs (yet) + restricted = "rhf" in context[f"/adc_{method.adc_type}"][method_name].keys() + # go through the different possible state kinds and import the states. data = {} - for kind, restricted in state_kinds[method.adc_type]: + for kind in state_kinds[method.adc_type][restricted]: states = _import_excited_states( context, method=method_name, adc_type=method.adc_type, import_nstates=import_nstates, state_kind=kind, restricted=restricted, @@ -88,7 +102,13 @@ def import_excited_states(context: h5py.File, method: AdcMethod, if kind == "any_or_spinflip": kind = "spin_flip" if is_spin_flip else "any" - data[kind_map.get(kind, kind)] = states + if method.adc_type == "pp": + data[kind_map.get(kind, kind)] = states + elif method.adc_type in ("ip", "ea"): + if restricted: + data["beta"] = {"doublet": states} + else: + data[kind_map.get(kind, kind)] = {"any": states} if not data: raise RuntimeError(f"Could not find any states for {method.name} in " f"{context.filename}.") @@ -117,7 +137,8 @@ def _import_excited_states(context: h5py.File, method: str, adc_type: str = "pp" Only import the first n states from the context. state_kind: str, optional The multiplicity of the states, e.g., singlet or triplet for restricted - pp-adc calculations. + pp-adc calculations. In case of an unrestricted IP/EA-ADC calc. it is + "alphas" or "betas" restricted: bool, optional Whether the adc calculation is based on a restricted reference state. dims_pref: str, optional @@ -125,13 +146,18 @@ def _import_excited_states(context: h5py.File, method: str, adc_type: str = "pp" tensors are exported too. The dimensions can be found by adding the given prefix to the context tree of the object. """ - # build the path under which to find the exicted states + # build the path under which to find the excited states tree = [f"adc_{adc_type}", method] if restricted: - assert state_kind is not None # needs to be defined for restricted calcs - tree.extend(["rhf", state_kind]) + tree.append("rhf") + if adc_type == "pp": + assert state_kind is not None # needs to be defined for restricted calcs + tree.append(state_kind) else: tree.append("uhf") + if adc_type in ("ip", "ea"): + assert state_kind is not None + tree.append(state_kind) tree.append("0") # we assume that we only have a single irrep!! tree = "/".join(tree) # check that we have states to read and return if not @@ -144,7 +170,12 @@ def _import_excited_states(context: h5py.File, method: str, adc_type: str = "pp" # context. data_to_read = {} for n in range(n_states): - state_tree = tree + f"/es{n}" + if adc_type == "pp": + state_tree = tree + f"/es{n}" + elif adc_type == "ip": + state_tree = tree + f"/ip{n}" + elif adc_type == "ea": + state_tree = tree + f"/ea{n}" # ensure that the state is converged _, converged = _extract_dataset(context[f"{state_tree}/converged"]) if not converged: @@ -209,10 +240,15 @@ def _import_state_to_state_data(context: h5py.File, method: str, # build the path under which to look for the state-to-state data. tree = [f"adc_{adc_type}", method] if restricted: - assert state_kind is not None # needs to be defined for restricted calcs - tree.extend(["rhf", "isr", state_kind]) + tree.extend(["rhf", "isr"]) + if adc_type == "pp": + assert state_kind is not None # needs to be defined for restricted calcs + tree.append(state_kind) else: tree.extend(["uhf", "isr"]) + if adc_type in ("ip", "ea"): + assert state_kind is not None + tree.append(state_kind) tree.append("0-0") # we assume that we only have a single irrep! tree = "/".join(tree) if tree not in context: @@ -306,6 +342,8 @@ def import_data(context: h5py.File, dims_pref: str = "dims/", "optdm/dm_bb_b": "ground_to_excited_tdm_bb_b", # transition dipole moment (vector): only when we have a optdm "tprop/dipole": "transition_dipole_moments", + # Pole strengths for IP/EA-ADC + "pole_strength": "pole_strengths", # doubles and triples part of the amplitude vector "u2": "eigenvectors_doubles", "u3": "eigenvectors_triples", diff --git a/adcc/tests/generators/run_qchem.py b/adcc/tests/generators/run_qchem.py index db192fd28..6ce72189e 100644 --- a/adcc/tests/generators/run_qchem.py +++ b/adcc/tests/generators/run_qchem.py @@ -27,6 +27,8 @@ def run_qchem(test_case: testcases.TestCase, method: AdcMethod, case: str, run_qchem_scf: bool = False, import_nstates: int = None, n_states: int = 0, n_singlets: int = 0, n_triplets: int = 0, n_spin_flip: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), **kwargs) -> tuple[dict | None, dict | None]: """ Run a qchem calculation for the given test case and method on top @@ -117,6 +119,7 @@ def run_qchem(test_case: testcases.TestCase, method: AdcMethod, case: str, n_core_orbitals=n_core_orbitals, n_frozen_core=n_frozen_core, n_frozen_virtual=n_frozen_virtual, any_states=n_states, singlet_states=n_singlets, triplet_states=n_triplets, + ip_states=n_ip_states, ea_states=n_ea_states, sf_states=n_spin_flip, run_qchem_scf=run_qchem_scf, **args ) # call qchem and wait for completion @@ -324,6 +327,8 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s bohr: bool = True, any_states: int = 0, singlet_states: int = 0, triplet_states: int = 0, sf_states: int = 0, + ip_states: tuple[int, int] = (0,0), + ea_states: tuple[int, int] = (0,0), maxiter: int = 160, conv_tol: int = 10, n_core_orbitals: int = 0, n_frozen_core: int = 0, n_frozen_virtual: int = 0, max_ss: int = None, @@ -363,6 +368,14 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s scf_options.extend(["scf_guess read", "max_scf_cycles 0"]) + # Q-Chem does not take eom_ip_alpha and beta for a restricted calculation + ip_restricted_states = ip_states[0] if ip_states[1] == 0 else 0 + if ip_restricted_states: + ip_states = (0, 0) + ea_restricted_states = ea_states[0] if ea_states[1] == 0 else 0 + if ea_restricted_states: + ea_states = (0, 0) + input = _qchem_template.format( method=method, basis=basis, @@ -371,6 +384,12 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s singlet_states=singlet_states, triplet_states=triplet_states, sf_states=sf_states, + ip_restricted_states=ip_restricted_states, + ea_restricted_states=ea_restricted_states, + ip_states_alpha=ip_states[0], + ip_states_beta=ip_states[1], + ea_states_alpha=ea_states[0], + ea_states_beta=ea_states[1], n_guesses=nguess_singles, bohr=bohr, maxiter=maxiter, @@ -407,6 +426,12 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s ee_singlets {singlet_states} ee_triplets {triplet_states} sf_states {sf_states} +ip_states {ip_restricted_states} +ea_states {ea_restricted_states} +eom_ip_alpha {ip_states_alpha} +eom_ip_beta {ip_states_beta} +eom_ea_alpha {ea_states_alpha} +eom_ea_beta {ea_states_beta} input_bohr {bohr} sym_ignore true adc_davidson_maxiter {maxiter} @@ -463,7 +488,17 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s "cvs-adc1": "cvs-adc(1)", "cvs-adc2": "cvs-adc(2)", "cvs-adc2x": "cvs-adc(2)-x", - "cvs-adc3": "cvs-adc(3)" + "cvs-adc3": "cvs-adc(3)", + "ip-adc0": "adc(0)", + "ip-adc1": "adc(1)", + "ip-adc2": "adc(2)", + "ip-adc2x": "adc(2)-x", + "ip-adc3": "adc(3)", + "ea-adc0": "adc(0)", + "ea-adc1": "adc(1)", + "ea-adc2": "adc(2)", + "ea-adc2x": "adc(2)-x", + "ea-adc3": "adc(3)", } _gs_density_order_dict = { diff --git a/adcc/tests/testcases.py b/adcc/tests/testcases.py index 690e83d75..24dc1e30a 100644 --- a/adcc/tests/testcases.py +++ b/adcc/tests/testcases.py @@ -92,6 +92,8 @@ def filter_cases(self, adc_type: str) -> tuple[str, ...]: # since cvs is not (yet) implemented for IP/EA. if adc_type == "pp": return self.cases + elif adc_type in ("ip", "ea"): + return tuple(case for case in self.cases if "cvs" not in case) raise NotImplementedError(f"Filtering for adc type {adc_type} not " "implemented.") @@ -110,14 +112,26 @@ def validate(self): continue assert component in requirements assert getattr(self, requirements[component], None) is not None - # validate the PP-ADC kinds + # validate the IP/EA/PP-ADC kinds assert len(fields(self.kinds)) == 3 - assert not self.kinds.ip - assert not self.kinds.ea - if self.restricted: - assert all(kind in ["singlet", "triplet"] for kind in self.kinds.pp) - else: - assert all(kind in ["any", "spin_flip"] for kind in self.kinds.pp) + + if self.kinds.pp: + if self.restricted: + assert all(kind in ["singlet", "triplet"] + for kind in self.kinds.pp) + else: + assert all(kind in ["any", "spin_flip"] + for kind in self.kinds.pp) + if self.kinds.ip: + if self.restricted: + assert all(kind in ["doublet"] for kind in self.kinds.ip) + else: + assert all(kind in ["any"] for kind in self.kinds.ip) + if self.kinds.ea: + if self.restricted: + assert all(kind in ["doublet"] for kind in self.kinds.ea) + else: + assert all(kind in ["any"] for kind in self.kinds.ea) def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: @@ -125,9 +139,9 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: Transforms the given kinds to a list of keywords to request states of the corresponding kind in an adc calculation. """ - # singlet, triplet -> n_singlets, n_triplets - # any -> n_states - # spin_flip -> n_spin_flip + # singlet, doublet, triplet -> n_singlets, n_triplets + # any -> n_states + # spin_flip -> n_spin_flip ret = [] for kind in kinds: if kind == "any": @@ -197,8 +211,10 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: def _init_test_cases() -> tuple[TestCase, ...]: test_cases: list[TestCase] = [] # some shared data - restricted_kinds = Kinds(pp=("singlet", "triplet")) - unrestricted_kinds = Kinds(pp=("any",)) + restricted_kinds = Kinds(pp=("singlet", "triplet"), + ip=("doublet",), + ea=("doublet",)) + unrestricted_kinds = Kinds(pp=("any",), ip=("any",), ea=("any",)) spin_flip_kinds = Kinds(pp=("spin_flip",)) # CH2NH2 ref_cases = ("gen", "cvs") diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py index 41c77d1b4..f949139cd 100644 --- a/adcc/tests/testdata_cache.py +++ b/adcc/tests/testdata_cache.py @@ -1,12 +1,15 @@ from . import testcases from adcc.AdcMatrix import AdcMatrix +from adcc.AdcMethod import AdcMethod from adcc.ExcitedStates import ExcitedStates +from adcc.ChargedExcitations import AttachedStates, DetachedStates from adcc.LazyMp import LazyMp from adcc.misc import cached_member_function from adcc.ReferenceState import ReferenceState from adcc.solver import EigenSolverStateBase -from adcc import hdf5io, guess_zero +from adcc import hdf5io +from adcc.guess import guess_zero, determine_spin_change from pathlib import Path import numpy as np @@ -114,11 +117,12 @@ def hfimport(self, system: str, case: str) -> dict: @cached_member_function() def _load_data(self, system: str, method: str, case: str, source: str, - gs_density_order: int = None) -> dict: + gs_density_order: int = None, + is_alpha: bool = None) -> dict: """ Load the reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and if it is an alpha process for IP/EA. Source defines the source which generated the reference data, i.e., either adcman or adcc. """ @@ -128,7 +132,7 @@ def _load_data(self, system: str, method: str, case: str, source: str, system = testcases.get_by_filename(system).pop() return self._load_data( system, method=method, case=case, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) assert isinstance(system, testcases.TestCase) assert case in system.cases @@ -142,52 +146,69 @@ def _load_data(self, system: str, method: str, case: str, source: str, else: # adc data is one level deeper than mpdata: gs_density_order datafile = datadir / system.adcdata_file_name(source, method) key = f"{case}/{gs_density_order}" + if AdcMethod(method).adc_type in ("ip", "ea"): + assert isinstance(is_alpha, bool) + spin = "alpha" if is_alpha else "beta" + key = f"{case}/{gs_density_order}/{spin}" if not datafile.exists(): raise FileNotFoundError(f"Missing reference data file {datafile}.") with h5py.File(datafile, "r") as hdf5_file: if key not in hdf5_file: - raise ValueError( - f"No data available for case {case} and gs_density_order " - f"{gs_density_order} in file {datafile}." - ) + if is_alpha is None: + raise ValueError( + f"No data available for case {case} and " + f"gs_density_order {gs_density_order} in file {datafile}." + ) + elif (source == "adcman" and + f"{case}/{gs_density_order}/beta" in hdf5_file): + # Little hack to load betas in case of a restricted adcman + # calculation because the default spin type is different + key = f"{case}/{gs_density_order}/beta" + else: + raise ValueError( + f"No data available for case {case}, gs_density_order " + f"{gs_density_order} and spin {spin} in file {datafile}." + ) data = hdf5io.extract_group(hdf5_file[key]) return data def adcc_data(self, system: str, method: str, case: str, - gs_density_order: int = None) -> dict: + gs_density_order: int = None, is_alpha: bool = None) -> dict: """ Load the adcc reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ return self._load_data( system=system, method=method, case=case, - gs_density_order=gs_density_order, source="adcc" + gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha ) def adcman_data(self, system: str, method: str, case: str, - gs_density_order: int = None) -> dict: + gs_density_order: int = None, is_alpha: bool = None) -> dict: """ Load the adcman reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ return self._load_data( system=system, method=method, case=case, - gs_density_order=gs_density_order, source="adcman" + gs_density_order=gs_density_order, source="adcman", + is_alpha=is_alpha ) @cached_member_function() - def _make_mock_adc_state(self, system: str, method: str, case: str, - kind: str, source: str, - gs_density_order: int = None) -> ExcitedStates: + def _make_mock_adc_state( + self, system: str, method: str, case: str, kind: str, source: str, + gs_density_order: int = None, is_alpha: bool = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+). - Source refers to the source with which the data were generated - (adcman/adcc). - The excited states object is build on top of the loaded HF data and + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) and optionally is_alpha for IP/EA. Source refers to the + source with which the data were generated (adcman/adcc). + The states object is build on top of the loaded HF data and contains the eigenstates and eigenvalues of the loaded ADC data. """ if isinstance(system, str): @@ -197,7 +218,7 @@ def _make_mock_adc_state(self, system: str, method: str, case: str, system = testcases.get_by_filename(system).pop() return self._make_mock_adc_state( system, method=method, case=case, kind=kind, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) assert isinstance(system, testcases.TestCase) assert case in system.cases @@ -205,7 +226,7 @@ def _make_mock_adc_state(self, system: str, method: str, case: str, # load the adc data data = self._load_data( system, method=method, case=case, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) adc_data = data.get(kind, None) if adc_data is None: @@ -233,12 +254,19 @@ def _make_mock_adc_state(self, system: str, method: str, case: str, elif refstate.restricted and kind == "triplet": symm = "antisymmetric" spin_change = 0 + elif refstate.restricted and kind == "doublet": + symm = "none" elif kind in ["spin_flip", "any"]: symm = "none" - spin_change = 0 if kind == "any" else -1 else: raise ValueError(f"Unknown kind: {kind}") + if source == "adcman" and refstate.restricted: + # Little hack to build beta states for a restricted case + is_alpha = False + # matrix.reference_state.restricted = False + spin_change = determine_spin_change(matrix.method, kind, is_alpha) + n_states = len(adc_data["eigenvalues"]) states.eigenvectors = [guess_zero(matrix, spin_change=spin_change, spin_block_symmetrisation=symm) @@ -251,32 +279,49 @@ def _make_mock_adc_state(self, system: str, method: str, case: str, evec[blocks[1]].set_from_ndarray( adc_data["eigenvectors_doubles"][i], 1e-14 ) - return ExcitedStates(states) + + # if source == "adcman" and refstate.restricted: + # # Revert changes above for restricted adcman references + # matrix.reference_state.restricted = True + + if matrix.method.adc_type == "pp": + return ExcitedStates(states) + elif matrix.method.adc_type == "ip": + return DetachedStates(states, is_alpha) + elif matrix.method.adc_type == "ea": + return AttachedStates(states, is_alpha) + else: + raise ValueError(f"Unknown ADC method: {method.name}") def adcc_states(self, system: str, method: str, kind: str, - case: str, gs_density_order: int = None) -> ExcitedStates: + case: str, gs_density_order: int = None, + is_alpha: bool = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+) using the adcc eigenstates - and eigenvalues. + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) using the adcc eigenstates and eigenvalues. """ return self._make_mock_adc_state( system, method=method, case=case, kind=kind, - gs_density_order=gs_density_order, source="adcc" + gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha ) def adcman_states(self, system: str, method: str, kind: str, - case: str, gs_density_order: int = None) -> ExcitedStates: + case: str, gs_density_order: int = None, + is_alpha: bool = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+) using the adcman eigenstates - and eigenvalues. + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) using the adcman eigenstates and eigenvalues. """ return self._make_mock_adc_state( system, method=method, case=case, kind=kind, - gs_density_order=gs_density_order, source="adcman" + gs_density_order=gs_density_order, source="adcman", + is_alpha=is_alpha ) @@ -291,7 +336,7 @@ def read_json_data(name: str) -> dict: return json.load(open(jsonfile, "r"), object_hook=_import_hook) -def _import_hook(data: dict): +def _import_hook(data: dict) -> dict: return {key: np.array(val) if isinstance(val, list) else val for key, val in data.items()} From ecbb175495b1e839a43c9f38fe1b8a4c79b27da4 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 10:31:19 +0100 Subject: [PATCH 14/28] adapted workflow tests --- adcc/tests/workflow_test.py | 476 +++++++++++++++++++++++++++++++++--- 1 file changed, 442 insertions(+), 34 deletions(-) diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py index 15dcd8693..f2229ba77 100644 --- a/adcc/tests/workflow_test.py +++ b/adcc/tests/workflow_test.py @@ -29,18 +29,26 @@ class TestWorkflow: - def test_validate_state_parameters_rhf(self): + def test_validate_state_parameters_rhf_pp(self): from adcc.workflow import validate_state_parameters - - refstate = testdata_cache.refstate("h2o_sto3g", case="gen") - - assert 3, "any" == validate_state_parameters(refstate, n_states=3) - assert 4, "singlet" == validate_state_parameters(refstate, n_states=4, - kind="singlet") - assert 2, "triplet" == validate_state_parameters(refstate, n_states=2, - kind="triplet") - assert 2, "triplet" == validate_state_parameters(refstate, n_triplets=2) - assert 6, "singlet" == validate_state_parameters(refstate, n_singlets=6) + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + matrix.method = AdcMethod("adc2") + + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3) + assert (4, "singlet", None) == validate_state_parameters( + matrix, n_states=4, kind="singlet") + assert (2, "triplet", None) == validate_state_parameters( + matrix, n_states=2, kind="triplet") + assert (2, "triplet", None) == validate_state_parameters( + matrix, n_triplets=2) + assert (6, "singlet", None) == validate_state_parameters( + matrix, n_singlets=6) invalid_cases = [ dict(), # No states requested @@ -52,21 +60,30 @@ def test_validate_state_parameters_rhf(self): dict(n_states=2, n_spin_flip=2), # States of two sorts dict(n_triplets=2, kind="singlet"), # kind and n_ do not agree dict(n_states=2, kind="bla"), # Kind invaled + dict(n_states=2, kind="doublet"), # Kind invalid for PP-ADC + dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC + dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC ] for case in invalid_cases: with pytest.raises(InputError): - validate_state_parameters(refstate, **case) + validate_state_parameters(matrix, **case) - def test_validate_state_parameters_uhf(self): + def test_validate_state_parameters_uhf_pp(self): from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod - refstate = testdata_cache.refstate("cn_sto3g", case="gen") + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("adc2") - assert 3, "any" == validate_state_parameters(refstate, n_states=3, - kind="any") - assert 3, "any" == validate_state_parameters(refstate, n_states=3) - assert 2, "spin_flip" == validate_state_parameters(refstate, - n_spin_flip=2) + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3) + assert (2, "spin_flip", None) == validate_state_parameters( + matrix, n_spin_flip=2) invalid_cases = [ dict(), # No states requested @@ -77,15 +94,170 @@ def test_validate_state_parameters_uhf(self): dict(n_triplets=2, n_singlets=2), # States of two sorts dict(n_states=2, n_spin_flip=2), # States of two sorts dict(n_spin_flip=2, kind="singlet"), # kind and n_ do not agree - dict(n_states=2, kind="bla"), # Kind invaled + dict(n_states=2, kind="bla"), # Kind invalid dict(n_states=4, kind="singlet"), # UHF with singlets dict(n_states=2, kind="triplet"), # UHF with triplets dict(n_triplets=2), # UHF with triplets dict(n_singlets=6), # UHF with singlets + dict(n_doublets=3), # UHF with doublets (only restricted IP/EA) + dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC + dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC + ] + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_rhf_ip(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + matrix.method = AdcMethod("ip-adc2") + + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=False)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=True)) # restricted always beta + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_states=2, kind="doublet")) + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=2)) + assert (6, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=6, is_alpha=True)) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_doublets=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_singlets=2), # Kind invalid for IP/EA-ADC + dict(n_triplets=2), # Kind invalid for IP/EA-ADC + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + dict(n_states=2, n_spin_flip=2), # States of two sorts + dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree + ] + + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_uhf_ip(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("ip-adc2") + + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", False) == validate_state_parameters( + matrix, n_states=3, is_alpha=False) + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, is_alpha=True) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_states=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_doublets=2), # UHF with doublets + dict(n_states=2, kind="doublet"), # UHF with doublets + dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + ] + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_rhf_ea(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + + # IP + matrix.method = AdcMethod("ea-adc2") + + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=False)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=True)) # restricted always beta + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_states=2, kind="doublet")) + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=2)) + assert (6, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=6, is_alpha=True)) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_doublets=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_singlets=2), # Kind invalid for IP/EA-ADC + dict(n_triplets=2), # Kind invalid for IP/EA-ADC + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + dict(n_states=2, n_spin_flip=2), # States of two sorts + dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree + ] + + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_uhf_ea(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("ea-adc2") + + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", False) == validate_state_parameters( + matrix, n_states=3, is_alpha=False) + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, is_alpha=True) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_states=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_doublets=2), # UHF with doublets + dict(n_states=2, kind="doublet"), # UHF with doublets + dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean ] for case in invalid_cases: with pytest.raises(InputError): - validate_state_parameters(refstate, **case) + validate_state_parameters(matrix, **case) def test_construct_adcmatrix(self): from adcc.workflow import construct_adcmatrix @@ -98,6 +270,8 @@ def test_construct_adcmatrix(self): res = construct_adcmatrix(hfdata, method="adc3") assert isinstance(res, adcc.AdcMatrix) assert res.method == adcc.AdcMethod("adc3") + assert res.method.adc_type == "pp" + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [] @@ -105,26 +279,52 @@ def test_construct_adcmatrix(self): res = construct_adcmatrix(hfdata, method="cvs-adc3", core_orbitals=1) assert isinstance(res, adcc.AdcMatrix) assert res.method == adcc.AdcMethod("cvs-adc3") + assert res.method.adc_type == "pp" + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [0, 7] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [] res = construct_adcmatrix(hfdata, method="adc2", frozen_core=1) + assert res.method.adc_type == "pp" + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [0, 7] assert res.mospaces.frozen_virtual == [] res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1) + assert res.method.adc_type == "pp" + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [6, 13] res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1, frozen_core=1) + assert res.method.adc_type == "pp" + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [0, 7] assert res.mospaces.frozen_virtual == [6, 13] + res = construct_adcmatrix(hfdata, method="ip-adc3") + assert isinstance(res, adcc.AdcMatrix) + assert res.method == adcc.AdcMethod("ip-adc3") + assert res.method.adc_type == "ip" + assert res.axis_blocks == ["h", "phh"] + assert res.mospaces.core_orbitals == [] + assert res.mospaces.frozen_core == [] + assert res.mospaces.frozen_virtual == [] + + res = construct_adcmatrix(hfdata, method="ea-adc2") + assert isinstance(res, adcc.AdcMatrix) + assert res.method == adcc.AdcMethod("ea-adc2") + assert res.method.adc_type == "ea" + assert res.axis_blocks == ["p", "pph"] + assert res.mospaces.core_orbitals == [] + assert res.mospaces.frozen_core == [] + assert res.mospaces.frozen_virtual == [] + invalid_cases = [ dict(), # Missing method dict(method="dadadad"), # Unknown method @@ -195,7 +395,7 @@ def test_construct_adcmatrix(self): match=r"^Ignored frozen_virtual parameter"): construct_adcmatrix(mtx_cvs, frozen_virtual=1) - def test_diagonalise_adcmatrix(self): + def test_diagonalise_adcmatrix_pp(self): from adcc.workflow import diagonalise_adcmatrix system = "h2o_sto3g" @@ -230,21 +430,81 @@ def test_diagonalise_adcmatrix(self): eigensolver="davidson", guesses=guesses) - def test_estimate_n_guesses(self): - from adcc.workflow import estimate_n_guesses + def test_diagonalise_adcmatrix_ip(self): + from adcc.workflow import diagonalise_adcmatrix + pytest.skip("adcman referencedata not yet available") + system = "h2o_sto3g" + case = "gen" + method = "ip-adc2" + kind = "doublet" - refstate = testdata_cache.refstate("h2o_sto3g", case="gen") - ground_state = adcc.LazyMp(refstate) - matrix = adcc.AdcMatrix("adc2", ground_state) + refdata = testdata_cache.adcman_data(system, method=method, case=case, + is_alpha=False) + ref_doublets = refdata[kind]["eigenvalues"] + n_states = min(len(ref_doublets), 3) + + matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) + + guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="h", + is_alpha=False) + res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, + guesses=guesses, is_alpha=False) + assert res.converged + assert res.eigenvalues[:n_states] == approx(ref_doublets[:n_states]) + + with pytest.raises(InputError): # Too low tolerance + # SCF tolerance = 1e-14 currently + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses,eigensolver="davidson", + conv_tol=1e-15) + + with pytest.raises(InputError): # Wrong solver method + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="blubber") + + with pytest.raises(ValueError): # Too few guesses + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + eigensolver="davidson", + guesses=guesses) - # Check minimal number of guesses is 4 and at some point - # we get more than four guesses - assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) - assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) - for i in range(3, 20): - assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + def test_diagonalise_adcmatrix_ea(self): + from adcc.workflow import diagonalise_adcmatrix + pytest.skip("adcman referencedata not yet available") + system = "h2o_sto3g" + case = "gen" + method = "ea-adc2" + kind = "doublet" - def test_obtain_guesses_by_inspection(self): + refdata = testdata_cache.adcman_data(system, method=method, case=case, + is_alpha=True) + ref_doublets = refdata[kind]["eigenvalues"] + n_states = min(len(ref_doublets), 3) + + matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) + + guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="p", + is_alpha=True) + res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, + guesses=guesses, is_alpha=True) + assert res.converged + assert res.eigenvalues[:n_states] == approx(ref_doublets[:n_states]) + + with pytest.raises(InputError): # Too low tolerance + # SCF tolerance = 1e-14 currently + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses,eigensolver="davidson", + conv_tol=1e-15) + + with pytest.raises(InputError): # Wrong solver method + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="blubber") + + with pytest.raises(ValueError): # Too few guesses + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + eigensolver="davidson", + guesses=guesses) + + def test_obtain_guesses_by_inspection_pp(self): from adcc.workflow import obtain_guesses_by_inspection refstate = testdata_cache.refstate("h2o_sto3g", case="gen") @@ -264,8 +524,156 @@ def test_obtain_guesses_by_inspection(self): matrix2, n_guesses=i, kind="triplet", n_guesses_doubles=2) assert len(res) == i + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, kind="singlet") + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="singlet", + n_guesses_doubles=4) + assert len(res) == 4 + with pytest.raises(InputError): obtain_guesses_by_inspection(matrix1, n_guesses=4, kind="any", n_guesses_doubles=2) with pytest.raises(InputError): obtain_guesses_by_inspection(matrix1, n_guesses=40, kind="any") + + def test_obtain_guesses_by_inspection_ip(self): + from adcc.workflow import obtain_guesses_by_inspection + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix2 = adcc.AdcMatrix("ip-adc2", ground_state) + matrix1 = adcc.AdcMatrix("ip-adc1", ground_state) + + # Test that the right number of guesses is returned + for i in range(4, 9): + res = obtain_guesses_by_inspection(matrix2, n_guesses=i, + kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == i + + for i in range(2, 5): + res = obtain_guesses_by_inspection( + matrix1, n_guesses=i, kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == i + + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, + kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="doublet", + spin_change=-0.5, is_alpha=False, + n_guesses_doubles=4) + assert len(res) == 4 + + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=6, kind="any", + spin_change=-0.5, is_alpha=True) + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=2, kind="any", + n_guesses_doubles=2, spin_change=-0.5, + is_alpha=True) + + def test_obtain_guesses_by_inspection_ea(self): + from adcc.workflow import obtain_guesses_by_inspection + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix2 = adcc.AdcMatrix("ea-adc2", ground_state) + matrix1 = adcc.AdcMatrix("ea-adc1", ground_state) + + # Test that the right number of guesses is returned + for i in range(4, 9): + res = obtain_guesses_by_inspection(matrix2, n_guesses=i, + kind="doublet", + spin_change=0.5, + is_alpha=True) + assert len(res) == i + + for i in range(1, 2): + res = obtain_guesses_by_inspection( + matrix1, n_guesses=i, kind="doublet", + spin_change=0.5, is_alpha=True) + assert len(res) == i + + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, + kind="doublet", + spin_change=0.5, is_alpha=True) + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="doublet", + spin_change=0.5, is_alpha=True, + n_guesses_doubles=4) + assert len(res) == 4 + + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=6, kind="any", + spin_change=0.5, is_alpha=True) + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=2, kind="any", + n_guesses_doubles=2, spin_change=0.5, + is_alpha=True) + + def test_construct_guesses_explicit(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=3, + kind="singlet", + spin_change=0, + n_guesses=5 + ) + + assert len(res) == 5 + + def test_construct_guesses_davidson(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=2, + kind="singlet", + spin_change=0, + n_guesses=None, + eigensolver="davidson" + ) + + assert len(res) == 4 + + def test_construct_guesses_lanczos(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=2, + kind="singlet", + spin_change=0, + n_guesses=None, + eigensolver="lanczos" + ) + + assert len(res) == 2 \ No newline at end of file From 2a2da191ca59bdddad03ddc22612572fb3cd8c37 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 13:57:03 +0100 Subject: [PATCH 15/28] property and state_density tests for IP/EA-ADC(0-3) (adcman and adcc) --- adcc/tests/properties_test.py | 145 ++++++++++++++++++++++---- adcc/tests/state_densities_test.py | 157 +++++++++++++++++++++++++---- 2 files changed, 259 insertions(+), 43 deletions(-) diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py index f63498c7c..50b9fb871 100644 --- a/adcc/tests/properties_test.py +++ b/adcc/tests/properties_test.py @@ -39,24 +39,33 @@ # -> independent of method, case (gen/cvs/fc/fv) and kind (singlet/triplet) # Actually, the tests should also be independent of the systems, because # we only load some already tested density and contract it with some operator. -methods = ["adc2"] +pp_methods = ["adc2"] +ip_ea_methods = ["ip-adc2", "ea-adc2"] generators = ["adcman", "adcc"] test_cases = testcases.get_by_filename( "h2o_sto3g", "h2o_def2tzvp", "cn_sto3g", "cn_ccpvdz", "hf_631g" ) -cases = [(case.file_name, "gen", kind) +cases_pp = [(case.file_name, m, "gen", kind) for case in test_cases + for m in pp_methods for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp] +cases_ip_ea = [(case.file_name, m, c, kind, is_alpha) + for case in test_cases + for m in ip_ea_methods + for c in ["gen"] if c in case.cases + for kind in getattr(case.kinds, AdcMethod(m).adc_type) + for is_alpha in ([True] if case.restricted else [True, False]) +] + gauge_origins = ["origin", "mass_center", "charge_center"] -@pytest.mark.parametrize("method", methods) class TestProperties: @pytest.mark.parametrize("generator", generators) - @pytest.mark.parametrize("system,case,kind", cases) - def test_transition_dipole_moments(self, system: str, case: str, kind: str, - method: str, generator: str): + @pytest.mark.parametrize("system,method,case,kind", cases_pp) + def test_transition_dipole_moments(self, system: str, method: str, + case: str, kind: str, generator: str): if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") @@ -87,9 +96,9 @@ def test_transition_dipole_moments(self, system: str, case: str, kind: str, assert_allclose_signfix(res_tdm, ref_tdm, atol=1e-5) @pytest.mark.parametrize("generator", generators) - @pytest.mark.parametrize("system,case,kind", cases) - def test_oscillator_strengths(self, system: str, case: str, kind: str, - method: str, generator: str): + @pytest.mark.parametrize("system,method,case,kind", cases_pp) + def test_oscillator_strengths(self, system: str, method: str, case: str, + kind: str, generator: str): if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") @@ -116,11 +125,41 @@ def test_oscillator_strengths(self, system: str, case: str, kind: str, assert ( res_oscs[i] == pytest.approx(2. / 3. * ref_tdm_norm * refevals[i]) ) + + @pytest.mark.parametrize("generator", generators) + @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea) + def test_pole_strengths(self, system: str, method: str, case: str, + kind: str, is_alpha: bool, generator: str): + if generator == "adcman": + if system.startswith("h2o"): + pytest.skip("Difficulties with data import for restricted cases") + if method.endswith("adc1"): + pytest.skip("No adcman reference data for IP/EA-ADC(1)") + elif method.endswith("adc2x"): + pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") + refdata = testdata_cache._load_data( + system=system, method=method, case=case, source=generator, + is_alpha=is_alpha + )[kind] + state = testdata_cache._make_mock_adc_state( + system=system, method=method, case=case, kind=kind, + source=generator, is_alpha=is_alpha + ) + + res_pols = state.pole_strength + print(refdata.keys()) + refevals = refdata["eigenvalues"] + ref_pols = refdata["pole_strengths"] + + n_ref = len(state.excitation_vector) + for i in range(n_ref): + assert state.excitation_energy[i] == refevals[i] + assert res_pols[i] == pytest.approx(ref_pols[i]) @pytest.mark.parametrize("generator", generators) - @pytest.mark.parametrize("system,case,kind", cases) - def test_state_dipole_moments(self, system: str, case: str, kind: str, - method: str, generator: str): + @pytest.mark.parametrize("system,method,case,kind", cases_pp) + def test_state_dipole_moments_pp(self, system: str, method: str, case: str, + kind: str, generator: str): if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") @@ -134,14 +173,41 @@ def test_state_dipole_moments(self, system: str, case: str, kind: str, res_dms = state.state_dipole_moment n_ref = len(state.excitation_vector) assert_allclose(res_dms, refdata["state_dipole_moments"][:n_ref], atol=1e-4) + + @pytest.mark.parametrize("generator", generators) + @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea) + def test_state_dipole_moments_ip_ea(self, system: str, method: str, + case: str, kind: str, is_alpha: bool, + generator: str): + if generator == "adcman": + if system.startswith("h2o"): + pytest.skip("Difficulties with data import for restricted cases") + if method.endswith("adc1"): + pytest.skip("No adcman reference data for IP/EA-ADC(1)") + elif method.endswith("adc2x"): + pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") + if system.startswith("cn"): + pytest.xfail("Faulty adcman GS dipole moments") + refdata = testdata_cache._load_data( + system=system, method=method, case=case, source=generator, + is_alpha=is_alpha + )[kind] + state = testdata_cache._make_mock_adc_state( + system=system, method=method, case=case, kind=kind, + source=generator, is_alpha=is_alpha + ) + + res_dms = state.state_dipole_moment + n_ref = len(state.excitation_vector) + assert_allclose(res_dms, refdata["state_dipole_moments"][:n_ref], atol=1e-4) # CVS-ADC state2state tdm not implemented @pytest.mark.parametrize("generator", generators) - @pytest.mark.parametrize("system,case,kind", [c for c in cases - if "cvs" not in c[1]]) - def test_state2state_transition_dipole_moments(self, system: str, case: str, - kind: str, method: str, - generator: str): + @pytest.mark.parametrize("system,method,case,kind", [c for c in cases_pp + if "cvs" not in c[2]]) + def test_state2state_transition_dipole_moments_pp( + self, system: str, case: str, kind: str, method: str, generator: str): + refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator )[kind] @@ -164,6 +230,43 @@ def test_state2state_transition_dipole_moments(self, system: str, case: str, assert_allclose_signfix(state2state.transition_dipole_moment[ii], fromi_ref[ii], atol=1e-4) + @pytest.mark.parametrize("generator", generators) + @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea) + def test_state2state_transition_dipole_moments_ip_ea( + self, system: str, method: str, case: str, kind: str, is_alpha: bool, + generator: str): + if generator == "adcman": + if system.startswith("h2o"): + pytest.skip("Difficulties with data import for restricted cases") + if method.endswith("adc1"): + pytest.skip("No adcman reference data for IP/EA-ADC(1)") + elif method.endswith("adc2x"): + pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") + refdata = testdata_cache._load_data( + system=system, method=method, case=case, source=generator, + is_alpha=is_alpha + )[kind] + state = testdata_cache._make_mock_adc_state( + system=system, method=method, case=case, kind=kind, + source=generator, is_alpha=is_alpha + ) + + refevals = refdata["eigenvalues"] + if len(refevals) < 2: + pytest.skip("Less than two states available.") + + state_to_state = refdata["state_to_state"] + for i in range(len(state.excitation_vector) - 1): + assert state.excitation_energy[i] == refevals[i] + fromi_ref = state_to_state[f"from_{i}"]["transition_dipole_moments"] + + state2state = State2States(state, initial=i) + for ii, j in enumerate(range(i + 1, state.size)): + assert state.excitation_energy[j] == refevals[j] + assert_allclose_signfix(state2state.transition_dipole_moment[ii], + fromi_ref[ii], atol=1e-4) + + @pytest.mark.parametrize("method", pp_methods) @pytest.mark.parametrize("case", ["gen", "cvs"]) def test_magnetic_transition_dipole_moments_z_component(self, method: str, case: str): @@ -188,7 +291,7 @@ def test_magnetic_transition_dipole_moments_z_component(self, method: str, assert tdm[2] < 1e-10 # Only adcc reference data available. - @pytest.mark.parametrize("system,case,kind", cases) + @pytest.mark.parametrize("system,method,case,kind", cases_pp) def test_magnetic_transition_dipole_moments(self, system: str, case: str, kind: str, method: str): refdata = testdata_cache._load_data( @@ -209,7 +312,7 @@ def test_magnetic_transition_dipole_moments(self, system: str, case: str, ) # Only adcc reference data available. - @pytest.mark.parametrize("system,case,kind", cases) + @pytest.mark.parametrize("system,method,case,kind", cases_pp) def test_transition_dipole_moments_velocity(self, system: str, case: str, kind: str, method: str): refdata = testdata_cache._load_data( @@ -228,7 +331,7 @@ def test_transition_dipole_moments_velocity(self, system: str, case: str, ) # Only adcc reference data available. - @pytest.mark.parametrize("system,case,kind", cases) + @pytest.mark.parametrize("system,method,case,kind", cases_pp) def test_transition_quadrupole_moments(self, system: str, case: str, kind: str, method: str): refdata = testdata_cache._load_data( @@ -249,7 +352,7 @@ def test_transition_quadrupole_moments(self, system: str, case: str, ) # Only adcc reference data available. - @pytest.mark.parametrize("system,case,kind", cases) + @pytest.mark.parametrize("system,method,case,kind", cases_pp) def test_rotatory_strengths(self, system: str, case: str, kind: str, method: str): refdata = testdata_cache._load_data( diff --git a/adcc/tests/state_densities_test.py b/adcc/tests/state_densities_test.py index b2e8954d5..e61759859 100644 --- a/adcc/tests/state_densities_test.py +++ b/adcc/tests/state_densities_test.py @@ -25,13 +25,15 @@ from pytest import approx from adcc import ExcitedStates, AdcMethod +from adcc.ChargedExcitations import DetachedStates, AttachedStates from adcc.State2States import State2States from .testdata_cache import testdata_cache from . import testcases -methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] +pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] +ip_ea_methods = [t + m for t in ["ip-", "ea-"] for m in pp_methods] generators = ["adcman", "adcc"] @@ -42,28 +44,81 @@ test_cases = testcases.get_by_filename( "h2o_sto3g", "h2o_def2tzvp", "cn_sto3g", "cn_ccpvdz", "hf_631g" ) -cases = [(case.file_name, c, kind) - for case in test_cases - for c in ["gen", "cvs"] if c in case.cases - for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp] +cases_pp = [(case.file_name, m, c, kind) + for case in test_cases + for m in pp_methods + for c in ["gen", "cvs"] if c in case.cases + for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp +] + +cases_ip_ea = [(case.file_name, m, c, kind, is_alpha) + for case in test_cases + for m in ip_ea_methods + for c in ["gen"] if c in case.cases + for kind in getattr(case.kinds, AdcMethod(m).adc_type) + for is_alpha in ([True] if case.restricted else [True, False]) +] -@pytest.mark.parametrize("method", methods) @pytest.mark.parametrize("generator", generators) class TestStateDensities: - @pytest.mark.parametrize("system,case,kind", cases) - def test_state_diffdm(self, system: str, case: str, kind: str, method: str, - generator: str): + @pytest.mark.parametrize("system,method,case,kind", cases_pp) + def test_state_diffdm_pp(self, system: str, method: str, case: str, + kind: str, generator: str): if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator )[kind] - # construct a ExcitedStates instance using the eigenvalues and eigenstates - # from the reference data. - state: ExcitedStates = getattr(testdata_cache, f"{generator}_states")( + # construct an ExcitedStates instance + # using the eigenvalues and eigenstates from the reference data. + state: ExcitedStates = getattr( + testdata_cache, f"{generator}_states")( system=system, method=method, case=case, kind=kind - ) + ) + + # since refdata was used to build state we have to have the same amount + # of states + for i in range(len(state.excitation_vector)): + # Check that we are talking about the same state when + # comparing reference and computed + assert state.excitation_energy[i] == refdata["eigenvalues"][i] + + dm_ao_a, dm_ao_b = state.state_diffdm[i].to_ao_basis() + assert dm_ao_a.to_ndarray() == approx(refdata["state_diffdm_bb_a"][i]) + assert dm_ao_b.to_ndarray() == approx(refdata["state_diffdm_bb_b"][i]) + + @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea) + def test_state_diffdm_ip_ea(self, system: str, method: str, case: str, + kind: str, is_alpha: bool, generator: str): + if generator == "adcman": + if system.startswith("h2o"): + pytest.skip("Difficulties with data import for restricted cases") + if method.endswith("adc1"): + pytest.skip("No adcman reference data for IP/EA-ADC(1)") + elif method.endswith("adc2x"): + pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") + refdata = testdata_cache._load_data( + system=system, method=method, case=case, source=generator, + is_alpha=is_alpha + )[kind] + # construct an DetachedStates/AttachedStates instance + # using the eigenvalues and eigenstates from the reference data. + if AdcMethod(method).adc_type == "ip": + state: DetachedStates = getattr( + testdata_cache, f"{generator}_states")( + system=system, method=method, case=case, kind=kind, + is_alpha=is_alpha + ) + elif AdcMethod(method).adc_type == "ea": + state: AttachedStates = getattr( + testdata_cache, f"{generator}_states")( + system=system, method=method, case=case, kind=kind, + is_alpha=is_alpha + ) + else: + raise ValueError(f"Unknown ADC method: {method.name}") + # since refdata was used to build state we have to have the same amount # of states for i in range(len(state.excitation_vector)): @@ -78,10 +133,10 @@ def test_state_diffdm(self, system: str, case: str, kind: str, method: str, # adcman does not compute the tdm for singlet -> triplet transitions, # because the transition dipole moment should be zero anyway # -> remove triplet tests - @pytest.mark.parametrize("system,case,kind", - [c for c in cases if c[2] != "triplet"]) - def test_ground_to_excited_tdm(self, system: str, case: str, kind: str, - method: str, generator: str): + @pytest.mark.parametrize("system,method,case,kind", + [c for c in cases_pp if c[3] != "triplet"]) + def test_ground_to_excited_tdm(self, system: str, method: str, case: str, + kind: str, generator: str): if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") refdata = testdata_cache._load_data( @@ -107,11 +162,10 @@ def test_ground_to_excited_tdm(self, system: str, case: str, kind: str, assert (dm_ao_b == approx(ref_dm_b)) # CVS state-to-state TDM is not implemented in adcc - @pytest.mark.parametrize("system,case,kind", - [c for c in cases if "cvs" not in c[1]]) - def test_state_to_state_tdm(self, system: str, case: str, kind: str, - method: str, generator: str): - + @pytest.mark.parametrize("system,method,case,kind", + [c for c in cases_pp if "cvs" not in c[2]]) + def test_state_to_state_tdm_pp(self, system: str, method: str, case: str, + kind: str, generator: str): refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator )[kind] @@ -142,3 +196,62 @@ def test_state_to_state_tdm(self, system: str, case: str, kind: str, dm_ao_a, dm_ao_b = state_to_state.transition_dm[j].to_ao_basis() np.testing.assert_allclose(dm_ao_a.to_ndarray(), ref_a, atol=1e-4) np.testing.assert_allclose(dm_ao_b.to_ndarray(), ref_b, atol=1e-4) + + @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea) + def test_state_to_state_tdm_ip_ea(self, system: str, method: str, case: str, + kind: str, is_alpha: bool, generator: str): + if generator == "adcman": + if system.startswith("h2o"): + pytest.skip("Difficulties with data import for restricted cases") + if method.endswith("adc1"): + pytest.skip("No adcman reference data for IP/EA-ADC(1)") + elif method.endswith("adc2x"): + pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") + + refdata = testdata_cache._load_data( + system=system, method=method, case=case, source=generator, + is_alpha=is_alpha + )[kind] + if len(refdata["eigenvalues"]) < 2: + pytest.skip("Less than two states available.") + s2s_data = refdata["state_to_state"] + + + if generator == "adcman" and system.startswith("h2o"): + is_alpha = False + + # construct a ExcitedStates instance using the eigenvalues and eigenstates + # from the reference data. + if AdcMethod(method).adc_type == "ip": + state: DetachedStates = getattr( + testdata_cache, f"{generator}_states")( + system=system, method=method, case=case, kind=kind, + is_alpha=is_alpha + ) + elif AdcMethod(method).adc_type == "ea": + state: AttachedStates = getattr( + testdata_cache, f"{generator}_states")( + system=system, method=method, case=case, kind=kind, + is_alpha=is_alpha + ) + else: + raise ValueError(f"Unknown ADC method: {method.name}") + + # since refdata was used to build state we have to have the same amount + # of states + for i in range(len(state.excitation_vector) - 1): + # Check that we are talking about the same state when + # comparing reference and computed + assert state.excitation_energy[i] == refdata["eigenvalues"][i] + fromi_ref_a = s2s_data[f"from_{i}"]["state_to_excited_tdm_bb_a"] + fromi_ref_b = s2s_data[f"from_{i}"]["state_to_excited_tdm_bb_b"] + + state_to_state = State2States(state, initial=i) + for j, (ref_a, ref_b) in enumerate(zip(fromi_ref_a, fromi_ref_b)): + ito = i + j + 1 + assert state.excitation_energy[ito] == refdata["eigenvalues"][ito] + ref_energy = refdata["eigenvalues"][ito] - refdata["eigenvalues"][i] + assert state_to_state.excitation_energy[j] == ref_energy + dm_ao_a, dm_ao_b = state_to_state.transition_dm[j].to_ao_basis() + np.testing.assert_allclose(dm_ao_a.to_ndarray(), ref_a, atol=1e-4) + np.testing.assert_allclose(dm_ao_b.to_ndarray(), ref_b, atol=1e-4) From 8ecd776469fc12e6bbbcf66c20e370876970d81c Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 15:28:27 +0100 Subject: [PATCH 16/28] guess tes setup --- adcc/tests/guess_test.py | 930 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 871 insertions(+), 59 deletions(-) diff --git a/adcc/tests/guess_test.py b/adcc/tests/guess_test.py index 6463500c0..ea4f1b9e8 100644 --- a/adcc/tests/guess_test.py +++ b/adcc/tests/guess_test.py @@ -34,8 +34,12 @@ # The methods to test -singles_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] -doubles_methods = ["adc2", "adc2x", "adc3"] +singles_methods_pp = ["adc0", "adc1", "adc2", "adc2x", "adc3"] +doubles_methods_pp = ["adc2", "adc2x", "adc3"] +singles_methods_ip = ["ip-adc0", "ip-adc1", "ip-adc2", "ip-adc2x", "ip-adc3"] +doubles_methods_ip = ["ip-adc2", "ip-adc2x", "ip-adc3"] +singles_methods_ea = ["ea-adc0", "ea-adc1", "ea-adc2", "ea-adc2x", "ea-adc3"] +doubles_methods_ea = ["ea-adc2", "ea-adc2x", "ea-adc3"] # the testcases h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop() cn_sto3g = testcases.get_by_filename("cn_sto3g").pop() @@ -43,6 +47,110 @@ class TestGuess: + def test_determine_spin_change_pp(self): + from adcc.guess import determine_spin_change + + method = adcc.AdcMethod("adc2") + + spin = determine_spin_change(method, kind="singlet") + assert spin == 0.0 + + spin = determine_spin_change(method, kind="spin_flip") + assert spin == -1.0 + + + def test_determine_spin_change_ip(self): + from adcc.guess import determine_spin_change + + method = adcc.AdcMethod("ip-adc2") + + spin_alpha = determine_spin_change(method, kind="any", is_alpha=True) + spin_beta = determine_spin_change(method, kind="any", is_alpha=False) + + assert spin_alpha == -0.5 + assert spin_beta == +0.5 + + with pytest.raises(TypeError): + determine_spin_change(method, kind="any", is_alpha=None) + + def test_determine_spin_change_ea(self): + from adcc.guess import determine_spin_change + + method = adcc.AdcMethod("ea-adc2") + + spin_alpha = determine_spin_change(method, kind="any", is_alpha=True) + spin_beta = determine_spin_change(method, kind="any", is_alpha=False) + + assert spin_alpha == +0.5 + assert spin_beta == -0.5 + + with pytest.raises(TypeError): + determine_spin_change(method, kind="any", is_alpha=None) + + def test_determine_spin_change_unknown_adc_type(self): + from adcc.guess import determine_spin_change + + method = adcc.AdcMethod("adc2") + method.adc_type = "bla" + + with pytest.raises(ValueError, match="Unknown ADC method"): + determine_spin_change(method, kind="any") + + def test_estimate_n_guesses_pp(self): + from adcc.guess import estimate_n_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + + def test_estimate_n_guesses_ip(self): + from adcc.guess import estimate_n_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("ip-adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + + # Test different behaviour for IP-ADC(0/1) + matrix = adcc.AdcMatrix("ip-adc0", ground_state) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + assert 5 == estimate_n_guesses(matrix, n_states=5, singles_only=True) + assert 10 == estimate_n_guesses(matrix, n_states=10, singles_only=True) + + def test_estimate_n_guesses_ea(self): + from adcc.guess import estimate_n_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("ea-adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, + singles_only=True) + + # Test different behaviour for EA-ADC(0/1) + matrix = adcc.AdcMatrix("ea-adc0", ground_state) + assert 2 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 2 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + assert 3 == estimate_n_guesses(matrix, n_states=3, singles_only=True) + def assert_symmetry_no_spin_change(self, matrix, guess, block, spin_block_symmetrisation): """ @@ -183,6 +291,152 @@ def assert_symmetry_spin_flip(self, matrix, guess, block): has_babb = np.max(np.abs(gtd[noa:, :nCa, nva:, nva:])) > 0 assert has_aaab or has_aaba or has_abbb or has_babb + def assert_symmetry_ip(self, matrix, guess, block, is_alpha): + """ + Assert a guess vector has the correct symmetry for alpha/beta detachment + """ + # Extract useful quantities + mospaces = matrix.mospaces + nCa = noa = mospaces.n_orbs_alpha("o1") + nCb = nob = mospaces.n_orbs_beta("o1") + nva = mospaces.n_orbs_alpha("v1") + nvb = mospaces.n_orbs_beta("v1") + if mospaces.has_core_occupied_space: + nCa = mospaces.n_orbs_alpha("o2") + nCb = mospaces.n_orbs_beta("o2") + + # Singles + gts = guess.h.to_ndarray() + assert gts.shape == (nCa + nCb,) + if is_alpha: + assert np.max(np.abs(gts[nCa:])) == 0 + else: + assert np.max(np.abs(gts[:nCa])) == 0 + + # Doubles + if "phh" not in matrix.axis_blocks: + return + + gtd = guess.phh.to_ndarray() + assert gtd.shape == (noa + nob, nCa + nCb, nva + nvb) + + if is_alpha: + assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a + assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a + assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b + assert np.max(np.abs(gtd[noa:, nCa:, :nva])) == 0 # bb->a + else: + assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b + assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b + assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a + assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b + + if matrix.reference_state.restricted: + # Restricted automatically means alpha ionization + # Thus forbid spin-flip blocks with right spin + assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b + + if not matrix.is_core_valence_separated: + assert_array_equal(gtd.transpose((1, 0, 2)), -gtd) + + if block == "h": + if is_alpha: + assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b + assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b + assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a + + assert np.max(np.abs(gts[:nCa])) > 0 # has_alpha + else: + assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a + assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a + assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b + + assert np.max(np.abs(gts[nCa:])) > 0 # has_beta + elif block == "phh": + if is_alpha: + assert np.max(np.abs(gts[:nCa])) == 0 + has_aaa = np.max(np.abs(gtd[:noa, :nCa, :nva])) > 0 + has_abb = np.max(np.abs(gtd[:noa, nCa:, nva:])) > 0 + has_bab = np.max(np.abs(gtd[noa:, :nCa, nva:])) > 0 + assert has_aaa or has_abb or has_bab + else: + assert np.max(np.abs(gts[nCa:])) == 0 + has_aba = np.max(np.abs(gtd[:noa, nCa:, :nva])) > 0 + has_baa = np.max(np.abs(gtd[noa:, :nCa, :nva])) > 0 + has_bbb = np.max(np.abs(gtd[noa:, nCa:, nva:])) > 0 + assert has_aba or has_baa or has_bbb + + def assert_symmetry_ea(self, matrix, guess, block, is_alpha): + """ + Assert a guess vector has the correct symmetry for alpha/beta attachment + """ + # Extract useful quantities + mospaces = matrix.mospaces + noa = mospaces.n_orbs_alpha("o1") + nob = mospaces.n_orbs_beta("o1") + nva = mospaces.n_orbs_alpha("v1") + nvb = mospaces.n_orbs_beta("v1") + + # Singles + gts = guess.p.to_ndarray() + assert gts.shape == (nva + nvb,) + if is_alpha: + assert np.max(np.abs(gts[nva:])) == 0 + else: + assert np.max(np.abs(gts[:nva])) == 0 + + # Doubles + if "pph" not in matrix.axis_blocks: + return + + gtd = guess.pph.to_ndarray() + assert gtd.shape == (noa + nob, nva + nvb, nva + nvb) + + if is_alpha: + assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab + assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba + assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb + assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb + else: + assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab + assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba + assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa + assert np.max(np.abs(gtd[noa:, :nva, :nva])) == 0 # b->aa + + if matrix.reference_state.restricted: + # Restricted automatically means alpha attachment + # Thus forbid spin-flip blocks with right spin + assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb + + assert_array_equal(gtd.transpose((0, 2, 1)), -gtd) + + if block == "p": + if is_alpha: + assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab + assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba + assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa + + assert np.max(np.abs(gts[:nva])) > 0 # has_alpha + else: + assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab + assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba + assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb + + assert np.max(np.abs(gts[nva:])) > 0 # has_beta + elif block == "pph": + if is_alpha: + assert np.max(np.abs(gts[:nva])) == 0 + has_aaa = np.max(np.abs(gtd[:noa, :nva, :nva])) > 0 + has_bab = np.max(np.abs(gtd[noa:, :nva, nva:])) > 0 + has_bba = np.max(np.abs(gtd[noa:, nva:, :nva])) > 0 + assert has_aaa or has_bab or has_bba + else: + assert np.max(np.abs(gts[nva:])) == 0 + has_aab = np.max(np.abs(gtd[:noa, :nva, nva:])) > 0 + has_aba = np.max(np.abs(gtd[:noa, nva:, :nva])) > 0 + has_bbb = np.max(np.abs(gtd[noa:, nva:, nva:])) > 0 + assert has_aab or has_aba or has_bbb + def assert_orthonormal(self, guesses): for (i, gi) in enumerate(guesses): for (j, gj) in enumerate(guesses): @@ -190,10 +444,10 @@ def assert_orthonormal(self, guesses): assert adcc.dot(gi, gj) == approx(ref) def assert_guess_values(self, matrix, block, guesses, spin_flip=False, - triplet=False): + triplet=False, is_alpha: bool = None): """ - Assert that the guesses correspond to the smallest - diagonal values. + Assert that the provided guesses correspond to the smallest + allowed diagonal elements for the requested block. """ # Extract useful quantities mospaces = matrix.mospaces @@ -204,40 +458,42 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, # Make a list of diagonal indices, ordered by the corresponding # diagonal values - sidcs = None - if block == "ph": - diagonal = matrix.diagonal().ph.to_ndarray() + diagonal = matrix.diagonal().get(block).to_ndarray() + + # Doubles guesses are constructed from the 0th order diagonal + if matrix.method.level > 1 and not matrix.method.name.endswith("adc2"): + if block == "pphh": + diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0( + matrix.reference_state + ).pphh.to_ndarray() + elif block == "phh": + diagonal = adcc.adc_ip.matrix.diagonal_phh_phh_0( + matrix.reference_state + ).phh.to_ndarray() + elif block == "pph": + diagonal = adcc.adc_ea.matrix.diagonal_pph_pph_0( + matrix.reference_state + ).pph.to_ndarray() + + # Build list of indices, which would sort the diagonal + order = np.argsort(diagonal.ravel()) + sidcs = list(zip(*np.unravel_index(order, diagonal.shape))) + assert sidcs - # Build list of indices, which would sort the diagonal - sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()), - diagonal.shape)) - assert sidcs.shape[0] == 1 + if block == "ph": if spin_flip: - sidcs = [idx for idx in sidcs[0] - if idx[0] < nCa and idx[1] >= nva] + sidcs = [idx for idx in sidcs + if idx[0] < nCa and idx[1] >= nva] else: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs if any((idx[0] >= nCa and idx[1] >= nva, idx[0] < nCa and idx[1] < nva)) # noqa: E221 ] elif block == "pphh": - # the doubles guesses are constructed from the 0th order diagonal - if matrix.method.name.endswith("adc2"): - diagonal = matrix.diagonal().pphh.to_ndarray() - else: - diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0( - matrix.reference_state - ).pphh.to_ndarray() - - # Build list of indices, which would sort the diagonal - sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()), - diagonal.shape)) - - assert sidcs.shape[0] == 1 if spin_flip: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] >= nva, # noqa: E221,E501 idx[0] < noa and idx[1] < nCa and idx[2] >= nva and idx[3] < nva, # noqa: E221,E501 idx[0] < noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501 @@ -245,7 +501,7 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, ] else: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs # aaaa / bbbb / abab / baba / abba / baab if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] < nva, # noqa: E221,E501 idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501 @@ -261,17 +517,69 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, # cover the ccvv block in CVS-ADC. if triplet and not mospaces.has_core_occupied_space: sidcs = [idx for idx in sidcs - if abs(idx[0] - idx[1]) != noa - or abs(idx[2] - idx[3]) != nva] + if abs(idx[0] - idx[1]) != noa + or abs(idx[2] - idx[3]) != nva] sidcs = [idx for idx in sidcs if idx[2] != idx[3]] if not matrix.is_core_valence_separated: sidcs = [idx for idx in sidcs if idx[0] != idx[1]] + elif block == "h": + # IP-ADC singles + if is_alpha: + sidcs = [idx for idx in sidcs if idx[0] < noa] + else: + sidcs = [idx for idx in sidcs if idx[0] >= noa] + elif block == "phh": + # IP-ADC doubles + if is_alpha: + sidcs = [ + idx for idx in sidcs + # aaa / abb / bab + if any(( + idx[0] < noa and idx[1] < nCa and idx[2] < nva, + idx[0] < noa and idx[1] >= nCa and idx[2] >= nva, + idx[0] >= noa and idx[1] < nCa and idx[2] >= nva)) + ] + else: + sidcs = [ + idx for idx in sidcs + # aba / baa / bbb + if any(( + idx[0] < noa and idx[1] >= nCa and idx[2] < nva, + idx[0] >= noa and idx[1] < nCa and idx[2] < nva, + idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva)) + ] + sidcs = [idx for idx in sidcs if idx[0] != idx[1]] + elif block == "p": + # EA-ADC singles + if is_alpha: + sidcs = [idx for idx in sidcs if idx[0] < nva] + else: + sidcs = [idx for idx in sidcs if idx[0] >= nva] + elif block == "pph": + # EA-ADC doubles + if is_alpha: + sidcs = [ + idx for idx in sidcs + if any(( + idx[0] < noa and idx[1] < nva and idx[2] < nva, + idx[0] >= noa and idx[1] < nva and idx[2] >= nva, + idx[0] >= noa and idx[1] >= nva and idx[2] < nva)) + ] + else: + sidcs = [ + idx for idx in sidcs + if any(( + idx[0] < noa and idx[1] < nva and idx[2] >= nva, + idx[0] < noa and idx[1] >= nva and idx[2] < nva, + idx[0] >= noa and idx[1] >= nva and idx[2] >= nva)) + ] + sidcs = [idx for idx in sidcs if idx[1] != idx[2]] # Group the indices by corresponding diagonal value def grouping(x): return np.round(diagonal[tuple(x)], decimals=12) gidcs = [[tuple(gitem) for gitem in group] - for _, group in itertools.groupby(sidcs, grouping)] + for key, group in itertools.groupby(sidcs, grouping)] igroup = 0 # The current diagonal value group we are in for (i, guess) in enumerate(guesses): # Extract indices of non-zero elements @@ -328,9 +636,47 @@ def base_test_spin_flip(self, system: str, case: str, method: str, block: str, self.assert_orthonormal(guesses) self.assert_guess_values(matrix, block, guesses, spin_flip=True) - @pytest.mark.parametrize("method", singles_methods) + def base_test_ip(self, system: str, case: str, method: str, block: str, + is_alpha: bool, max_guesses: int = 10): + """ + Test IP-ADC guess construction for alpha/beta detachment + """ + hf = testdata_cache.refstate(system, case=case) + matrix = adcc.AdcMatrix(method, hf) + spin_change = -0.5 if is_alpha else +0.5 + for n_guesses in range(3, max_guesses + 1): + guesses = adcc.guess.guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=spin_change, + is_alpha=is_alpha + ) + assert len(guesses) == n_guesses + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha) + + def base_test_ea(self, system: str, case: str, method: str, block: str, + is_alpha: bool, max_guesses: int = 10): + """ + Test EA-ADC guess construction for alpha/beta attachment + """ + hf = testdata_cache.refstate(system, case=case) + matrix = adcc.AdcMatrix(method, hf) + spin_change = +0.5 if is_alpha else -0.5 + for n_guesses in range(1, max_guesses + 1): + guesses = adcc.guess.guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=spin_change, + is_alpha=is_alpha + ) + assert len(guesses) == n_guesses + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", h2o_sto3g.cases) - def test_singles_h2o(self, method: str, case: str): + def test_singles_h2o_pp(self, method: str, case: str): guesses = { # fewer guesses available "fv-cvs": 1, "cvs": 2, "fc": 8, "fv": 5, "fc-fv": 4, "fc-cvs": 2, "fc-fv-cvs": 1 @@ -340,9 +686,9 @@ def test_singles_h2o(self, method: str, case: str): max_guesses=guesses.get(case, 10) ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", h2o_sto3g.cases) - def test_doubles_h2o(self, method: str, case: str): + def test_doubles_h2o_pp(self, method: str, case: str): guesses = { # fewer ocvv guesses available "fv-cvs": 4, "fc-fv-cvs": 3 } @@ -351,9 +697,41 @@ def test_doubles_h2o(self, method: str, case: str): max_guesses=guesses.get(case, 5) ) - @pytest.mark.parametrize("method", singles_methods) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ip")) + def test_singles_h2o_ip(self, method: str, case: str): + self.base_test_ip( + "h2o_sto3g", case, method, block="h", is_alpha=True, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ip")) + def test_doubles_h2o_ip(self, method: str, case: str): + self.base_test_ip( + "h2o_sto3g", case, method, block="phh", is_alpha=True, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ea")) + def test_singles_h2o_ea(self, method: str, case: str): + self.base_test_ea( + "h2o_sto3g", case, method, block="p", is_alpha=True, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ea")) + def test_doubles_h2o_ea(self, method: str, case: str): + self.base_test_ea( + "h2o_sto3g", case, method, block="pph", is_alpha=True, + max_guesses=4 + ) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", cn_sto3g.cases) - def test_singles_cn(self, method: str, case: str): + def test_singles_cn_pp(self, method: str, case: str): guesses = { # fewer guesses available "cvs": 7, "fc-cvs": 7, "fv-cvs": 5, "fc-fv-cvs": 5 } @@ -362,34 +740,108 @@ def test_singles_cn(self, method: str, case: str): max_guesses=guesses.get(case, 10) ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", cn_sto3g.cases) - def test_doubles_cn(self, method: str, case: str): + def test_doubles_cn_pp(self, method: str, case: str): self.base_test_no_spin_change( system="cn_sto3g", case=case, method=method, block="pphh", max_guesses=5 ) - @pytest.mark.parametrize("method", singles_methods) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ip")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_cn_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "cn_sto3g", case, method, block="h", is_alpha=is_alpha, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ip")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_cn_ip(self, method: str, case: str, is_alpha: bool): + case="gen" + self.base_test_ip( + "cn_sto3g", case, method, block="phh", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ea")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_cn_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "cn_sto3g", case, method, block="p", is_alpha=is_alpha, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ea")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_cn_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "cn_sto3g", case, method, block="pph", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", hf_631g.cases) - def test_singles_hf(self, method: str, case: str): + def test_singles_hf_pp(self, method: str, case: str): self.base_test_spin_flip( system="hf_631g", case=case, method=method, block="ph", max_guesses=10 ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", hf_631g.cases) - def test_doubles_hf(self, method: str, case: str): + def test_doubles_hf_pp(self, method: str, case: str): self.base_test_spin_flip( system="hf_631g", case=case, method=method, block="pphh", max_guesses=5 ) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", hf_631g.filter_cases("ip")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_hf_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "hf_631g", case, method, block="h", is_alpha=is_alpha, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", hf_631g.filter_cases("ip")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_hf_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "hf_631g", case, method, block="phh", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", hf_631g.filter_cases("ea")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_hf_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "hf_631g", case, method, block="p", is_alpha=is_alpha, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", hf_631g.filter_cases("ea")) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_hf_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "hf_631g", case, method, block="pph", is_alpha=is_alpha, + max_guesses=5 + ) + + # # Tests against reference values # - def base_reference(self, matrix, ref): + def base_reference_pp(self, matrix, ref): symmetrisations = ["none"] if matrix.reference_state.restricted: symmetrisations = ["symmetric", "antisymmetric"] @@ -411,15 +863,234 @@ def base_reference(self, matrix, ref): nonzeros = np.dstack(np.where(guess_b != 0)) assert nonzeros.shape[0] == 1 nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] - values = guess_b[guess_b != 0] - assert nonzeros == ref_sb[i][0] - assert_array_equal(values, np.array(ref_sb[i][1])) + indices_sorted = tuple(sorted(nonzeros)) + indices_ref_sorted = tuple(sorted(ref_sb[i][0])) + assert indices_sorted == indices_ref_sorted + + def base_reference_degenerate_pp(self, matrix, ref): + """ + Validate PP guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + symmetrisations = ["none"] + if matrix.reference_state.restricted: + symmetrisations = ["symmetric", "antisymmetric"] + + for block in ["ph", "pphh"]: + for symm in symmetrisations: + ref_sb = ref[(block, symm)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block, spin_change=0, + spin_block_symmetrisation=symm + ) + assert len(guesses) == len(ref_sb) + for gs in guesses: + self.assert_symmetry_no_spin_change(matrix, gs, block, symm) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) + + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) + + def base_reference_ip(self, matrix, ref, is_alpha=True): + spin_change = -0.5 if is_alpha else +0.5 + for block in ["h", "phh"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + for (i, guess) in enumerate(guesses): + guess_b = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(guess_b != 0)) + assert nonzeros.shape[0] == 1 + nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] + values = guess_b[guess_b != 0] + assert nonzeros == ref_sb[i][0] + assert_array_equal(values, np.array(ref_sb[i][1])) + + def base_reference_degenerate_ip(self, matrix, ref, is_alpha=True): + """ + Validate IP guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + spin_change = -0.5 if is_alpha else +0.5 + for block in ["h", "phh"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) - @pytest.mark.parametrize("method", doubles_methods) - def test_reference_h2o(self, method: str): + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) + + def base_reference_ea(self, matrix, ref, is_alpha=True): + spin_change = +0.5 if is_alpha else -0.5 + for block in ["p", "pph"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + for (i, guess) in enumerate(guesses): + guess_b = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(guess_b != 0)) + assert nonzeros.shape[0] == 1 + nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] + values = guess_b[guess_b != 0] + assert nonzeros == ref_sb[i][0] + assert_array_equal(values, np.array(ref_sb[i][1])) + + def base_reference_degenerate_ea(self, matrix, ref, is_alpha=True): + """ + Validate EA guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + spin_change = +0.5 if is_alpha else -0.5 + for block in ["p", "pph"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) + + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) + + @pytest.mark.parametrize("method", doubles_methods_pp) + def test_reference_h2o_pp(self, method: str): hf = testdata_cache.refstate("h2o_sto3g", "gen") matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) - self.base_reference(matrix=matrix, ref=self.get_ref_h2o()) + self.base_reference_pp(matrix=matrix, ref=self.get_ref_h2o_pp()) + + @pytest.mark.parametrize("method", doubles_methods_ip) + def test_reference_h2o_ip(self, method: str): + hf = testdata_cache.refstate("h2o_sto3g", "gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + self.base_reference_ip(matrix=matrix, ref=self.get_ref_h2o_ip()) + + @pytest.mark.parametrize("method", doubles_methods_ea) + def test_reference_h2o_ea(self, method: str): + hf = testdata_cache.refstate("h2o_sto3g", "gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + self.base_reference_ea(matrix=matrix, ref=self.get_ref_h2o_ea()) # NOTE: This test is a bit weird: the order of the guesses is # ill defined, because some orbitals are degenerate for cn sto3g: @@ -431,13 +1102,44 @@ def test_reference_h2o(self, method: str): # against hard coded reference data. The test against numpy above should be # sufficient. - # @pytest.mark.parametrize("method", doubles_methods) - # def test_reference_cn(self, method: str): - # hf = testdata_cache.refstate("cn_sto3g", case="gen") - # matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) - # self.base_reference(matrix=matrix, ref=self.get_ref_cn()) + # Current workaround: Compare guess energies rather than exact ordering for + # these cases by calling 'base_reference_degenerate_{adc_type}()' + + @pytest.mark.parametrize("method", doubles_methods_pp) + def test_reference_cn_pp(self, method: str): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + # NOTE: doubles guesses for higher ADC levels are constructed + # from the ADC(2) zeroth-order diagonal. + # We enforce this here explicitly to avoid method-dependent + # degeneracy reordering. + matrix._diagonal = adcc.AdcMatrix(method="adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_pp(matrix=matrix, ref=self.get_ref_cn_pp()) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_reference_cn_ip(self, method: str, is_alpha: bool): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + matrix._diagonal = adcc.AdcMatrix(method="ip-adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_ip( + matrix=matrix, ref=self.get_ref_cn_ip(), is_alpha=is_alpha + ) - def get_ref_h2o(self): + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_reference_cn_ea(self, method: str, is_alpha: bool): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + matrix._diagonal = adcc.AdcMatrix(method="ea-adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_ea( + matrix=matrix, ref=self.get_ref_cn_ea(), is_alpha=is_alpha + ) + + def get_ref_h2o_pp(self): sq8 = 1 / np.sqrt(8) sq12 = 1 / np.sqrt(12) sq48 = 1 / np.sqrt(48) @@ -523,7 +1225,7 @@ def get_ref_h2o(self): ], } - def get_ref_cn(self): + def get_ref_cn_pp(self): sq8 = 1 / np.sqrt(8) return { ("ph", "none"): [ @@ -553,3 +1255,113 @@ def get_ref_cn(self): [0.5, -0.5, -0.5, 0.5]), ], } + + def get_ref_h2o_ip(self): + sq6 = 1 / np.sqrt(6) + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + return { + ("h", True): [ + ([(4, )], [1]), + ([(3, )], [1]), + ([(2, )], [1]), + ([(1, )], [1]), + ([(0, )], [1]) + ], + ("phh", True): [ + ([(4, 9, 2), (9, 4, 2)], asymm), + ([(3, 4, 0), (3, 9, 2), (4, 3, 0), + (4, 8, 2), (8, 4, 2), (9, 3, 2)], + [-sq6, -sq6, sq6, -sq6, sq6, sq6]), + ([(3, 8, 2), (8, 3, 2)], asymm), + ([(4, 9, 3), (9, 4, 3)], asymm), + ([(3, 4, 1), (3, 9, 3), (4, 3, 1), + (4, 8, 3), (8, 4, 3), (9, 3, 3)], + [-sq6, -sq6, sq6, -sq6, sq6, sq6]) + ], + } + + def get_ref_h2o_ea(self): + sq6 = 1 / np.sqrt(6) + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + return { + ("p", True): [ + ([(0, )], [1]), + ([(1, )], [1]) + ], + ("pph", True): [ + ([(9, 0, 2), (9, 2, 0)], asymm), + ([(8, 0, 2), (8, 2, 0)], asymm), + ([(4, 0, 1), (4, 1, 0), (9, 0, 3), + (9, 1, 2), (9, 2, 1), (9, 3, 0)], + [-sq6, sq6, -sq6, -sq6, sq6, sq6]), + ([(3, 0, 1), (3, 1, 0), (8, 0, 3), + (8, 1, 2), (8, 2, 1), (8, 3, 0)], + [-sq6, sq6, -sq6, -sq6, sq6, sq6]), + ([(7, 0, 2), (7, 2, 0)], asymm) + ], + } + + def get_ref_cn_ip(self): + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)] + return { + ("h", True): [ + ([(6, )], [1]), + ([(4, )], [1]), # occ. 4 and 5 are degenerate + ([(5, )], [1]), # occ. 4 and 5 are degenerate + ([(3, )], [1]), + ([(2, )], [1]) + ], + ("h", False): [ + ([(11, )], [1]), + ([(12, )], [1]), + ([(10, )], [1]), + ([(9, )], [1]), + ([(8, )], [1]) + ], + ("phh", True): [ + ([(6, 11, 3), (11, 6, 3)], asymm1), + ([(6, 12, 3), (12, 6, 3)], asymm1), + ([(4, 11, 3), (11, 4, 3)], asymm), + ([(4, 12, 3), (12, 4, 3)], asymm), + ([(5, 11, 3), (11, 5, 3)], asymm1) + ], + ("phh", False): [ + ([(11, 12, 3), (12, 11, 3)], asymm), + ([(10, 11, 3), (11, 10, 3)], asymm), + ([(10, 12, 3), (12, 10, 3)], asymm), + ([(6, 11, 0), (11, 6, 0)], asymm1), + ([(6, 12, 0), (12, 6, 0)], asymm1) + ], + } + + def get_ref_cn_ea(self): + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)] + return { + ("p", True): [ + ([(0, )], [1]), + ([(1, )], [1]), + ([(2, )], [1]) + ], + ("p", False): [ + ([(3, )], [1]), + ([(4, )], [1]), + ([(5, )], [1]), + ([(6, )], [1]) + ], + ("pph", True): [ + ([(11, 0, 3), (11, 3, 0)], asymm), + ([(12, 0, 3), (12, 3, 0)], asymm), + ([(11, 1, 3), (11, 3, 1)], asymm1), + ([(12, 1, 3), (12, 3, 1)], asymm1), + ([(10, 0, 3), (10, 3, 0)], asymm) + ], + ("pph", False): [ + ([(6, 0, 3), (6, 3, 0)], asymm), + ([(6, 1, 3), (6, 3, 1)], asymm1), + ([(4, 0, 3), (4, 3, 0)], asymm), # occ. 4 and 5 are degenerate + ([(5, 0, 3), (5, 3, 0)], asymm), # occ. 4 and 5 are degenerate + ([(4, 1, 3), (4, 3, 1)], asymm1) + ], + } \ No newline at end of file From 31dc8a9f401a048fcc48536bd53fa42a56fd7e86 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 15:42:13 +0100 Subject: [PATCH 17/28] put spin_change back into guess functions and make guesses_doublet available --- adcc/guess/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py index 89ed751c1..8fb0aaae4 100644 --- a/adcc/guess/__init__.py +++ b/adcc/guess/__init__.py @@ -25,7 +25,7 @@ from .util import estimate_n_guesses, determine_spin_change __all__ = ["guess_zero", "guesses_from_diagonal", - "get_spin_block_symmetrisation", + "get_spin_block_symmetrisation", "guesses_doublet", "guesses_singlet", "guesses_triplet", "guesses_any", "guesses_spin_flip", "guess_symmetries", "estimate_n_guesses", "determine_spin_change"] @@ -64,13 +64,13 @@ def guesses_singlet(matrix, n_guesses, block="ph", **kwargs): kwargs Any other argument understood by guesses_from_diagonal. """ return guesses_from_diagonal( - matrix, n_guesses, block=block, + matrix, n_guesses, block=block, spin_change=0, spin_block_symmetrisation=get_spin_block_symmetrisation("singlet"), **kwargs ) -def guesses_doublet(matrix, n_guesses, block="h", is_alpha=None, **kwargs): +def guesses_doublet(matrix, n_guesses, block="h", is_alpha=True, **kwargs): """ Obtain guesses for computing doublet states by inspecting the passed ADC matrix. @@ -84,8 +84,13 @@ def guesses_doublet(matrix, n_guesses, block="h", is_alpha=None, **kwargs): IP-/EA-ADC calculation. kwargs Any other argument understood by guesses_from_diagonal. """ + if matrix.method.adc_type == "ip": + spin_change = -0.5 + elif matrix.method.adc_type == "ea": + spin_change = 0.5 return guesses_from_diagonal( - matrix, n_guesses, block=block, is_alpha=is_alpha, + matrix, n_guesses, block=block, + is_alpha=is_alpha, spin_change=spin_change, spin_block_symmetrisation= get_spin_block_symmetrisation("doublet"), **kwargs ) @@ -106,7 +111,7 @@ def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): kwargs Any other argument understood by guesses_from_diagonal. """ return guesses_from_diagonal( - matrix, n_guesses, block=block, + matrix, n_guesses, block=block, spin_change=0, spin_block_symmetrisation= get_spin_block_symmetrisation("triplet"), **kwargs ) @@ -131,7 +136,7 @@ def guesses_spin_flip(matrix, n_guesses, block="ph", **kwargs): kwargs Any other argument understood by guesses_from_diagonal. """ return guesses_from_diagonal( - matrix, n_guesses, block=block, + matrix, n_guesses, block=block, spin_change=-1, spin_block_symmetrisation= get_spin_block_symmetrisation("spin_flip"), **kwargs ) \ No newline at end of file From 69dc17b87c02066d8c81a5874eadfc8c0ef4b052 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Tue, 24 Feb 2026 15:42:35 +0100 Subject: [PATCH 18/28] updated workflow test --- adcc/tests/workflow_test.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py index f2229ba77..44fe1be34 100644 --- a/adcc/tests/workflow_test.py +++ b/adcc/tests/workflow_test.py @@ -432,7 +432,7 @@ def test_diagonalise_adcmatrix_pp(self): def test_diagonalise_adcmatrix_ip(self): from adcc.workflow import diagonalise_adcmatrix - pytest.skip("adcman referencedata not yet available") + # pytest.skip("adcman referencedata not yet available") system = "h2o_sto3g" case = "gen" method = "ip-adc2" @@ -445,10 +445,10 @@ def test_diagonalise_adcmatrix_ip(self): matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) - guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="h", - is_alpha=False) + guesses = adcc.guesses_doublet(matrix, n_guesses=6, block="h", + is_alpha=True) res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, - guesses=guesses, is_alpha=False) + guesses=guesses, is_alpha=True) assert res.converged assert res.eigenvalues[:n_states] == approx(ref_doublets[:n_states]) @@ -469,20 +469,19 @@ def test_diagonalise_adcmatrix_ip(self): def test_diagonalise_adcmatrix_ea(self): from adcc.workflow import diagonalise_adcmatrix - pytest.skip("adcman referencedata not yet available") system = "h2o_sto3g" case = "gen" method = "ea-adc2" kind = "doublet" refdata = testdata_cache.adcman_data(system, method=method, case=case, - is_alpha=True) + is_alpha=False) ref_doublets = refdata[kind]["eigenvalues"] n_states = min(len(ref_doublets), 3) matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) - guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="p", + guesses = adcc.guesses_doublet(matrix, n_guesses=6, block="p", is_alpha=True) res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, guesses=guesses, is_alpha=True) From 047d5f40238c375ed3fab6b753dec26b1060a216 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 09:08:42 +0100 Subject: [PATCH 19/28] added matrix reference data for alpha ip/ea calcs and set alpha default for loading ip/ea reference data --- adcc/tests/generators/generate_adcc_data.py | 17 +++++++---------- adcc/tests/testdata_cache.py | 8 ++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/adcc/tests/generators/generate_adcc_data.py b/adcc/tests/generators/generate_adcc_data.py index 4b03b0af3..db8b95d90 100644 --- a/adcc/tests/generators/generate_adcc_data.py +++ b/adcc/tests/generators/generate_adcc_data.py @@ -71,20 +71,17 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, is_alpha=is_alpha, **kwargs ) assert states.kind == kind # maybe we predicted wrong? - if f"{key}/matrix" not in hdf5_file: - # the matrix data is only dumped once for each case. I think it does not - # make sense to dump the data once for a singlet and once for a triplet - # trial vector. + if method.adc_type in ("ip", "ea"): + key = f"{key}/{spin}" + if f"{key}/matrix" not in hdf5_file and spin == "alpha": + # the matrix data is only dumped once for each case and only for alpha. + # I think it does not make sense to dump the data once for a singlet + # and once for a triplet trial vector as well as an alpha and a beta one. matrix_group = hdf5_file.create_group(f"{key}/matrix") trial_vec = adcc_copy(states.excitation_vector[0]).set_random() dump_matrix_testdata(states.matrix, trial_vec, matrix_group) # dump the excited states data - if method.adc_type == "pp": - kind_group = hdf5_file.create_group(f"{key}/{states.kind}") - elif method.adc_type in ("ip", "ea"): - kind_group = hdf5_file.create_group(f"{key}/{spin}/{states.kind}") - else: - raise ValueError(f"Unknown ADC method: {method.name}") + kind_group = hdf5_file.create_group(f"{key}/{states.kind}") dump_excited_states(states, kind_group, dump_nstates=dump_nstates) diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py index f949139cd..0e81a6202 100644 --- a/adcc/tests/testdata_cache.py +++ b/adcc/tests/testdata_cache.py @@ -179,6 +179,8 @@ def adcc_data(self, system: str, method: str, case: str, reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._load_data( system=system, method=method, case=case, gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha @@ -191,6 +193,8 @@ def adcman_data(self, system: str, method: str, case: str, reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._load_data( system=system, method=method, case=case, gs_density_order=gs_density_order, source="adcman", @@ -303,6 +307,8 @@ def adcc_states(self, system: str, method: str, kind: str, state kind (singlet/triplet/any/...) and optionally gs_density_order (2/3/sigma4+) using the adcc eigenstates and eigenvalues. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._make_mock_adc_state( system, method=method, case=case, kind=kind, gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha @@ -318,6 +324,8 @@ def adcman_states(self, system: str, method: str, kind: str, state kind (singlet/triplet/any/...) and optionally gs_density_order (2/3/sigma4+) using the adcman eigenstates and eigenvalues. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._make_mock_adc_state( system, method=method, case=case, kind=kind, gs_density_order=gs_density_order, source="adcman", From 7cb94c75561d377de8e065ff8641504cbf329f51 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 09:09:13 +0100 Subject: [PATCH 20/28] adapted spin_change behavior --- adcc/guess/guess_zero.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/adcc/guess/guess_zero.py b/adcc/guess/guess_zero.py index c57cc2b1b..4ed50dc21 100644 --- a/adcc/guess/guess_zero.py +++ b/adcc/guess/guess_zero.py @@ -84,10 +84,11 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"): " You passed {}".format(spin_change)) max_spin_change = 0.5 * len(matrix.axis_blocks[-1]) + valid_spin_changes = [max_spin_change - i for i in range(int(2 * max_spin_change + 1))] - if abs(spin_change) > max_spin_change: - raise ValueError("spin_change for may only be in the " - f"range [{-max_spin_change}, {max_spin_change}] and " + if spin_change not in valid_spin_changes: + raise ValueError("spin_change for may only be one of " + f"{valid_spin_changes}, and " f"not {spin_change}.") symmetries = {} From 7a06f83417c75720d773f2a566c1bc48127cc525 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 10:08:26 +0100 Subject: [PATCH 21/28] IP/EA-ADC AdcMatrix tests --- adcc/tests/AdcMatrix_test.py | 233 +++++++++++++++++++++++++++++------ 1 file changed, 194 insertions(+), 39 deletions(-) diff --git a/adcc/tests/AdcMatrix_test.py b/adcc/tests/AdcMatrix_test.py index 325eb2f30..b23247fa1 100644 --- a/adcc/tests/AdcMatrix_test.py +++ b/adcc/tests/AdcMatrix_test.py @@ -68,7 +68,7 @@ def test_default_block_orders(self): assert block_orders == ref assert block_orders == cvs_block_orders - def test_validate_block_orders(self): + def test_validate_block_orders_pp(self): valid_block_orders = ( {"ph_ph": 0}, # adc0 {"ph_ph": 1, "ph_pphh": None, "ppphhh_ppphhh": None}, # adc1 @@ -90,7 +90,7 @@ def test_validate_block_orders(self): block_orders, AdcMethod("adc0") ) - def test_validate_space(self): + def test_validate_space_pp(self): # some valid PP-ADC methods assert AdcMatrixlike._is_valid_space( "ph", AdcMethod("adc0") @@ -112,15 +112,65 @@ def test_validate_space(self): "phh", AdcMethod("adc0") ) + def test_validate_block_orders_ip_ea(self): + # Valid IP block orders + valid_ip = ( + {"h_h": 0}, # ip-adc0 + {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 0}, # ip-adc2 + ) + for block_orders in valid_ip: + AdcMatrixlike._validate_block_orders( + block_orders, AdcMethod("ip-adc2") + ) + + # Valid EA block orders + valid_ea = ( + {"p_p": 0}, # ea-adc0 + {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 0}, # ea-adc2 + ) + for block_orders in valid_ea: + AdcMatrixlike._validate_block_orders( + block_orders, AdcMethod("ea-adc2") + ) + + # Invalid mixed blocks + with pytest.raises(ValueError): + AdcMatrixlike._validate_block_orders( + {"ph_ph": 0}, AdcMethod("ip-adc2") + ) + + def test_validate_space_ip(self): + ip = AdcMethod("ip-adc0") + assert AdcMatrixlike._is_valid_space("h", ip) + assert AdcMatrixlike._is_valid_space("phh", ip) + assert AdcMatrixlike._is_valid_space("ppphhhh", ip) + assert not AdcMatrixlike._is_valid_space("ph", ip) + assert not AdcMatrixlike._is_valid_space("p", ip) + + def test_validate_space_ea(self): + ea = AdcMethod("ea-adc0") + assert AdcMatrixlike._is_valid_space("p", ea) + assert AdcMatrixlike._is_valid_space("pph", ea) + assert AdcMatrixlike._is_valid_space("pppphhh", ea) + assert not AdcMatrixlike._is_valid_space("ph", ea) + assert not AdcMatrixlike._is_valid_space("h", ea) + h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop() -methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] +pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] +ip_methods = ["ip-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]] +ea_methods = ["ea-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]] + +cases = [(m, c) for c in ["gen", "cvs"] for m in pp_methods] +# No CVS for IP-ADC (yet) +# Test only for is_alpha=True for simplicity +cases += [(m, c) for c in ["gen"] for m in ip_methods] +cases += [(m, c) for c in ["gen"] for m in ea_methods] # Distinct implementations of the matrix equations only exist for the cases # "gen" and "cvs". -@pytest.mark.parametrize("method", methods) -@pytest.mark.parametrize("case", ["gen", "cvs"]) +@pytest.mark.parametrize("method,case", cases) @pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"]) class TestAdcMatrix: def load_matrix_data(self, system: str, case: str, method: str) -> dict: @@ -171,16 +221,18 @@ def test_matvec(self, system: str, case: str, method: str): if matrix.reference_state.restricted: if matrix.method.adc_type == "pp": kind = "singlet" + elif matrix.method.adc_type in ("ip", "ea"): + kind = "doublet" else: raise ValueError(f"Unknown adc type {matrix.method.adc_type}.") else: kind = "any" # we don't do the test for spin flip trial_vec = self.construct_trial_vec(system, case, method, kind) result = matrix @ trial_vec - assert_allclose(matdata["matvec_singles"], result.ph.to_ndarray(), + assert_allclose(matdata["matvec_singles"], result.get(matrix.axis_blocks[0]).to_ndarray(), rtol=1e-10, atol=1e-12) if "matvec_doubles" in matdata: - assert_allclose(matdata["matvec_doubles"], result.pphh.to_ndarray(), + assert_allclose(matdata["matvec_doubles"], result.get(matrix.axis_blocks[1]).to_ndarray(), rtol=1e-10, atol=1e-12) def test_compute_block(self, system: str, case: str, method: str): @@ -191,6 +243,8 @@ def test_compute_block(self, system: str, case: str, method: str): if matrix.reference_state.restricted: if matrix.method.adc_type == "pp": kind = "singlet" + elif matrix.method.adc_type in ("ip", "ea"): + kind = "doublet" else: raise ValueError(f"Unknwon adc type {matrix.method.adc_type}.") else: @@ -209,10 +263,89 @@ def test_compute_block(self, system: str, case: str, method: str): atol=1e-12 ) + def test_hermiticity(self, system, case, method): + matrix = self.construct_matrix(system, case, method) + + # Only test for Hermitian ADC variants + # (Projected matrix may not preserve symmetry fully) + spin_change = 0 + if matrix.method.adc_type == "ip": + spin_change = -0.5 + elif matrix.method.adc_type == "ea": + spin_change = 0.5 + + v = adcc.guess_zero(matrix, spin_change=spin_change) + w = adcc.guess_zero(matrix, spin_change=spin_change) + + v.set_random() + w.set_random() + + Av = matrix @ v + Aw = matrix @ w + + lhs = v.dot(Aw) + rhs = Av.dot(w) + + assert abs(lhs - rhs) < 1e-10 + class TestAdcMatrixInterface: - @pytest.mark.parametrize("method", methods) - @pytest.mark.parametrize("case", h2o_sto3g.cases) + @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods) + @pytest.mark.parametrize("system", ["h2o_sto3g"]) + @pytest.mark.parametrize("case", ["gen"]) # no CVS for IP/EA + def test_axis_structure_all_types(self, system, case, method): + reference_state = testdata_cache.refstate(system=system, case=case) + ground_state = adcc.LazyMp(reference_state) + + matrix = adcc.AdcMatrix(method, ground_state) + + assert matrix.ndim == 2 + assert matrix.shape[0] == matrix.shape[1] + assert len(matrix) == matrix.shape[0] + + blocks = matrix.axis_blocks + assert isinstance(blocks, list) + assert len(blocks) >= 1 + + # Block ordering must follow excitation rank + lengths = [len(b) for b in blocks] + assert lengths == sorted(lengths) + + # Axis dictionaries must match blocks + assert sorted(matrix.axis_spaces.keys(), key=len) == blocks + assert sorted(matrix.axis_lengths.keys(), key=len) == blocks + + # Validate block labels by ADC type + adc_type = matrix.method.adc_type + + if adc_type == "pp": + assert all(set(b).issubset({"p", "h"}) for b in blocks) + assert blocks[0].count("p") == 1 + assert blocks[0].count("h") == 1 + + elif adc_type == "ip": + # First block must remove one electron + assert blocks[0].count("h") == 1 + assert blocks[0].count("p") == 0 + + elif adc_type == "ea": + # First block must add one electron + assert blocks[0].count("p") == 1 + assert blocks[0].count("h") == 0 + + else: + raise AssertionError(f"Unknown ADC type {adc_type}") + + # Validate axis lengths consistency + for block in blocks: + assert matrix.axis_lengths[block] > 0 + + # Reference consistency + assert matrix.reference_state == reference_state + assert matrix.mospaces == reference_state.mospaces + + @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods) + @pytest.mark.parametrize("case", ["gen"]) # No CVS for IP/EA @pytest.mark.parametrize("system", ["h2o_sto3g"]) def test_properties(self, system: str, case: str, method: str): reference_state = testdata_cache.refstate(system=system, case=case) @@ -225,8 +358,14 @@ def test_properties(self, system: str, case: str, method: str): assert matrix.is_core_valence_separated == ("cvs" in case) # check that the blocks are correct blocks = matrix.axis_blocks + assert isinstance(blocks, list) + assert len(blocks) >= 1 if matrix.method.adc_type == "pp": assert blocks == ["ph", "pphh", "ppphhh"][:matrix.method.level // 2 + 1] + elif matrix.method.adc_type == "ip": + assert blocks == ["h", "phh", "pphhh"][:matrix.method.level // 2 + 1] + elif matrix.method.adc_type == "ea": + assert blocks == ["p", "pph", "ppphh"][:matrix.method.level // 2 + 1] else: raise NotImplementedError(f"Unknown adc type {matrix.method.adc_type}.") assert sorted(matrix.axis_spaces.keys(), key=len) == blocks @@ -268,19 +407,28 @@ def test_properties(self, system: str, case: str, method: str): assert matrix.mospaces == reference_state.mospaces assert isinstance(matrix.timer, adcc.timings.Timer) - def test_intermediates_adc2(self): + @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"]) + def test_intermediates_adc2(self, method: str): ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen")) - matrix = adcc.AdcMatrix("adc2", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) assert isinstance(matrix.intermediates, Intermediates) intermediates = Intermediates(ground_state) matrix.intermediates = intermediates assert matrix.intermediates == intermediates - def test_matvec_adc2(self): + @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"]) + def test_matvec_adc2(self, method: str): ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen")) - matrix = adcc.AdcMatrix("adc2", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) + blocks = matrix.axis_blocks - vectors = [adcc.guess_zero(matrix) for _ in range(3)] + spin_change = 0 + if matrix.method.adc_type == "ip": + spin_change = -0.5 + elif matrix.method.adc_type == "ea": + spin_change = 0.5 + + vectors = [adcc.guess_zero(matrix, spin_change=spin_change) for _ in range(3)] for vec in vectors: vec.set_random() v, w, x = vectors @@ -293,32 +441,32 @@ def test_matvec_adc2(self): # @ operator (1 vector) resv = matrix @ v diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 # @ operator (multiple vectors) resv, resw, resx = matrix @ [v, w, x] diffs = [refv - resv, refw - resw, refx - resx] for i in range(3): - assert diffs[i].ph.dot(diffs[i].ph) < 1e-12 - assert diffs[i].pphh.dot(diffs[i].pphh) < 1e-12 + assert diffs[i].get(blocks[0]).dot(diffs[i].get(blocks[0])) < 1e-12 + assert diffs[i].get(blocks[1]).dot(diffs[i].get(blocks[1])) < 1e-12 # compute matvec resv = matrix.matvec(v) diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 resv = matrix.rmatvec(v) diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 # Test apply - resv.ph = matrix.block_apply("ph_ph", v.ph) - resv.ph += matrix.block_apply("ph_pphh", v.pphh) + resv[blocks[0]] = matrix.block_apply(f"{blocks[0]}_{blocks[0]}", v.get(blocks[0])) + resv[blocks[0]] += matrix.block_apply(f"{blocks[0]}_{blocks[1]}", v.get(blocks[1])) refv = matrix.matvec(v) - diffv = resv.ph - refv.ph + diffv = resv.get(blocks[0]) - refv.get(blocks[0]) assert diffv.dot(diffv) < 1e-12 def test_extra_term(self): @@ -389,40 +537,47 @@ def apply(invec): @pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"]) +@pytest.mark.parametrize("method", ["adc3", "ip-adc3", "ea-adc3"]) class TestAdcMatrixShifted: - def construct_matrices(self, system, shift): + def construct_matrices(self, system:str, method: str, shift: float): reference_state = testdata_cache.refstate(system, case="gen") ground_state = adcc.LazyMp(reference_state) - matrix = adcc.AdcMatrix("adc3", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) shifted = AdcMatrixShifted(matrix, shift) return matrix, shifted - def test_diagonal(self, system: str): + def test_diagonal(self, system: str, method: str): shift = -0.3 - matrix, shifted = self.construct_matrices(system, shift) + matrix, shifted = self.construct_matrices(system, method, shift) - for block in ("ph", "pphh"): + for block in matrix.axis_blocks: odiag = matrix.diagonal()[block].to_ndarray() sdiag = shifted.diagonal()[block].to_ndarray() assert np.max(np.abs(sdiag - shift - odiag)) < 1e-12 - def test_matmul(self, system: str): + def test_matmul(self, system: str, method: str): shift = -0.3 - matrix, shifted = self.construct_matrices(system, shift) + matrix, shifted = self.construct_matrices(system, method, shift) + blocks = matrix.axis_blocks - vec = adcc.guess_zero(matrix) + spin_change = 0 + if matrix.method.adc_type == "ip": + spin_change = -0.5 + elif matrix.method.adc_type == "ea": + spin_change = 0.5 + + vec = adcc.guess_zero(matrix, spin_change=spin_change) vec.set_random() ores = matrix @ vec sres = shifted @ vec - assert ores.ph.describe_symmetry() == sres.ph.describe_symmetry() - assert ores.pphh.describe_symmetry() == sres.pphh.describe_symmetry() + for block in blocks: + assert ores.get(block).describe_symmetry() == sres.get( + block).describe_symmetry() - diff_s = sres.ph - ores.ph - shift * vec.ph - diff_d = sres.pphh - ores.pphh - shift * vec.pphh - assert np.max(np.abs(diff_s.to_ndarray())) < 1e-12 - assert np.max(np.abs(diff_d.to_ndarray())) < 1e-12 + diff = sres.get(block) - ores.get(block) - shift * vec.get(block) + assert np.max(np.abs(diff.to_ndarray())) < 1e-12 # TODO Test block_view, block_apply From 98a07bc0efd06cc9c0c442fbce50f1344bb641a6 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 10:32:12 +0100 Subject: [PATCH 22/28] minimal ChargedExcitation test suite --- adcc/tests/ChargedExcitations_test.py | 157 ++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 adcc/tests/ChargedExcitations_test.py diff --git a/adcc/tests/ChargedExcitations_test.py b/adcc/tests/ChargedExcitations_test.py new file mode 100644 index 000000000..41a17325b --- /dev/null +++ b/adcc/tests/ChargedExcitations_test.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +import pytest +from numpy.testing import assert_allclose + +from adcc.AdcMethod import AdcMethod +from adcc.ChargedExcitations import DetachedStates, AttachedStates + +from .testdata_cache import testdata_cache + + +# --------------------------------------------------------------------- +# Shared parametrization (reuse your existing case table if available) +# --------------------------------------------------------------------- + +cases_ip_ea = [ + ("h2o_sto3g", "ip-adc2", "gen", "doublet"), + ("h2o_sto3g", "ea-adc2", "gen", "doublet"), +] + + +# --------------------------------------------------------------------- +# 1. Basic construction + size consistency +# --------------------------------------------------------------------- + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_basic_interface(system, method, case, kind): + adc_type = AdcMethod(method).adc_type + + if adc_type == "ip": + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + assert isinstance(state, DetachedStates) + elif adc_type == "ea": + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + assert isinstance(state, AttachedStates) + else: + raise AssertionError("Unexpected ADC type") + + # size matches number of excitation vectors + assert state.size == len(state.excitation_vector) + assert state.size == len(state.excitation_energy) + + +# --------------------------------------------------------------------- +# 2. Pole strength is well-defined and matches state count +# --------------------------------------------------------------------- + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_pole_strength(system, method, case, kind): + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + + ps = state.pole_strength + + assert len(ps) == state.size + assert (ps >= 0).all() # positive pole_strenghts + + +# --------------------------------------------------------------------- +# 3. QC variable export consistency +# --------------------------------------------------------------------- + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_qcvars_export(system, method, case, kind): + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + + qcvars = state.to_qcvars(properties=False) + + adc_type = AdcMethod(method).adc_type + + if adc_type == "ip": + assert any("IONIZATION POTENTIALS" in key for key in qcvars) + elif adc_type == "ea": + assert any("ELECTRON AFFINITIES" in key for key in qcvars) + + assert any("NUMBER" in key for key in qcvars) + + +# --------------------------------------------------------------------- +# 4. describe() runs without error and contains expected wording +# --------------------------------------------------------------------- + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_describe(system, method, case, kind): + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + + adc_type = AdcMethod(method).adc_type + # Only test restricted alpha + if adc_type == "ip": + state.spin_change = -0.5 + elif adc_type == "ea": + state.spin_change = 0.5 + + desc = state.describe() + + assert state.kind in desc.lower() + + if adc_type == "ip": + assert "ionization" in desc.lower() + assert "detachment" in desc.lower() + elif adc_type == "ea": + assert "affinity" in desc.lower() + assert "attachment" in desc.lower() + + +# --------------------------------------------------------------------- +# 5. IP/EA hermiticity-style sanity check +# --------------------------------------------------------------------- +# +# Hermiticity of the ADC matrix itself belongs in: +# tests/test_adc_matrix.py +# +# NOT here. +# +# However, what *is* appropriate here: +# Energies must be real-valued. +# + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_energies_real(system, method, case, kind): + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + + assert_allclose(state.excitation_energy.imag, 0.0) From 38f06d6124f7d578c18d7bae66b55bfc085f4972 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 11:10:55 +0100 Subject: [PATCH 23/28] adapted property and state_dens tests to not perform ip/ea-adc1 tests --- adcc/tests/properties_test.py | 1 - adcc/tests/state_densities_test.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py index 50b9fb871..6713a6ebc 100644 --- a/adcc/tests/properties_test.py +++ b/adcc/tests/properties_test.py @@ -147,7 +147,6 @@ def test_pole_strengths(self, system: str, method: str, case: str, ) res_pols = state.pole_strength - print(refdata.keys()) refevals = refdata["eigenvalues"] ref_pols = refdata["pole_strengths"] diff --git a/adcc/tests/state_densities_test.py b/adcc/tests/state_densities_test.py index e61759859..64d8a72b1 100644 --- a/adcc/tests/state_densities_test.py +++ b/adcc/tests/state_densities_test.py @@ -33,7 +33,9 @@ pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"] -ip_ea_methods = [t + m for t in ["ip-", "ea-"] for m in pp_methods] +# No need to test IP/EA-ADC(1) since it is equivalent to IP/EA-ADC(0) +ip_ea_methods = [ + t + m for t in ["ip-", "ea-"] for m in ["adc0", "adc2", "adc2x", "adc3"]] generators = ["adcman", "adcc"] From 3b5d9f4f0a13c4d4dc30d4afe13cbf7f308bae5a Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 17:35:34 +0100 Subject: [PATCH 24/28] adcman now produces alpha reference data also for restricted cases so adapted generators --- adcc/tests/generators/import_qchem_data.py | 4 ++-- adcc/tests/generators/run_qchem.py | 12 ------------ adcc/tests/testdata_cache.py | 13 ------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/adcc/tests/generators/import_qchem_data.py b/adcc/tests/generators/import_qchem_data.py index 71ca35ae1..1be18b974 100644 --- a/adcc/tests/generators/import_qchem_data.py +++ b/adcc/tests/generators/import_qchem_data.py @@ -78,7 +78,7 @@ def import_excited_states(context: h5py.File, method: AdcMethod, method_name += "s" else: method_name: str = method.name.split('-')[-1] # No cvs (yet) - restricted = "rhf" in context[f"/adc_{method.adc_type}"][method_name].keys() + restricted = "rhf" in context[f"adc_{method.adc_type}"][method_name].keys() # go through the different possible state kinds and import the states. data = {} @@ -106,7 +106,7 @@ def import_excited_states(context: h5py.File, method: AdcMethod, data[kind_map.get(kind, kind)] = states elif method.adc_type in ("ip", "ea"): if restricted: - data["beta"] = {"doublet": states} + data["alpha"] = {"doublet": states} else: data[kind_map.get(kind, kind)] = {"any": states} if not data: diff --git a/adcc/tests/generators/run_qchem.py b/adcc/tests/generators/run_qchem.py index 6ce72189e..e468b8a23 100644 --- a/adcc/tests/generators/run_qchem.py +++ b/adcc/tests/generators/run_qchem.py @@ -368,14 +368,6 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s scf_options.extend(["scf_guess read", "max_scf_cycles 0"]) - # Q-Chem does not take eom_ip_alpha and beta for a restricted calculation - ip_restricted_states = ip_states[0] if ip_states[1] == 0 else 0 - if ip_restricted_states: - ip_states = (0, 0) - ea_restricted_states = ea_states[0] if ea_states[1] == 0 else 0 - if ea_restricted_states: - ea_states = (0, 0) - input = _qchem_template.format( method=method, basis=basis, @@ -384,8 +376,6 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s singlet_states=singlet_states, triplet_states=triplet_states, sf_states=sf_states, - ip_restricted_states=ip_restricted_states, - ea_restricted_states=ea_restricted_states, ip_states_alpha=ip_states[0], ip_states_beta=ip_states[1], ea_states_alpha=ea_states[0], @@ -426,8 +416,6 @@ def generate_qchem_input_file(infile: str, method: AdcMethod, basis: str, xyz: s ee_singlets {singlet_states} ee_triplets {triplet_states} sf_states {sf_states} -ip_states {ip_restricted_states} -ea_states {ea_restricted_states} eom_ip_alpha {ip_states_alpha} eom_ip_beta {ip_states_beta} eom_ea_alpha {ea_states_alpha} diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py index 0e81a6202..273f40ecc 100644 --- a/adcc/tests/testdata_cache.py +++ b/adcc/tests/testdata_cache.py @@ -159,11 +159,6 @@ def _load_data(self, system: str, method: str, case: str, source: str, f"No data available for case {case} and " f"gs_density_order {gs_density_order} in file {datafile}." ) - elif (source == "adcman" and - f"{case}/{gs_density_order}/beta" in hdf5_file): - # Little hack to load betas in case of a restricted adcman - # calculation because the default spin type is different - key = f"{case}/{gs_density_order}/beta" else: raise ValueError( f"No data available for case {case}, gs_density_order " @@ -265,10 +260,6 @@ def _make_mock_adc_state( else: raise ValueError(f"Unknown kind: {kind}") - if source == "adcman" and refstate.restricted: - # Little hack to build beta states for a restricted case - is_alpha = False - # matrix.reference_state.restricted = False spin_change = determine_spin_change(matrix.method, kind, is_alpha) n_states = len(adc_data["eigenvalues"]) @@ -284,10 +275,6 @@ def _make_mock_adc_state( adc_data["eigenvectors_doubles"][i], 1e-14 ) - # if source == "adcman" and refstate.restricted: - # # Revert changes above for restricted adcman references - # matrix.reference_state.restricted = True - if matrix.method.adc_type == "pp": return ExcitedStates(states) elif matrix.method.adc_type == "ip": From 5cdf07766a335732013abc6a551b7410211ec55c Mon Sep 17 00:00:00 2001 From: fedy9 Date: Wed, 25 Feb 2026 17:36:01 +0100 Subject: [PATCH 25/28] adapted tests, all running now --- adcc/tests/properties_test.py | 20 +++++--------------- adcc/tests/state_densities_test.py | 16 ++-------------- adcc/tests/workflow_test.py | 8 +++----- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py index 6713a6ebc..f99dc66c3 100644 --- a/adcc/tests/properties_test.py +++ b/adcc/tests/properties_test.py @@ -131,11 +131,7 @@ def test_oscillator_strengths(self, system: str, method: str, case: str, def test_pole_strengths(self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - if system.startswith("h2o"): - pytest.skip("Difficulties with data import for restricted cases") - if method.endswith("adc1"): - pytest.skip("No adcman reference data for IP/EA-ADC(1)") - elif method.endswith("adc2x"): + if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator, @@ -179,11 +175,9 @@ def test_state_dipole_moments_ip_ea(self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - if system.startswith("h2o"): - pytest.skip("Difficulties with data import for restricted cases") - if method.endswith("adc1"): - pytest.skip("No adcman reference data for IP/EA-ADC(1)") - elif method.endswith("adc2x"): + pytest.xfail("Dipole moment of charged species is gauge dependent." + " Not (yet) implemented in adcc in contrast to adcman.") + if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") if system.startswith("cn"): pytest.xfail("Faulty adcman GS dipole moments") @@ -235,11 +229,7 @@ def test_state2state_transition_dipole_moments_ip_ea( self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - if system.startswith("h2o"): - pytest.skip("Difficulties with data import for restricted cases") - if method.endswith("adc1"): - pytest.skip("No adcman reference data for IP/EA-ADC(1)") - elif method.endswith("adc2x"): + if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator, diff --git a/adcc/tests/state_densities_test.py b/adcc/tests/state_densities_test.py index 64d8a72b1..ca7eead42 100644 --- a/adcc/tests/state_densities_test.py +++ b/adcc/tests/state_densities_test.py @@ -94,11 +94,7 @@ def test_state_diffdm_pp(self, system: str, method: str, case: str, def test_state_diffdm_ip_ea(self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - if system.startswith("h2o"): - pytest.skip("Difficulties with data import for restricted cases") - if method.endswith("adc1"): - pytest.skip("No adcman reference data for IP/EA-ADC(1)") - elif method.endswith("adc2x"): + if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator, @@ -203,11 +199,7 @@ def test_state_to_state_tdm_pp(self, system: str, method: str, case: str, def test_state_to_state_tdm_ip_ea(self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - if system.startswith("h2o"): - pytest.skip("Difficulties with data import for restricted cases") - if method.endswith("adc1"): - pytest.skip("No adcman reference data for IP/EA-ADC(1)") - elif method.endswith("adc2x"): + if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") refdata = testdata_cache._load_data( @@ -218,10 +210,6 @@ def test_state_to_state_tdm_ip_ea(self, system: str, method: str, case: str, pytest.skip("Less than two states available.") s2s_data = refdata["state_to_state"] - - if generator == "adcman" and system.startswith("h2o"): - is_alpha = False - # construct a ExcitedStates instance using the eigenvalues and eigenstates # from the reference data. if AdcMethod(method).adc_type == "ip": diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py index 44fe1be34..169a5abef 100644 --- a/adcc/tests/workflow_test.py +++ b/adcc/tests/workflow_test.py @@ -438,8 +438,7 @@ def test_diagonalise_adcmatrix_ip(self): method = "ip-adc2" kind = "doublet" - refdata = testdata_cache.adcman_data(system, method=method, case=case, - is_alpha=False) + refdata = testdata_cache.adcman_data(system, method=method, case=case) ref_doublets = refdata[kind]["eigenvalues"] n_states = min(len(ref_doublets), 3) @@ -474,8 +473,7 @@ def test_diagonalise_adcmatrix_ea(self): method = "ea-adc2" kind = "doublet" - refdata = testdata_cache.adcman_data(system, method=method, case=case, - is_alpha=False) + refdata = testdata_cache.adcman_data(system, method=method, case=case) ref_doublets = refdata[kind]["eigenvalues"] n_states = min(len(ref_doublets), 3) @@ -569,7 +567,7 @@ def test_obtain_guesses_by_inspection_ip(self): # Only doubles guesses res = obtain_guesses_by_inspection(matrix2, n_guesses=4, kind="doublet", - spin_change=-0.5, is_alpha=False, + spin_change=-0.5, is_alpha=True, n_guesses_doubles=4) assert len(res) == 4 From 5d2ac4ed583c3043ac566a726b00351ec39f7230 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Thu, 26 Feb 2026 16:01:09 +0100 Subject: [PATCH 26/28] adapted new OneParticle Syntax --- adcc/adc_ea/state2state_transition_dm.py | 5 +++-- adcc/adc_ea/state_diffdm.py | 5 +++-- adcc/adc_ip/state2state_transition_dm.py | 5 +++-- adcc/adc_ip/state_diffdm.py | 5 +++-- adcc/tests/testdata_cache.py | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/adcc/adc_ea/state2state_transition_dm.py b/adcc/adc_ea/state2state_transition_dm.py index 738b9e557..0b7991e81 100644 --- a/adcc/adc_ea/state2state_transition_dm.py +++ b/adcc/adc_ea/state2state_transition_dm.py @@ -28,7 +28,8 @@ from adcc.functions import einsum from adcc.Intermediates import Intermediates from adcc.AmplitudeVector import AmplitudeVector -from adcc.OneParticleOperator import OneParticleOperator +from adcc.OneParticleDensity import OneParticleDensity +from adcc.NParticleOperator import OperatorSymmetry from .util import check_doubles_amplitudes, check_singles_amplitudes @@ -38,7 +39,7 @@ def s2s_tdm_ea_adc0(mp, amplitude_l, amplitude_r, intermediates): ul1 = amplitude_l.p ur1 = amplitude_r.p - dm = OneParticleOperator(mp, is_symmetric=False) + dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.NOSYMMETRY) dm.vv = einsum("a,b->ab", ul1, ur1) return dm diff --git a/adcc/adc_ea/state_diffdm.py b/adcc/adc_ea/state_diffdm.py index 181d10f02..160aa2fac 100644 --- a/adcc/adc_ea/state_diffdm.py +++ b/adcc/adc_ea/state_diffdm.py @@ -28,7 +28,8 @@ from adcc.functions import einsum from adcc.Intermediates import Intermediates from adcc.AmplitudeVector import AmplitudeVector -from adcc.OneParticleOperator import OneParticleOperator +from adcc.OneParticleDensity import OneParticleDensity +from adcc.NParticleOperator import OperatorSymmetry from .util import check_doubles_amplitudes, check_singles_amplitudes @@ -37,7 +38,7 @@ def diffdm_ea_adc0(mp, amplitude, intermediates): check_singles_amplitudes([b.v], amplitude) u1 = amplitude.p - dm = OneParticleOperator(mp, is_symmetric=True) + dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN) dm.vv = einsum("a,b->ab", u1, u1) return dm diff --git a/adcc/adc_ip/state2state_transition_dm.py b/adcc/adc_ip/state2state_transition_dm.py index d78878d64..0d3923958 100644 --- a/adcc/adc_ip/state2state_transition_dm.py +++ b/adcc/adc_ip/state2state_transition_dm.py @@ -28,7 +28,8 @@ from adcc.functions import einsum from adcc.Intermediates import Intermediates from adcc.AmplitudeVector import AmplitudeVector -from adcc.OneParticleOperator import OneParticleOperator +from adcc.OneParticleDensity import OneParticleDensity +from adcc.NParticleOperator import OperatorSymmetry from .util import check_doubles_amplitudes, check_singles_amplitudes @@ -38,7 +39,7 @@ def s2s_tdm_ip_adc0(mp, amplitude_l, amplitude_r, intermediates): ul1 = amplitude_l.h ur1 = amplitude_r.h - dm = OneParticleOperator(mp, is_symmetric=False) + dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.NOSYMMETRY) dm.oo = -einsum("j,i->ij", ul1, ur1) return dm diff --git a/adcc/adc_ip/state_diffdm.py b/adcc/adc_ip/state_diffdm.py index 9fab5225b..d31bfc93e 100644 --- a/adcc/adc_ip/state_diffdm.py +++ b/adcc/adc_ip/state_diffdm.py @@ -28,7 +28,8 @@ from adcc.functions import einsum from adcc.Intermediates import Intermediates from adcc.AmplitudeVector import AmplitudeVector -from adcc.OneParticleOperator import OneParticleOperator +from adcc.OneParticleDensity import OneParticleDensity +from adcc.NParticleOperator import OperatorSymmetry from .util import check_doubles_amplitudes, check_singles_amplitudes @@ -37,7 +38,7 @@ def diffdm_ip_adc0(mp, amplitude, intermediates): check_singles_amplitudes([b.o], amplitude) u1 = amplitude.h - dm = OneParticleOperator(mp, is_symmetric=True) + dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN) dm.oo = -einsum("j,i->ij", u1, u1) return dm diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py index b7da19bc2..a4679b2a4 100644 --- a/adcc/tests/testdata_cache.py +++ b/adcc/tests/testdata_cache.py @@ -312,7 +312,7 @@ def adcc_states(self, system: str, method: str, kind: str, def adcman_states(self, system: str, method: str, kind: str, case: str, gs_density_order: Optional[int] = None, - is_alpha: Optional[bool] + is_alpha: Optional[bool] = None ) -> ExcitedStates | AttachedStates | DetachedStates: """ Create an ExcitedStates/AttachedStates/DetachedStates instance for the From 6ce84088e6b67bdbd8e6ccc27f7be69b3386ec52 Mon Sep 17 00:00:00 2001 From: fedy9 Date: Thu, 5 Mar 2026 10:30:09 +0100 Subject: [PATCH 27/28] updated 1p property tests and name of pp test, all running for ip/ea adc 0-3 --- adcc/tests/adc_pp/state_diffdm_test.py | 2 +- adcc/tests/properties_test.py | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/adcc/tests/adc_pp/state_diffdm_test.py b/adcc/tests/adc_pp/state_diffdm_test.py index 24c07cd51..c96ff1c6c 100644 --- a/adcc/tests/adc_pp/state_diffdm_test.py +++ b/adcc/tests/adc_pp/state_diffdm_test.py @@ -226,7 +226,7 @@ def state_diffdm_adc3(self, mp, amplitude) -> OneParticleDensity: ).symmetrise() return dm - def test_adcn(self, method: str, system: str, case: str, kind: str): + def test_pp_adcn(self, method: str, system: str, case: str, kind: str): state = testdata_cache.adcc_states( system=system, method=method, kind=kind, case=case ) diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py index 596e3cce7..5eca29a14 100644 --- a/adcc/tests/properties_test.py +++ b/adcc/tests/properties_test.py @@ -64,14 +64,14 @@ case.kinds, AdcMethod(m).adc_type) for is_alpha in ([True] if case.restricted else [True, False]) ] -unrestricted_cases_ip_ea = [] -# (case.file_name, m, "gen", kind, is_alpha) -# for case in test_cases if not case.restricted -# for m in ip_ea_methods -# for kind in ["doublet", "any"] if kind in getattr( -# case.kinds, AdcMethod(m).adc_type) -# for is_alpha in [True, False] -# ] +unrestricted_cases_ip_ea = [ + (case.file_name, m, "gen", kind, is_alpha) + for case in test_cases if not case.restricted + for m in ip_ea_methods + for kind in ["doublet", "any"] if kind in getattr( + case.kinds, AdcMethod(m).adc_type) + for is_alpha in [True, False] +] gauge_origins = ["origin", "mass_center", "charge_center"] @@ -190,12 +190,8 @@ def test_state_dipole_moments_ip_ea(self, system: str, method: str, case: str, kind: str, is_alpha: bool, generator: str): if generator == "adcman": - pytest.xfail("Dipole moment of charged species is gauge dependent." - " Not (yet) implemented in adcc in contrast to adcman.") if method.endswith("adc2x"): pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x") - if system.startswith("cn"): - pytest.xfail("Faulty adcman GS dipole moments") refdata = testdata_cache._load_data( system=system, method=method, case=case, source=generator, is_alpha=is_alpha From 9dea173b25003767d409029ea9d17b83e7db87cc Mon Sep 17 00:00:00 2001 From: fedy9 Date: Thu, 5 Mar 2026 10:32:34 +0100 Subject: [PATCH 28/28] added 2p densities/properties for IP/EA-ADC(0-2), TODO: tests for ip/ea-adc2 fail, ip/ea-adc3 tests not implemented yet --- adcc/ChargedExcitations.py | 31 +++- adcc/adc_ea/__init__.py | 3 +- adcc/adc_ea/state_diffdm_2p.py | 202 +++++++++++++++++++++++++ adcc/adc_ip/__init__.py | 3 +- adcc/adc_ip/state_diffdm_2p.py | 194 ++++++++++++++++++++++++ adcc/tests/adc_ea/__init__.py | 0 adcc/tests/adc_ea/state_diffdm_test.py | 117 ++++++++++++++ adcc/tests/adc_ip/__init__.py | 0 adcc/tests/adc_ip/state_diffdm_test.py | 117 ++++++++++++++ 9 files changed, 658 insertions(+), 9 deletions(-) create mode 100644 adcc/adc_ea/state_diffdm_2p.py create mode 100644 adcc/adc_ip/state_diffdm_2p.py create mode 100644 adcc/tests/adc_ea/__init__.py create mode 100644 adcc/tests/adc_ea/state_diffdm_test.py create mode 100644 adcc/tests/adc_ip/__init__.py create mode 100644 adcc/tests/adc_ip/state_diffdm_test.py diff --git a/adcc/ChargedExcitations.py b/adcc/ChargedExcitations.py index 39ad211a9..35d98266a 100644 --- a/adcc/ChargedExcitations.py +++ b/adcc/ChargedExcitations.py @@ -49,7 +49,7 @@ def _pole_strength(self, state_n: int) -> np.ndarray: self.matrix.intermediates) def describe_helper(self, pole_strengths=True, state_dipole_moments=False, - block_norms=True, excitation_type="energy",): + block_norms=True, excitation_type="energy", ssq=False): """ Creates and returns the to be printed columns @@ -68,6 +68,8 @@ def describe_helper(self, pole_strengths=True, state_dipole_moments=False, excitation_type : str, optional Defines the name of the energy property. 'ionization potential'/'electron affinity' for IP/EA + ssq : bool, optional + Show the values of the excited states, by default ``False``. """ has_dipole = "electric_dipole" in self.operators.available # Collect the columns to print @@ -123,6 +125,15 @@ def describe_helper(self, pole_strengths=True, state_dipole_moments=False, unit="x(au) y(au) z(au) abs(au)" )) values.clear() + # values + if ssq and not self.reference_state.restricted: + values.extend(f"{ssq:^9.4f}" + for ssq in self.state_ssq) + columns.append(TableColumn( + header="", values=values.copy(), unit="(au)" + )) + values.clear() + return columns @@ -140,7 +151,7 @@ def __init__(self, data, is_alpha: bool, method: str = None, f"{self.method.name}") def describe(self, pole_strengths=True, state_dipole_moments=False, - block_norms=True): + block_norms=True, ssq=False): """ Return a string providing a human-readable description of the class @@ -159,8 +170,11 @@ def describe(self, pole_strengths=True, state_dipole_moments=False, assert (self.matrix.axis_blocks == ["h"] or self.matrix.axis_blocks == ["h", "phh"]) columns = self.describe_helper( - pole_strengths, state_dipole_moments, block_norms, - "ionization potential") + pole_strengths=pole_strengths, + state_dipole_moments=state_dipole_moments, + block_norms=block_norms, + excitation_type="ionization potential", + ssq=ssq) # Format the state information: kind, spin_change, # alpha/beta detachment, and convergence @@ -222,7 +236,7 @@ def __init__(self, data, is_alpha: bool, method: str = None, f"{self.method.name}") def describe(self, pole_strengths=True, state_dipole_moments=False, - block_norms=True): + block_norms=True, ssq=False): """ Return a string providing a human-readable description of the class @@ -241,8 +255,11 @@ def describe(self, pole_strengths=True, state_dipole_moments=False, assert (self.matrix.axis_blocks == ["p"] or self.matrix.axis_blocks == ["p", "pph"]) columns = self.describe_helper( - pole_strengths, state_dipole_moments, block_norms, - "electron affinity") + pole_strengths=pole_strengths, + state_dipole_moments=state_dipole_moments, + block_norms=block_norms, + excitation_type="electron affinity", + ssq=ssq) # Format the state information: kind, spin_change, # alpha/beta detachment, and convergence diff --git a/adcc/adc_ea/__init__.py b/adcc/adc_ea/__init__.py index 08c3d83b8..22f213541 100644 --- a/adcc/adc_ea/__init__.py +++ b/adcc/adc_ea/__init__.py @@ -21,6 +21,7 @@ ## ## --------------------------------------------------------------------- from .state_diffdm import state_diffdm +from .state_diffdm_2p import state_diffdm_2p from .pole_strength import pole_strength from .state2state_transition_dm import state2state_transition_dm @@ -30,5 +31,5 @@ from the high-level objects in the adcc main module. """ -__all__ = ["state_diffdm", "state2state_transition_dm", +__all__ = ["state_diffdm", "state_diffdm_2p", "state2state_transition_dm", "pole_strength"] diff --git a/adcc/adc_ea/state_diffdm_2p.py b/adcc/adc_ea/state_diffdm_2p.py new file mode 100644 index 000000000..a26b5e4b2 --- /dev/null +++ b/adcc/adc_ea/state_diffdm_2p.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.TwoParticleDensity import TwoParticleDensity +from adcc.NParticleOperator import OperatorSymmetry + +from .util import check_doubles_amplitudes, check_singles_amplitudes +from math import sqrt + + +def diffdm_ea_adc0_2p(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + u1 = amplitude.p + + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN) + + # TODO: Store intermediate? + # p1_vv = einsum("a,b->ab", u1, u1).evaluate() + + dm.ovov = ( + # N^4: O^2V^2 / N^4: O^2V^2 + + 1.0 * einsum("ab,ij->iajb", einsum("a,b->ab", u1, u1), d_oo) + ) + return dm + + +def diffdm_ea_adc1_2p(mp, amplitude, intermediates): + check_singles_amplitudes([b.v], amplitude) + u1 = amplitude.p + + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN) + + # ADC(1) diffdm + t2 = mp.t2(b.oovv) + + dm.oovv = ( + # N^4: O^2V^2 / N^4: O^2V^2 + + 2.0 * einsum("ija,b->ijab", einsum("c,ijac->ija", u1, t2), u1).antisymmetrise(2, 3) + ) + return dm + + +def diffdm_ea_adc2_2p(mp, amplitude, intermediates): + dm = diffdm_ea_adc1_2p(mp, amplitude, intermediates) # Get ADC(1) result + check_doubles_amplitudes([b.o, b.v, b.v], amplitude) + u1, u2 = amplitude.p, amplitude.pph + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + t2 = mp.t2(b.oovv) + td2 = mp.td2(b.oovv) + p0 = mp.mp2_diffdm + + dm.oooo += ( + 4.0 * ( + # N^4: O^2V^2 / N^4: O^4 + + 1.0 * einsum("il,jk->ijkl", einsum("iab,lab->il", u2, u2), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + 1.0 * einsum("ik,jl->ijkl", einsum("kmc,imc->ik", einsum("b,kmbc->kmc", u1, t2), einsum("a,imac->imc", u1, t2)), d_oo) + ).antisymmetrise(0, 1).antisymmetrise(2, 3) + # N^5: O^4V^1 / N^4: O^2V^2 + - 1.0 * einsum("klc,ijc->ijkl", einsum("b,klbc->klc", u1, t2), einsum("a,ijac->ijc", u1, t2)) + ) + dm.ooov += ( + 2.0 * ( + # N^4: O^3V^1 / N^4: O^3V^1 + + sqrt(2) * einsum("ia,jk->ijka", einsum("b,iab->ia", u1, u2), d_oo) + # N^4: O^3V^1 / N^4: O^3V^1 + + 1 * einsum("ia,jk->ijka", einsum("i,a->ia", einsum("b,ib->i", u1, p0.ov), u1), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + sqrt(2) * einsum("ja,ik->ijka", einsum("lb,jlab->ja", einsum("c,lbc->lb", u1, u2), t2), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + 1.0 / sqrt(2) * einsum("ja,ik->ijka", einsum("j,a->ja", einsum("lbc,jlbc->j", u2, t2), u1), d_oo) + ).antisymmetrise(0, 1) + # N^5: O^3V^2 / N^4: O^2V^2 + + sqrt(2) * einsum("kb,ijab->ijka", einsum("c,kbc->kb", u1, u2), t2) + # N^5: O^3V^2 / N^4: O^2V^2 + + 1.0 / sqrt(2) * einsum("ijk,a->ijka", einsum("kbc,ijbc->ijk", u2, t2), u1) + + ) + dm.oovv += ( + 2.0 * einsum("ija,b->ijab", einsum("c,ijac->ija", u1, td2), u1).antisymmetrise(2, 3) + ) + dm.ovov += ( + # N^5: O^2V^3 / N^4: O^2V^2 + - 2.0 * einsum("jac,ibc->iajb", u2, u2) + # N^4: O^2V^2 / N^4: O^2V^2 + + 1.0 * einsum("ab,ij->iajb", einsum("a,b->ab", u1, u1), p0.oo) + # N^4: O^1V^3 / N^4: O^2V^2 + + 2.0 * einsum("ab,ij->iajb", einsum("kac,kbc->ab", u2, u2), d_oo) + # N^5: O^3V^2 / N^4: O^2V^2 + + 1.0 * einsum("ikb,jka->iajb", einsum("d,ikbd->ikb", u1, t2), einsum("c,jkac->jka", u1, t2)) + # N^4: O^2V^2 / N^4: O^2V^2 + - 0.5 * einsum("ab,ij->iajb", einsum("klb,kla->ab", einsum("d,klbd->klb", u1, t2), einsum("c,klac->kla", u1, t2)), d_oo) + + 2.0 * ( + # N^5: O^3V^2 / N^4: O^2V^2 + - 1 * einsum("ijb,a->iajb", einsum("jkc,ikbc->ijb", einsum("d,jkcd->jkc", u1, t2), t2), u1) + # N^4: O^2V^2 / N^4: O^2V^2 + - 0.5 * einsum("ab,ij->iajb", einsum("b,a->ab", einsum("c,bc->b", u1, p0.vv), u1), d_oo) + ).symmetrise([(0, 2), (1, 3)]) + ) + dm.ovvv += ( + ( + # N^4: O^1V^3 / N^4: O^1V^3 + + 1 * einsum("ac,ib->iabc", einsum("a,c->ac", u1, u1), p0.ov) + # N^5: O^2V^3 / N^4: O^1V^3 + + sqrt(2) * einsum("iac,b->iabc", einsum("jad,ijcd->iac", u2, t2), u1) + ).antisymmetrise(2, 3) + # N^4: O^1V^3 / N^4: O^1V^3 + - sqrt(2) * einsum("a,ibc->iabc", u1, u2) + # N^5: O^2V^3 / N^4: O^1V^3 + + sqrt(2) * einsum("ja,ijbc->iabc", einsum("d,jad->ja", u1, u2), t2) + ) + dm.vvvv += ( + # N^5: O^1V^4 / N^4: V^4 + + 2.0 * einsum("iab,icd->abcd", u2, u2) + + 1.0 * ( + # N^4: V^4 / N^4: V^4 + + 4.0 * einsum("ac,bd->abcd", einsum("a,c->ac", u1, u1), p0.vv) + ).antisymmetrise(0, 1).antisymmetrise(2, 3) + # N^5: O^2V^3 / N^4: V^4 + + 1.0 * ( + 2.0 * einsum("bcd,a->abcd", einsum("ijb,ijcd->bcd", einsum("e,ijbe->ijb", u1, t2), t2), u1) + ).antisymmetrise(0, 1).symmetrise([(0, 2), (1, 3)]) + ) + return dm + + +# dict controlling the dispatch of the state_diffdm function +DISPATCH = { + "ea-adc0": diffdm_ea_adc0_2p, + "ea-adc1": diffdm_ea_adc1_2p, + "ea-adc2": diffdm_ea_adc2_2p, + "ea-adc2x": diffdm_ea_adc2_2p, # same as ADC(2) +} + + +def state_diffdm_2p(method, ground_state, amplitude, intermediates=None): + """ + Compute the two-particle difference density matrix of an excited state + in the MO basis. + + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude, AmplitudeVector): + raise TypeError("amplitude should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state_diffdm_2p is not implemented " + f"for {method.name}.") + else: + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret.evaluate() diff --git a/adcc/adc_ip/__init__.py b/adcc/adc_ip/__init__.py index 08c3d83b8..22f213541 100644 --- a/adcc/adc_ip/__init__.py +++ b/adcc/adc_ip/__init__.py @@ -21,6 +21,7 @@ ## ## --------------------------------------------------------------------- from .state_diffdm import state_diffdm +from .state_diffdm_2p import state_diffdm_2p from .pole_strength import pole_strength from .state2state_transition_dm import state2state_transition_dm @@ -30,5 +31,5 @@ from the high-level objects in the adcc main module. """ -__all__ = ["state_diffdm", "state2state_transition_dm", +__all__ = ["state_diffdm", "state_diffdm_2p", "state2state_transition_dm", "pole_strength"] diff --git a/adcc/adc_ip/state_diffdm_2p.py b/adcc/adc_ip/state_diffdm_2p.py new file mode 100644 index 000000000..b7385a0b3 --- /dev/null +++ b/adcc/adc_ip/state_diffdm_2p.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from adcc import block as b +from adcc.LazyMp import LazyMp +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like +from adcc.Intermediates import Intermediates +from adcc.AmplitudeVector import AmplitudeVector +from adcc.TwoParticleDensity import TwoParticleDensity +from adcc.NParticleOperator import OperatorSymmetry + +from .util import check_doubles_amplitudes, check_singles_amplitudes +from math import sqrt + + +def diffdm_ip_adc0_2p(mp, amplitude, intermediates): + check_singles_amplitudes([b.o], amplitude) + u1 = amplitude.h + + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN) + + dm.oooo = ( + + 4.0 * ( + # N^4: O^4 / N^4: O^4 + + 1 * einsum("il,jk->ijkl", einsum("i,l->il", u1, u1), d_oo) + ).antisymmetrise(0, 1).antisymmetrise(2, 3) + ) + return dm + + +def diffdm_ip_adc1_2p(mp, amplitude, intermediates): + dm = diffdm_ip_adc0_2p(mp, amplitude, intermediates) # Get ADC(0) result + u1 = amplitude.h + + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + t2 = mp.t2(b.oovv) + + dm.oovv += ( + # N^4: O^2V^2 / N^4: O^2V^2 + + 2.0 * einsum("iab,j->ijab", einsum("k,ikab->iab", u1, t2), u1).antisymmetrise(0, 1) + ) + return dm + + +def diffdm_ip_adc2_2p(mp, amplitude, intermediates): + dm = diffdm_ip_adc1_2p(mp, amplitude, intermediates) # Get ADC(1) result + check_doubles_amplitudes([b.o, b.o, b.v], amplitude) + u1, u2 = amplitude.h, amplitude.phh + hf = mp.reference_state + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + + t2 = mp.t2(b.oovv) + td2 = mp.td2(b.oovv) + p0 = mp.mp2_diffdm + + dm.oooo += ( + # N^5: O^4V^1 / N^4: O^4 + + 2.0 * einsum("ija,kla->ijkl", u2, u2) + + 4.0 * ( + # N^4: O^4 / N^4: O^4 + + 1.0 * einsum("il,jk->ijkl", einsum("i,l->il", u1, u1), p0.oo) + # N^4: O^3V^1 / N^4: O^4 + - 2.0 * einsum("ik,jl->ijkl", einsum("ima,kma->ik", u2, u2), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + 0.5 * einsum("ik,jl->ijkl", einsum("kab,iab->ik", einsum("n,knab->kab", u1, t2), einsum("m,imab->iab", u1, t2)), d_oo) + ).antisymmetrise(0, 1).antisymmetrise(2, 3) + # N^5: O^3V^2 / N^4: O^2V^2 + + 1.0 * ( + 2.0 * einsum("jkl,i->ijkl", einsum("jab,klab->jkl", einsum("m,jmab->jab", u1, t2), t2), u1) + ).antisymmetrise(0, 1).symmetrise([(0, 2), (1, 3)]) + + 1.0 * ( + # N^4: O^4 / N^4: O^4 + + 4.0 * einsum("il,jk->ijkl", einsum("l,i->il", einsum("m,lm->l", u1, p0.oo), u1), d_oo) + ).antisymmetrise(0, 1).antisymmetrise(2, 3).symmetrise([(0, 2), (1, 3)]) + ) + dm.ooov += ( + # N^4: O^3V^1 / N^4: O^3V^1 + + sqrt(2) * einsum("k,ija->ijka", u1, u2) + # N^5: O^3V^2 / N^4: O^2V^2 + - sqrt(2) * einsum("kb,ijab->ijka", einsum("l,klb->kb", u1, u2), t2) + + 2.0 * ( + # N^4: O^3V^1 / N^4: O^3V^1 + + 1.0 * einsum("jk,ia->ijka", einsum("j,k->jk", u1, u1), p0.ov) + # N^5: O^3V^2 / N^4: O^2V^2 + + sqrt(2) * einsum("ika,j->ijka", einsum("klb,ilab->ika", u2, t2), u1) + # N^4: O^3V^1 / N^4: O^3V^1 + + sqrt(2) * einsum("ja,ik->ijka", einsum("l,jla->ja", u1, u2), d_oo) + # N^4: O^3V^1 / N^4: O^3V^1 + + 1.0 * einsum("ia,jk->ijka", einsum("a,i->ia", einsum("l,la->a", u1, p0.ov), u1), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + sqrt(2) * einsum("ja,ik->ijka", einsum("mb,jmab->ja", einsum("l,lmb->mb", u1, u2), t2), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + + 0.5 * sqrt(2) * einsum("ia,jk->ijka", einsum("a,i->ia", einsum("lmb,lmab->a", u2, t2), u1), d_oo) + ).antisymmetrise(0, 1) + ) + dm.oovv += ( + # N^4: O^2V^2 / N^4: O^2V^2 + + 2.0 * einsum("iab,j->ijab", einsum("k,ikab->iab", u1, td2), u1).antisymmetrise(0, 1) + ) + dm.ovov += ( + # N^5: O^3V^2 / N^4: O^2V^2 + - 2.0 * einsum("jka,ikb->iajb", u2, u2) + # N^4: O^2V^2 / N^4: O^2V^2 + + 1.0 * einsum("ab,ij->iajb", einsum("kla,klb->ab", u2, u2), d_oo) + # N^4: O^2V^2 / N^4: O^2V^2 + - 1.0 * einsum("ij,ab->iajb", einsum("i,j->ij", u1, u1), p0.vv) + # N^5: O^2V^3 / N^4: O^2V^2 + + 1.0 * einsum("ibc,jac->iajb", einsum("l,ilbc->ibc", u1, t2), einsum("k,jkac->jac", u1, t2)) + # N^4: O^1V^3 / N^4: O^2V^2 + + 1.0 * einsum("ab,ij->iajb", einsum("lbc,lac->ab", einsum("m,lmbc->lbc", u1, t2), einsum("k,klac->lac", u1, t2)), d_oo) + # N^5: O^2V^3 / N^4: O^2V^2 + - 2.0 * einsum("jab,i->iajb", einsum("kbc,jkac->jab", einsum("l,klbc->kbc", u1, t2), t2), u1).symmetrise([(0, 2), (1, 3)]) + ) + dm.ovvv += ( + # N^5: O^2V^3 / N^4: O^1V^3 + - sqrt(2) * einsum("ja,ijbc->iabc", einsum("k,jka->ja", u1, u2), t2) + # N^5: O^2V^3 / N^4: O^1V^3 + - 0.5 * sqrt(2) * einsum("abc,i->iabc", einsum("jka,jkbc->abc", u2, t2), u1) + ) + dm.vvvv += ( + # N^5: O^1V^4 / N^4: V^4 + - 1.0 * einsum("kcd,kab->abcd", einsum("j,jkcd->kcd", u1, t2), einsum("i,ikab->kab", u1, t2)) + ) + return dm + + +# dict controlling the dispatch of the state_diffdm function +DISPATCH = { + "ip-adc0": diffdm_ip_adc0_2p, + "ip-adc1": diffdm_ip_adc1_2p, + "ip-adc2": diffdm_ip_adc2_2p, + "ip-adc2x": diffdm_ip_adc2_2p, # same as ADC(2) +} + + +def state_diffdm_2p(method, ground_state, amplitude, intermediates=None): + """ + Compute the two-particle difference density matrix of an excited state + in the MO basis. + + Parameters + ---------- + method : str, AdcMethod + The method to use for the computation (e.g. "adc2") + ground_state : LazyMp + The ground state upon which the excitation was based + amplitude : AmplitudeVector + The amplitude vector + intermediates : adcc.Intermediates + Intermediates from the ADC calculation to reuse + """ + if not isinstance(method, AdcMethod): + method = AdcMethod(method) + if not isinstance(ground_state, LazyMp): + raise TypeError("ground_state should be a LazyMp object.") + if not isinstance(amplitude, AmplitudeVector): + raise TypeError("amplitude should be an AmplitudeVector object.") + if intermediates is None: + intermediates = Intermediates(ground_state) + + if method.name not in DISPATCH: + raise NotImplementedError("state_diffdm_2p is not implemented " + f"for {method.name}.") + else: + ret = DISPATCH[method.name](ground_state, amplitude, intermediates) + return ret.evaluate() diff --git a/adcc/tests/adc_ea/__init__.py b/adcc/tests/adc_ea/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adcc/tests/adc_ea/state_diffdm_test.py b/adcc/tests/adc_ea/state_diffdm_test.py new file mode 100644 index 000000000..f6d8299d8 --- /dev/null +++ b/adcc/tests/adc_ea/state_diffdm_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +import numpy as np +import pytest +from math import sqrt + +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like +from adcc.adc_ea.state_diffdm_2p import state_diffdm_2p +from adcc.adc_ea.state_diffdm import state_diffdm +from adcc.MoSpaces import split_spaces + +from .. import testcases +from ..testdata_cache import testdata_cache + + +test_cases = testcases.get_by_filename("h2o_sto3g", "cn_sto3g") +cases = [(case.file_name, c, kind, is_alpha) + for case in test_cases for c in ["gen"] for kind in case.kinds.ea + for is_alpha in ([True] if case.restricted else [True, False])] +methods = ["ea-adc0", "ea-adc2"] # TODO: Test diffdm_ea_adc2_2p + + +@pytest.mark.parametrize("method", methods) +@pytest.mark.parametrize("system,case,kind,is_alpha", cases) +class TestStateDiffDm: + def calculate_adcn_electron_affinity(self, state): + hf = state.reference_state + mp = state.ground_state + n_states = len(state.excitation_energy) + excitation_energy = np.zeros((n_states)) + method = state.method + level = method.level + + method_order_minus_one = None + if level - 1 >= 0: + method_order_minus_one = AdcMethod("ea-adc" + str(level - 1)) + + for ea in range(n_states): + evec = state.excitation_vector[ea] + # TODO switch to ISR(3) implemntation + if method.level == 3: + raise NotImplementedError( + "State density not implemented for EA-ADC(3)") + else: + dens_1p = state_diffdm(method, mp, evec) + + # one particle + # fock operator part + excitation_energy[ea] = einsum("pq,pq", hf.foo, dens_1p.oo) + excitation_energy[ea] += einsum("pq,pq", hf.fvv, dens_1p.vv) + + if method_order_minus_one is not None: + # two particle part + dens_2p = state_diffdm_2p(method_order_minus_one, mp, evec) + # go for ISR(1)-d for ADC(2) + if method_order_minus_one.level == 1: + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + dens_2p.ooov += ( + + sqrt(2) * einsum("ia,jk->ijka", einsum("b,iab->ia", evec.p, evec.pph), d_oo) + ) + dens_2p.ovvv += ( + - sqrt(2) * einsum("a,ibc->iabc", evec.p, evec.pph) + ) + for block in dens_2p.blocks: + # compute + # 1/4 [(1 - P_pq) (1 - P_rs) 1 / (n_occ - 1) delta_qs] + # * D^pq_rs + # = 1 / (n_occ - 1) D^pq_rq + s1, s2, s3, s4 = split_spaces(block) + if s2 == s4: + eri_1p = np.einsum( + "piqi->pq", hf.eri(f"{s1}o1{s3}o1").to_ndarray() + ) + n_occ = hf.foo.shape[1] + excitation_energy[ea] -= 1 / (n_occ - 1) * np.einsum( + "pr,pqrq->", + eri_1p, + dens_2p[block].to_ndarray() + ) + # and the full 2e part + excitation_energy[ea] += 0.25 * einsum( + "pqrs,pqrs", dens_2p[block], hf.eri(block) + ) + return excitation_energy + + + def test_ea_adcn(self, method: str, system: str, case: str, kind: str, + is_alpha: bool): + state = testdata_cache.adcc_states( + system=system, method=method, kind=kind, case=case, + is_alpha=is_alpha + ) + ref = state.excitation_energy_uncorrected + adcn = self.calculate_adcn_electron_affinity(state) + np.testing.assert_allclose(adcn, ref, atol=1e-12) diff --git a/adcc/tests/adc_ip/__init__.py b/adcc/tests/adc_ip/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adcc/tests/adc_ip/state_diffdm_test.py b/adcc/tests/adc_ip/state_diffdm_test.py new file mode 100644 index 000000000..286d6e73d --- /dev/null +++ b/adcc/tests/adc_ip/state_diffdm_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +import numpy as np +import pytest +from math import sqrt + +from adcc.AdcMethod import AdcMethod +from adcc.functions import einsum, zeros_like +from adcc.adc_ip.state_diffdm_2p import state_diffdm_2p +from adcc.adc_ip.state_diffdm import state_diffdm +from adcc.MoSpaces import split_spaces + +from .. import testcases +from ..testdata_cache import testdata_cache + + +test_cases = testcases.get_by_filename("h2o_sto3g", "cn_sto3g") +cases = [(case.file_name, c, kind, is_alpha) + for case in test_cases for c in ["gen"] for kind in case.kinds.ip + for is_alpha in ([True] if case.restricted else [True, False])] +methods = ["ip-adc0", "ip-adc2"] # TODO: Test diffdm_ip_adc2_2p + + +@pytest.mark.parametrize("method", methods) +@pytest.mark.parametrize("system,case,kind,is_alpha", cases) +class TestStateDiffDm: + def calculate_adcn_ionization_potential(self, state): + hf = state.reference_state + mp = state.ground_state + n_states = len(state.excitation_energy) + excitation_energy = np.zeros((n_states)) + method = state.method + level = method.level + + method_order_minus_one = None + if level - 1 >= 0: + method_order_minus_one = AdcMethod("ip-adc" + str(level - 1)) + + for ip in range(n_states): + evec = state.excitation_vector[ip] + # TODO switch to ISR(3) implemntation + if method.level == 3: + raise NotImplementedError( + "State density not implemented for IP-ADC(3)") + else: + dens_1p = state_diffdm(method, mp, evec) + + # one particle + # fock operator part + excitation_energy[ip] = einsum("pq,pq", hf.foo, dens_1p.oo) + excitation_energy[ip] += einsum("pq,pq", hf.fvv, dens_1p.vv) + + if method_order_minus_one is not None: + # two particle part + dens_2p = state_diffdm_2p(method_order_minus_one, mp, evec) + # go for ISR(1)-d for ADC(2) + if method_order_minus_one.level == 1: + d_oo = zeros_like(hf.foo) + d_oo.set_mask("ii", 1) + dens_2p.ooov += ( + + sqrt(2) * einsum("k,ija->ijka", evec.h, evec.phh) + + sqrt(2) * einsum( + "ja,ik->ijka", einsum( + "l,jla->ja", evec.h, evec.phh), d_oo) + ) + for block in dens_2p.blocks: + # compute + # 1/4 [(1 - P_pq) (1 - P_rs) 1 / (n_occ - 1) delta_qs] + # * D^pq_rs + # = 1 / (n_occ - 1) D^pq_rq + s1, s2, s3, s4 = split_spaces(block) + if s2 == s4: + eri_1p = np.einsum( + "piqi->pq", hf.eri(f"{s1}o1{s3}o1").to_ndarray() + ) + n_occ = hf.foo.shape[1] + excitation_energy[ip] -= 1 / (n_occ - 1) * np.einsum( + "pr,pqrq->", + eri_1p, + dens_2p[block].to_ndarray() + ) + # and the full 2e part + excitation_energy[ip] += 0.25 * einsum( + "pqrs,pqrs", dens_2p[block], hf.eri(block) + ) + return excitation_energy + + + def test_ip_adcn(self, method: str, system: str, case: str, kind: str, + is_alpha: bool): + state = testdata_cache.adcc_states( + system=system, method=method, kind=kind, case=case, + is_alpha=is_alpha + ) + ref = state.excitation_energy_uncorrected + adcn = self.calculate_adcn_ionization_potential(state) + np.testing.assert_allclose(adcn, ref, atol=1e-12)