diff --git a/adcc/AdcMatrix.py b/adcc/AdcMatrix.py
index 7b6c5974..a0643881 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, Method, AdcType
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},
"isr1s": {"ph_ph": 1, "ph_pphh": None, "pphh_ph": None, "pphh_pphh": None},
"isr2d": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 0},
}
@@ -104,7 +108,9 @@ def _default_block_orders(cls, method: Method,
# - determine which spaces are available in the ADC(n) matrix
# starting from the given minimal space
min_space = {
- AdcType.PP: "ph"
+ AdcType.PP: "ph",
+ AdcType.IP: "h",
+ AdcType.EA: "p",
}.get(method.adc_type, None)
if min_space is None:
raise ValueError(f"Unknown adc type {method.adc_type.to_str()} for "
@@ -196,6 +202,10 @@ def _is_valid_space(cls, space: str, method: Method) -> bool:
# be equal or differ e.g. by +-1 (IP/EA)
if method.adc_type is AdcType.PP:
return n_particle == n_hole
+ elif method.adc_type is AdcType.IP:
+ return n_particle == n_hole - 1
+ elif method.adc_type is AdcType.EA:
+ return n_particle == n_hole + 1
raise ValueError(f"Unknown adc type {method.adc_type.to_str()} for method "
f"{method.name}. Can not validate space.")
@@ -268,11 +278,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 = {
+ AdcType.PP: ppmatrix.block,
+ AdcType.IP: ipmatrix.block,
+ AdcType.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,
- variant=variant)
- for block, order in self.block_orders.items() if order is not None
+ 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
}
self.blocks = {bl: blocks[bl].apply for bl in blocks}
if diagonal_precomputed:
@@ -442,33 +460,57 @@ 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)
- # doubles part is antisymmetric wrt. (i,j,a,b) <-> (j,i,a,b)
- scratch = invec.antisymmetrise([(0, 1)]).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
-
- def symmetrise_generic_adc_triples(invec):
- # triples part is antisymmetric wrt. permutations of (i,j,k)
- # and wrt. permutations of (a,b,c)
- scratch = (
- invec.antisymmetrise([(0, 1, 2)]).antisymmetrise([(3, 4, 5)])
- )
- # triples part is symmetric wrt. permutations of (i,j,k) and
- # permutations of (a,b,c)
- # NOTE: The doubles fix the (numerical) symmetry with a single
- # symmetrise call. This is not possible for triples, since the
- # following symmetrise call only covers 6 of the 18
- # even permutations generated by the 2 antisymmetrise calls above:
- # ijkabc + ikjacb + jikbac + jkibca + kijcab + kjicba
- return scratch.symmetrise([(0, 1, 2), (3, 4, 5)])
- ret["ppphhh"] = symmetrise_generic_adc_triples
+ if self.method.adc_type is AdcType.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)
+ # doubles part is antisymmetric wrt. (i,j,a,b) <-> (j,i,a,b)
+ scratch = invec.antisymmetrise([(0, 1)]).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
+
+ def symmetrise_generic_adc_triples(invec):
+ # triples part is antisymmetric wrt. permutations of (i,j,k)
+ # and wrt. permutations of (a,b,c)
+ scratch = (
+ invec.antisymmetrise([(0, 1, 2)]).antisymmetrise(
+ [(3, 4, 5)])
+ )
+ # triples part is symmetric wrt. permutations of (i,j,k) and
+ # permutations of (a,b,c)
+ # NOTE: The doubles fix the (numerical) symmetry with a single
+ # symmetrise call. This is not possible for triples, since the
+ # following symmetrise call only covers 6 of the 18
+ # even permutations generated by the 2 antisymmetrise calls
+ # above:
+ # ijkabc + ikjacb + jikbac + jkibca + kijcab + kjicba
+ return scratch.symmetrise([(0, 1, 2), (3, 4, 5)])
+ ret["ppphhh"] = symmetrise_generic_adc_triples
+ elif self.method.adc_type is AdcType.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
+
+ def symmetrise_generic_adc_triples(invec):
+ # TODO
+ pass
+ elif self.method.adc_type is AdcType.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
+
+ def symmetrise_generic_adc_triples(invec):
+ # TODO
+ pass
return ret
def dense_basis(self, axis_blocks=None, ordering="adcc"):
diff --git a/adcc/AdcMethod.py b/adcc/AdcMethod.py
index df14e6b5..c3257a3c 100644
--- a/adcc/AdcMethod.py
+++ b/adcc/AdcMethod.py
@@ -70,6 +70,8 @@ def to_int(self) -> int:
class AdcType(Enum):
PP = "pp"
+ IP = "ip"
+ EA = "ea"
def to_str(self) -> str:
return self.value
@@ -253,7 +255,7 @@ def at_level(self: T, newlevel: Union[int, str]) -> T:
def as_method(self, method_cls: type[T]) -> T:
"""
- Return a equivalent Method with the method base name replaced
+ Return an equivalent Method with the method base name replaced
by the provided name.
"""
assert self._method_base_name is not None
@@ -294,7 +296,23 @@ class AdcMethod(Method):
): LevelSpec(
max_level=3,
special_levels=(MethodLevel.TWO_X,)
- )
+ ),
+ LevelKey(
+ adc_type=AdcType.IP,
+ gs_type=GroundStateType.MP,
+ cvs=False
+ ): LevelSpec(
+ max_level=3,
+ special_levels=(MethodLevel.TWO_X,)
+ ),
+ LevelKey(
+ adc_type=AdcType.EA,
+ gs_type=GroundStateType.MP,
+ cvs=False
+ ): LevelSpec(
+ max_level=3,
+ special_levels=(MethodLevel.TWO_X,)
+ ),
}
@@ -316,5 +334,21 @@ class IsrMethod(Method):
): LevelSpec(
max_level=2,
special_levels=(MethodLevel.ONE_S, MethodLevel.TWO_D)
+ ),
+ LevelKey(
+ adc_type=AdcType.IP,
+ gs_type=GroundStateType.MP,
+ cvs=False
+ ): LevelSpec(
+ max_level=2,
+ special_levels=None
+ ),
+ LevelKey(
+ adc_type=AdcType.EA,
+ gs_type=GroundStateType.MP,
+ cvs=False
+ ): LevelSpec(
+ max_level=2,
+ special_levels=None
)
}
diff --git a/adcc/AmplitudeVector.py b/adcc/AmplitudeVector.py
index aff573b6..b08adad2 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/ChargedExcitations.py b/adcc/ChargedExcitations.py
new file mode 100644
index 00000000..1178cbf7
--- /dev/null
+++ b/adcc/ChargedExcitations.py
@@ -0,0 +1,191 @@
+#!/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
+from scipy import constants
+
+from .import adc_ip, adc_ea
+from .ElectronicStates import TableColumn, ElectronicStates, _timer_name
+from .functions import dot
+from .misc import cached_member_function
+from .AdcMethod import AdcType
+
+
+class ChargedExcitation(ElectronicStates):
+
+ @property
+ def pole_strength(self) -> np.ndarray:
+ """Array of pole strengths of all computed states"""
+ pass
+
+ @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"""
+ pass
+
+ def _describe_helper(self, block_norms=True, excitation_type_name="energy"):
+ """
+ Creates and returns the to be printed columns
+
+ Parameters
+ ----------
+ 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_name : str, optional
+ Defines the name of the energy property.
+ 'ionization potential'/'electron affinity' for IP/EA
+ """
+ # 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_name, values=values.copy(),
+ unit="(au) (eV)"
+ ))
+ 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()
+
+ 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 is not AdcType.IP:
+ raise ValueError("DetachedStates computes excited state properties"
+ " for IP-ADC. Got the non-IP-ADC method "
+ f"{self.method.name}")
+
+ def describe(self, block_norms=True):
+ """
+ Return a string providing a human-readable description of the class
+
+ Parameters
+ ----------
+ 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(
+ block_norms=block_norms,
+ excitation_type_name="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}), {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)
+
+
+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 is not AdcType.EA:
+ raise ValueError("DetachedStates computes excited state properties"
+ " for EA-ADC. Got the non-EA-ADC method "
+ f"{self.method.name}")
+
+ def describe(self, 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(
+ block_norms=block_norms,
+ excitation_type_name="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}), {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)
diff --git a/adcc/ElectronicStates.py b/adcc/ElectronicStates.py
index 417daf88..fd5d4b31 100644
--- a/adcc/ElectronicStates.py
+++ b/adcc/ElectronicStates.py
@@ -72,7 +72,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))
@@ -112,7 +113,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
@@ -460,7 +461,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
@@ -486,25 +487,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}"
@@ -767,6 +754,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 fae6c727..757d019b 100644
--- a/adcc/ExcitedStates.py
+++ b/adcc/ExcitedStates.py
@@ -155,7 +155,7 @@ def describe(self, oscillator_strengths=True, rotatory_strengths=False,
unit="x(au) y(au) z(au) abs(au)"
))
values.clear()
- values.clear()
+ # values
if ssq and not self.reference_state.restricted:
values.extend(f"{ssq:^9.4f}"
for ssq in self.state_ssq)
@@ -163,7 +163,24 @@ def describe(self, oscillator_strengths=True, rotatory_strengths=False,
header="", values=values.copy(), unit="(au)"
))
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/__init__.py b/adcc/__init__.py
index 67208921..8283e053 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
@@ -49,8 +51,9 @@
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 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
@@ -61,13 +64,17 @@
"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", "OneParticleDensity",
"TwoParticleOperator", "TwoParticleDensity", "OperatorSymmetry",
"guesses_singlet", "guesses_triplet", "guesses_any",
- "guess_symmetries", "guesses_spin_flip", "guess_zero", "LazyMp",
+ "guesses_doublet", "guess_symmetries", "guesses_spin_flip",
+ "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.18.0"
@@ -146,6 +153,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 00000000..e69de29b
diff --git a/adcc/adc_ea/matrix.py b/adcc/adc_ea/matrix.py
new file mode 100644
index 00000000..ac2fcd93
--- /dev/null
+++ b/adcc/adc_ea/matrix.py
@@ -0,0 +1,206 @@
+#!/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 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
+
+#
+# An explanation of the dispatch structure can be found 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 (p, pph 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):
+ 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):
+ 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):
+ # Same as ADC(0)
+ return block_p_p_0(hf, mp, intermediates)
+
+
+def diagonal_pph_pph_1(hf):
+ return AmplitudeVector(pph=(
+ + 2.0 * direct_sum(
+ "-ia+b->iab", einsum("iaia->ia", hf.ovov), hf.fvv.diagonal())
+ + 1.0 * direct_sum(
+ "-i+ab->iab", hf.foo.diagonal(), einsum("abab->ab", hf.vvvv))
+ ).symmetrise(1, 2))
+
+
+def block_pph_pph_1(hf, mp, intermediates):
+ def apply(ampl):
+ return AmplitudeVector(pph=(
+ - einsum("jab,ij->iab", ampl.pph, hf.foo)
+ + 2.0 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2)
+ + 0.5 * einsum("abcd,icd->iab", hf.vvvv, ampl.pph)
+ - 2.0 * einsum("icka,kcb->iab", hf.ovov, ampl.pph).antisymmetrise(1, 2)
+ ))
+ return AdcBlock(apply, diagonal_pph_pph_1(hf))
+
+
+#
+# 1st order coupling
+#
+def block_p_pph_1(hf, mp, intermediates):
+ 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):
+ 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):
+ # 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):
+ # 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):
+ # 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):
+ 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_inf_vv
+ )
+
+
+@register_as_intermediate
+def sigma_inf_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/util.py b/adcc/adc_ea/util.py
new file mode 100644
index 00000000..b1409749
--- /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) 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 .
+##
+## ---------------------------------------------------------------------
+
+
+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 00000000..e69de29b
diff --git a/adcc/adc_ip/matrix.py b/adcc/adc_ip/matrix.py
new file mode 100644
index 00000000..74ca2814
--- /dev/null
+++ b/adcc/adc_ip/matrix.py
@@ -0,0 +1,210 @@
+#!/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 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
+
+#
+# An explanation of the dispatch structure can be found 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 (h, phh 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):
+ 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):
+ 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):
+ # Same as ADC(0)
+ return block_h_h_0(hf, mp, intermediates)
+
+
+def diagonal_phh_phh_1(hf):
+ return AmplitudeVector(phh=(
+ - 2.0 * direct_sum(
+ "i+ja->ija", hf.foo.diagonal(), einsum("iaia->ia", hf.ovov))
+ + 1.0 * direct_sum(
+ "ij+a->ija", einsum("ijij->ij", hf.oooo), hf.fvv.diagonal())
+ ).symmetrise(0, 1))
+
+
+def block_phh_phh_1(hf, mp, intermediates):
+ def apply(ampl):
+ return AmplitudeVector(phh=(
+ + einsum("ac,ijc->ija", hf.fvv, ampl.phh)
+ - 2.0 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1)
+ + 0.5 * einsum("ijkl,kla->ija", hf.oooo, ampl.phh)
+ - 2.0 * 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):
+ 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):
+ 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):
+ # 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_inf_oo
+ )
+
+
+@register_as_intermediate
+def sigma_inf_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/util.py b/adcc/adc_ip/util.py
new file mode 100644
index 00000000..3ef04258
--- /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) 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 .
+##
+## ---------------------------------------------------------------------
+
+
+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 f516d705..56085fe2 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 f5f0ae96..b5a9d52f 100644
--- a/adcc/block.py
+++ b/adcc/block.py
@@ -176,3 +176,24 @@ def invert_transpose_tuple(p: tuple[int, ...]) -> tuple[int, ...]:
factor *= -1
transpose = invert_transpose_tuple(transpose)
return (canonical_block, factor, transpose)
+
+
+def get_block_name(spaces, order, variant, has_core_occupied_space):
+ """
+ Assembles name of block function with variant, spaces, and order.
+ """
+ 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/guess/__init__.py b/adcc/guess/__init__.py
index b2690876..05c5e937 100644
--- a/adcc/guess/__init__.py
+++ b/adcc/guess/__init__.py
@@ -22,25 +22,30 @@
## ---------------------------------------------------------------------
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
+from ..AdcMethod import AdcType
__all__ = ["guess_zero", "guesses_from_diagonal",
+ "get_spin_block_symmetrisation", "guesses_doublet",
"guesses_singlet", "guesses_triplet", "guesses_any",
- "guesses_spin_flip", "guess_symmetries"]
+ "guesses_spin_flip", "guess_symmetries",
+ "estimate_n_guesses", "determine_spin_change"]
-def guess_kwargs_kind(kind):
+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 = 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),
- )
+ symmetrisation = {
+ "singlet": "symmetric",
+ "doublet": "none",
+ "triplet": "antisymmetric",
+ "spin_flip": "none",
+ "any": "none"
+ }
try:
- return kwargsmap[kind]
+ return symmetrisation[kind]
except KeyError:
raise ValueError(f"Kind not known: {kind}")
@@ -50,15 +55,46 @@ 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.
+ 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)
+ return guesses_from_diagonal(
+ 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=True, **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.
+ """
+ if matrix.method.adc_type is AdcType.IP:
+ spin_change = -0.5
+ elif matrix.method.adc_type is AdcType.EA:
+ spin_change = 0.5
+ else:
+ raise ValueError(f"Invalid doublet ADC type: {matrix.method.adc_type}")
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block,
+ is_alpha=is_alpha, spin_change=spin_change,
+ spin_block_symmetrisation=get_spin_block_symmetrisation("doublet"),
+ **kwargs
+ )
def guesses_triplet(matrix, n_guesses, block="ph", **kwargs):
@@ -66,18 +102,21 @@ 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.
+ 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)
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_change=0,
+ spin_block_symmetrisation=get_spin_block_symmetrisation("triplet"),
+ **kwargs
+ )
-# guesses for computing any state (singlet or triplet)
+# guesses for computing any state (excluding spin-flip states)
guesses_any = guesses_from_diagonal
@@ -86,12 +125,15 @@ 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.
+ 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)
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_change=-1,
+ spin_block_symmetrisation=get_spin_block_symmetrisation("spin_flip"),
+ **kwargs
+ )
diff --git a/adcc/guess/guess_zero.py b/adcc/guess/guess_zero.py
index 6378c7fe..653e5eab 100644
--- a/adcc/guess/guess_zero.py
+++ b/adcc/guess/guess_zero.py
@@ -23,6 +23,7 @@
from adcc import AmplitudeVector, Symmetry, Tensor
from ..AdcMatrix import AdcMatrixlike
+from ..AdcMethod import AdcType
def guess_zero(matrix, spin_change=0, spin_block_symmetrisation="none"):
@@ -83,28 +84,25 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"):
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 "ppphhh" in matrix.axis_blocks:
- max_spin_change = 3
- if spin_change > max_spin_change:
- raise ValueError("spin_change for singles guesses may only be in the "
- f"range [{-max_spin_change}, {max_spin_change}] and "
- f"not {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 spin_change not in valid_spin_changes:
+ raise ValueError("spin_change may only be one of "
+ f"{valid_spin_changes}, and not {spin_change}.")
symmetries = {}
- if "ph" in matrix.axis_blocks:
- symmetries["ph"] = guess_symmetry_singles(
- matrix, spin_change=spin_change,
- spin_block_symmetrisation=spin_block_symmetrisation
- )
- if "pphh" in matrix.axis_blocks:
- symmetries["pphh"] = guess_symmetry_doubles(
+ singles = matrix.axis_blocks[0]
+ symmetries[singles] = guess_symmetry_singles(
+ matrix, spin_change=spin_change,
+ spin_block_symmetrisation=spin_block_symmetrisation, block=singles
+ )
+ if len(matrix.axis_blocks) >= 2:
+ doubles = matrix.axis_blocks[1]
+ symmetries[doubles] = guess_symmetry_doubles(
matrix, spin_change=spin_change,
- spin_block_symmetrisation=spin_block_symmetrisation
+ spin_block_symmetrisation=spin_block_symmetrisation, block=doubles
)
if "ppphhh" in matrix.axis_blocks:
symmetries["ppphhh"] = guess_symmetry_triples(
@@ -115,24 +113,36 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"):
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 is not AdcType.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"]
@@ -140,34 +150,82 @@ 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)]
+ if matrix.method.adc_type is AdcType.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
+ # 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 is AdcType.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
+
+ elif matrix.method.adc_type is AdcType.EA:
+ # 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
- # 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
return symmetry
diff --git a/adcc/guess/guesses_from_diagonal.py b/adcc/guess/guesses_from_diagonal.py
index 2d94f972..83aa78ca 100644
--- a/adcc/guess/guesses_from_diagonal.py
+++ b/adcc/guess/guesses_from_diagonal.py
@@ -28,10 +28,12 @@
from itertools import groupby
from ..AdcMatrix import AdcMatrixlike
+from ..AdcMethod import AdcType
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 +47,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 +79,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 +88,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 +133,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 +203,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 +223,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 +252,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 +278,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 +291,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 is AdcType.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 = {AdcType.IP: libadcc.fill_ip_doubles_guesses,
+ AdcType.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/guess/util.py b/adcc/guess/util.py
new file mode 100644
index 00000000..9ddf59a7
--- /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) 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 ..AdcMethod import AdcMethod, AdcType
+from typing import Optional
+
+
+def determine_spin_change(method: AdcMethod, kind: str,
+ is_alpha: Optional[bool] = None) -> float:
+ if method.adc_type is AdcType.PP:
+ if kind == "spin_flip":
+ return -1.0
+ else:
+ return 0.0
+ elif method.adc_type is AdcType.IP:
+ 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 is AdcType.EA:
+ 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 type: {method.adc_type}")
+
+
+def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2,
+ 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.
+ 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.to_int() < 2
+ and matrix.method.adc_type is not AdcType.PP
+ ):
+ # Adjustment for IP- and EA-ADC(0/1) calculations
+ estimate = (n_occ_a if matrix.method.adc_type is AdcType.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)
diff --git a/adcc/projection.py b/adcc/projection.py
index d6eee258..a25fa5fc 100644
--- a/adcc/projection.py
+++ b/adcc/projection.py
@@ -26,7 +26,8 @@
import numpy as np
-from .guess import guess_kwargs_kind, 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,
- **guess_kwargs_kind(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,
- **guess_kwargs_kind(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
diff --git a/adcc/solver/explicit_symmetrisation.py b/adcc/solver/explicit_symmetrisation.py
index d49e55bf..10ff0a31 100644
--- a/adcc/solver/explicit_symmetrisation.py
+++ b/adcc/solver/explicit_symmetrisation.py
@@ -24,6 +24,7 @@
from adcc import evaluate
from adcc.AmplitudeVector import AmplitudeVector
+from adcc.AdcMethod import AdcType
# TODO
# This interface is not that great and leads to duplicate information
@@ -76,6 +77,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 is AdcType.IP
def symmetrise(self, new_vectors):
if isinstance(new_vectors, AmplitudeVector):
@@ -87,12 +90,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/AdcMatrix_test.py b/adcc/tests/AdcMatrix_test.py
index 6525529d..d12ba582 100644
--- a/adcc/tests/AdcMatrix_test.py
+++ b/adcc/tests/AdcMatrix_test.py
@@ -124,7 +124,7 @@ def test_default_block_orders(self):
}
assert block_orders == ref_isr2d
- 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
@@ -149,7 +149,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")
@@ -171,15 +171,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", "adc4"]
+pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"]
+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:
@@ -241,16 +291,20 @@ def test_matvec(self, system: str, case: str, method: str):
if matrix.reference_state.restricted:
if matrix.method.adc_type is AdcType.PP:
kind = "singlet"
+ elif matrix.method.adc_type in (AdcType.IP, AdcType.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)
if "matvec_triples" in matdata:
assert_allclose(matdata["matvec_triples"], result.ppphhh.to_ndarray(),
@@ -266,6 +320,8 @@ def test_compute_block(self, system: str, case: str, method: str):
if matrix.reference_state.restricted:
if matrix.method.adc_type is AdcType.PP:
kind = "singlet"
+ elif matrix.method.adc_type in (AdcType.IP, AdcType.EA):
+ kind = "doublet"
else:
raise ValueError(f"Unknwon adc type {matrix.method.adc_type}.")
else:
@@ -284,10 +340,91 @@ def test_compute_block(self, system: str, case: str, method: str):
atol=1e-12
)
+ def test_hermiticity(self, system, case, method):
+ if "cvs" in case and method == "adc4":
+ pytest.skip("CVS-ADC(4) not implemented")
+ 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 is AdcType.IP:
+ spin_change = -0.5
+ elif matrix.method.adc_type is AdcType.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) < 1.5e-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 is AdcType.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 is AdcType.IP:
+ # First block must remove one electron
+ assert blocks[0].count("h") == 1
+ assert blocks[0].count("p") == 0
+
+ elif adc_type is AdcType.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):
if "cvs" in case and method == "adc4":
@@ -302,10 +439,20 @@ 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 is AdcType.PP:
assert blocks == (
["ph", "pphh", "ppphhh"][:matrix.method.level.to_int() // 2 + 1]
)
+ elif matrix.method.adc_type is AdcType.IP:
+ assert blocks == (
+ ["h", "phh", "pphhh"][:matrix.method.level.to_int() // 2 + 1]
+ )
+ elif matrix.method.adc_type is AdcType.EA:
+ assert blocks == (
+ ["p", "pph", "ppphh"][:matrix.method.level.to_int() // 2 + 1]
+ )
else:
raise NotImplementedError(f"Unknown adc type {matrix.method.adc_type}.")
assert sorted(matrix.axis_spaces.keys(), key=len) == blocks
@@ -347,19 +494,29 @@ 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 is AdcType.IP:
+ spin_change = -0.5
+ elif matrix.method.adc_type is AdcType.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
@@ -372,32 +529,34 @@ 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):
@@ -468,40 +627,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 is AdcType.IP:
+ spin_change = -0.5
+ elif matrix.method.adc_type is AdcType.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
diff --git a/adcc/tests/ChargedExcitations_test.py b/adcc/tests/ChargedExcitations_test.py
new file mode 100644
index 00000000..16d149d8
--- /dev/null
+++ b/adcc/tests/ChargedExcitations_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+import pytest
+
+from adcc.AdcMethod import AdcMethod, AdcType
+from adcc.ChargedExcitations import DetachedStates, AttachedStates
+
+from .testdata_cache import testdata_cache
+
+
+cases_ip_ea = [
+ ("h2o_sto3g", "ip-adc2", "gen", "doublet"),
+ ("h2o_sto3g", "ea-adc2", "gen", "doublet"),
+]
+
+
+@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 is AdcType.IP:
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+ assert isinstance(state, DetachedStates)
+ elif adc_type is AdcType.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)
diff --git a/adcc/tests/data/SHA256SUMS b/adcc/tests/data/SHA256SUMS
index 180ec316..22e907bd 100644
--- a/adcc/tests/data/SHA256SUMS
+++ b/adcc/tests/data/SHA256SUMS
@@ -5,12 +5,26 @@ eb433c83b7bde2693ef2ae9a6adc16ddfe21ae89dbb86778ff8227be919d237c cn_ccpvdz_adcc
dd3b0776c3a1dd46e3720c7f2a2db8dfe26aae30a6a9bf9f14da2e9345742870 cn_ccpvdz_adcc_adc2.hdf5
d28c1ffd6a8bc5574db71889b171c5c535c3558fcfd81bb77d26560ebd7bbce8 cn_ccpvdz_adcc_adc2x.hdf5
8502e612dcf7b18261df569c12924949c1cd348455048da00f8bef70a28f1d68 cn_ccpvdz_adcc_adc3.hdf5
+f9e07576076f1bffd9c439aeec1abb49427eb99366e65d7a0d5ffb9f451c498b cn_ccpvdz_adcc_ea-adc0.hdf5
+d8e0843e97317c214b3f3ed8dd1209c2d097df1a202d843d336b1d995733d48b cn_ccpvdz_adcc_ea-adc2.hdf5
+f8d01a40d3052b87c22f14ab9c643dfb31fbf48d8b0cbad712c57956e972fc85 cn_ccpvdz_adcc_ea-adc2x.hdf5
+7b3ea86eb2b84ba8b69c8c3d985e69fe1fe1c2e40637b480965fdd5651fe98a1 cn_ccpvdz_adcc_ea-adc3.hdf5
+843ef518e0cf3e97c88dd5e1658442d4c0c77c0773aa10b66495d1d5986efe5f cn_ccpvdz_adcc_ip-adc0.hdf5
+52759d5ce71664e9bb5cb33599f3c069a5fb685e7c14df6b82df897f267b559a cn_ccpvdz_adcc_ip-adc2.hdf5
+5019358f1070122e9b23ce8d551016438b36e84ce91788531da19a2940273c62 cn_ccpvdz_adcc_ip-adc2x.hdf5
+a7f7181990ed30cf38cda0a0b88421672b1bf974fa7fa7d5fb2d1955b3d4e4bc cn_ccpvdz_adcc_ip-adc3.hdf5
cefa3b262340d035749887bc065200c20e67de70625ef6f6ebc022b979921d07 cn_ccpvdz_adcc_mpdata.hdf5
8f8c3c86cb9f4b22f8bcfff10e7092fcec4258fbf2a451e3cd6456b1adb7ba85 cn_ccpvdz_adcman_adc0.hdf5
be3f5e6090ba4d6fef72760e982bc268dc49f870db1afc101af60e08be559423 cn_ccpvdz_adcman_adc1.hdf5
259b19562741b25ff08491c446e69af92a34ad320d204b7dcfe5a6a4928a9072 cn_ccpvdz_adcman_adc2.hdf5
8a8349b6d5b7f9eaf1b51f9d68f0ae4bb1b36b5d74ec9a9bac580e27cf77aedc cn_ccpvdz_adcman_adc2x.hdf5
1efce537b0cc5d7cb227acee19241f6ce11ce7d4e29a3cd9570f869c2b2c3f7c cn_ccpvdz_adcman_adc3.hdf5
+3a245b724cb53d058ffd76f664eabed5c93b124f3077de82ab361369d3f5fc0d cn_ccpvdz_adcman_ea-adc0.hdf5
+80d3bb9122f0fd31a65a0c651479e4af8d04968c974faee1ba7d89bd6c7231dc cn_ccpvdz_adcman_ea-adc2.hdf5
+af48346ca95af1a4795d56a43dedd9c76404f52f0babd78d4947c75700dcb509 cn_ccpvdz_adcman_ea-adc3.hdf5
+51b34f119b24040e98d1fa750630146079c8e039948994149b93f6cd0dd45de3 cn_ccpvdz_adcman_ip-adc0.hdf5
+4979368df74815035adf6e4b84ef5da3068a4d7fbbe6e014d3902d28d4e4ef80 cn_ccpvdz_adcman_ip-adc2.hdf5
+a8743b12ccf3ba67716008972be04d58298c28f4b4624d4377861e0a2bc13ff5 cn_ccpvdz_adcman_ip-adc3.hdf5
24069d49f0fae0a16b4141e04ebbaf9e0895adffb22c984f50c1a700a49212cf cn_ccpvdz_adcman_mpdata.hdf5
db27db84b2dea991a4e24b6ce7433f2619769505267bd486723b1b9bd017a5f3 cn_ccpvdz_hfdata.hdf5
8c3f128cd46df98eaaba3658b849150459f738e9b3cc1cc42f2b5ff6ad288c9f cn_ccpvdz_hfimport.hdf5
@@ -20,6 +34,14 @@ db27db84b2dea991a4e24b6ce7433f2619769505267bd486723b1b9bd017a5f3 cn_ccpvdz_hfda
fe4da3d8dad6fb394329d6d409860641e99465b8d53f0efc880129238dfb4efd cn_sto3g_adcc_adc2x.hdf5
2a041d39ac6d1bbc1bc0365ba4e191a44f2e059c9c706891746500bb09dd2eb6 cn_sto3g_adcc_adc3.hdf5
7e56ecaf37b8d58da2e98519542623466d51c253be8cc189a0281b2b419390f7 cn_sto3g_adcc_adc4.hdf5
+ffeae310f386671ef10ba52d637576494bdc6ff2ff7e0f027027064405a044fa cn_sto3g_adcc_ea-adc0.hdf5
+f3a17cc0733110ab0bfc7b683cfa042acdf63789cee50447d83fb7d4d677803e cn_sto3g_adcc_ea-adc2.hdf5
+8f79f2c1acfdf7b9789cb0d4c4ca1c62fc4284814c0a89620fe65527c5a8e594 cn_sto3g_adcc_ea-adc2x.hdf5
+a911fec9a9a8ee92e91facdab68be0afa0d6d4c7a725b53b6abf1d21631632ac cn_sto3g_adcc_ea-adc3.hdf5
+c850181d00e405c6c97cd1d1340b13a19909482d61c63981c8ceb5c0301dbfcd cn_sto3g_adcc_ip-adc0.hdf5
+c14114493b7c977102fa553480e6c10230ab2d0d37a3769a65e5ce15c4f66c97 cn_sto3g_adcc_ip-adc2.hdf5
+87feeadbd2eb2e5dcc0500c7c90f6af89494188efae718e38df48deca679e82b cn_sto3g_adcc_ip-adc2x.hdf5
+940d655a575c48ab26257b445e7a2822f75de3b49cf1d887d1ce164d260a6303 cn_sto3g_adcc_ip-adc3.hdf5
17994daf530ea179f872cd73695a7e492a0cd4c39947ded1c97afb663d90fa58 cn_sto3g_adcc_mpdata.hdf5
f6974049ccf8a732ea584dbbebf608f0e17d28bd5320c0c63e1d1a5db80f54a7 cn_sto3g_adcman_adc0.hdf5
9950c0720546a0b313e755945e7fd0d5462967a4366f60470c4a85f500bffb5f cn_sto3g_adcman_adc1.hdf5
@@ -27,6 +49,12 @@ a866f1c06508e1bf48567c2546938e471b83c82ecce7dfce6815288c1e0226a6 cn_sto3g_adcma
7b6e09a40fd22f211119afe8f6f46d2c573ca905fa69c52f6f5d053e2c26447e cn_sto3g_adcman_adc2x.hdf5
2312e925cb33a47bf476c5528531c0c6908b8803a5d5f6785d1ff0f7da946353 cn_sto3g_adcman_adc3.hdf5
6e34b23da0215dd1e3969a5c1159abb9dcbae843183c3823f0ac872e9df7ae48 cn_sto3g_adcman_adc4.hdf5
+333b8b76527bc1e017230aa6e7d030faad001cfa0d5d382677264699dc3b3081 cn_sto3g_adcman_ea-adc0.hdf5
+ef78680d939d2e5204bb7217481c980c2d46cbccbc9da6375b0db975e92b34f4 cn_sto3g_adcman_ea-adc2.hdf5
+4ec353b931bd0986dad3cc9823a3ec6634d39ee2ebd1bea6c800ba047c32012b cn_sto3g_adcman_ea-adc3.hdf5
+b36e8397ee7af7eb627be2ab355a6b20e90b3a417f42b60ead0b5e8bde16c9d8 cn_sto3g_adcman_ip-adc0.hdf5
+cfc731c423999b1b1f79929e180f7d3cc76199079b4fc69dca1b2e0ccf9672ba cn_sto3g_adcman_ip-adc2.hdf5
+99b1ba20f1bc57be2acc083edd5ce31dcd1d2eedb6cb8e286862821bc4adb629 cn_sto3g_adcman_ip-adc3.hdf5
8f7a0fb42f351ffb3577636ae224a03a370a14fcea9d9252bef5c3fa37bb2444 cn_sto3g_adcman_mpdata.hdf5
7d843dd9981ef78f11a78051aea1ea61af8f12dc244b93e3f76fd70602c8f86e cn_sto3g_hfdata.hdf5
73adda09433dab2263e3dc2b37e10d5127e4c54a4296fdf2574ab71ec44d172a cn_sto3g_hfimport.hdf5
@@ -46,12 +74,26 @@ b579b2e62d4d02c0e5d16ab8b8e30009582152dba63ac12cea77f7dbbd80c29d formaldehyde_s
28e018d79c5481938f77177fbeb6f65e6c04122ef7f8fc3db478a5d31592da6c h2o_def2tzvp_adcc_adc2.hdf5
4a35c56f21d7c0ae424225bc756ac9b8a9f9b2f32b90e5aea45351b4249196f2 h2o_def2tzvp_adcc_adc2x.hdf5
6157f5d41dec84612de81005b17cdcc531efc74d5da42e1a49d2cb53492f3248 h2o_def2tzvp_adcc_adc3.hdf5
+528c73962158a58994c1cfd46ef92f05a34b66ca511cde4842290c0e6da8f2ff h2o_def2tzvp_adcc_ea-adc0.hdf5
+fd1d0720f62ff186d5227beb215d25eca9734ac5c132049c718ce38797f7cc18 h2o_def2tzvp_adcc_ea-adc2.hdf5
+40c91d44899604ad3e31558593ce20e0d3bff316bcf231f95f57a51c474f9992 h2o_def2tzvp_adcc_ea-adc2x.hdf5
+3e1f6ea7e08e4c51dba686f730cbaa84d33e5270ddd6e044f32889f1823dcb11 h2o_def2tzvp_adcc_ea-adc3.hdf5
+4a0776fb0ac75c0203bc41e9c25e3be5d80e1e5323071fde500fedb84711bd4c h2o_def2tzvp_adcc_ip-adc0.hdf5
+0381025325cdcb7b6f63ad6acb5901f5609316c1a85baf8475a7aba94457c337 h2o_def2tzvp_adcc_ip-adc2.hdf5
+68c1e39d23afbea60a4a4a4fd6c5dec43d4c7304ecd73b51f8b18308b18ca4c2 h2o_def2tzvp_adcc_ip-adc2x.hdf5
+33800f9d0b2a0edec35710bea483631bdfa29582a99f7ed2ef2f54da7a9321e0 h2o_def2tzvp_adcc_ip-adc3.hdf5
96eefd0a6314651f96cb4480a34652fc866984496aa365313b1521680c984fdd h2o_def2tzvp_adcc_mpdata.hdf5
2a88059266ac32a34cc352e1d2f782565317cec8573bdd81c4e4019ecc9adb65 h2o_def2tzvp_adcman_adc0.hdf5
dcf8ae501f7875705191abf754b33cf82f14065db2c95ab84ef3004b70ee9e75 h2o_def2tzvp_adcman_adc1.hdf5
7cd549b23a8939f5c989882b250284d5f6b20d14e97055f5caba7e6a6138f4b2 h2o_def2tzvp_adcman_adc2.hdf5
24394b48a8218eed94d2c9462df8ecd065ec3ad985e215389fee535268584c2f h2o_def2tzvp_adcman_adc2x.hdf5
86b875b3f7e7695477f2fc34b3860b9736e5d23c8803278f88cda7eadf9d833f h2o_def2tzvp_adcman_adc3.hdf5
+373dad86be62b2f255a58d1d2371d1b6ca4adb8c33bf2a168ad729138973f76f h2o_def2tzvp_adcman_ea-adc0.hdf5
+cae121c0658e8cef2b79d0adc119d4397787a8a5c5f8f4608225ba0266e6a0d0 h2o_def2tzvp_adcman_ea-adc2.hdf5
+cde62e824358e468d10a44b2b8271afec44eb0b350ad2b54f4168c0b90b32e54 h2o_def2tzvp_adcman_ea-adc3.hdf5
+d06b4f8c48e072009cf4f830cf882f9b4b887fe5eeb02bc7d72b69cfc4c3795b h2o_def2tzvp_adcman_ip-adc0.hdf5
+1f29b185958fddd686782425030b3014ddf4e363e8e73a146d9e3f7a20b114d5 h2o_def2tzvp_adcman_ip-adc2.hdf5
+8fc2922530d65cb23d821e662f9dedee62d91ad2508bf246ed3cbba640dbf75c h2o_def2tzvp_adcman_ip-adc3.hdf5
1b8a1e0ee2b80d0aac77876e4ca291fd213f0981587a59d6e4585d953c06d59e h2o_def2tzvp_adcman_mpdata.hdf5
36b3b93aa1071e61611d96b6b59ed13d37ec718303b8ef60a6c10aef07276717 h2o_def2tzvp_hfdata.hdf5
8a447ed3025662a2a71d866c697d345dbace45655e14c8777c10190f7f4c5967 h2o_def2tzvp_hfimport.hdf5
@@ -61,6 +103,14 @@ b56e74c8e71a9ab5ef62d44131c721e8d5ce236f9ed69d82869d702468aff7a8 h2o_sto3g_adcc
8e17428b0d7e93e91e3ba366f7d2a9aded91a5d8d37b47fcc790c20b5b150872 h2o_sto3g_adcc_adc2x.hdf5
de9a094a6da75d063a7d1a4137347409c7dd2ced6ae1bdd2ebece6b2712506d6 h2o_sto3g_adcc_adc3.hdf5
14b54dcc669aeb76815481e881d47dce8f7b949de2dba3ab6e7108e73ae508e5 h2o_sto3g_adcc_adc4.hdf5
+83b8899a0b7851e18b3e246b802a7e8679d91881c3838797ee6fdb596e54f977 h2o_sto3g_adcc_ea-adc0.hdf5
+c166cd64e6a4a8eac9f761c133d685646124b291887407b89a31b02e197eafe7 h2o_sto3g_adcc_ea-adc2.hdf5
+5badc1e98c937938c5e0cdebdc50fa11dd4b9d3e0f989e02e558ee867df04fb0 h2o_sto3g_adcc_ea-adc2x.hdf5
+c6ec9963d3cf5fad77ed6c57a4af166864c207f8af20189e3583b4f7233e2605 h2o_sto3g_adcc_ea-adc3.hdf5
+d2f58d41610467bcda51778d979d9b0d39bcd11146ce7ec6780f7e04134c95ef h2o_sto3g_adcc_ip-adc0.hdf5
+9e7d019e831be4c71a9acd50b27794c31ced89b3877d859df43a5e033a2d9fa4 h2o_sto3g_adcc_ip-adc2.hdf5
+82982e49f56fe32b8fe60a71fd31a3375e6669bae4830a1ebf6f554b11f88764 h2o_sto3g_adcc_ip-adc2x.hdf5
+d0067822d5061ac91091e303d1ad586a415d7c6190af3abce573fbb3a971d998 h2o_sto3g_adcc_ip-adc3.hdf5
4751e791a2ce1968106ffdb5128984a777554d8d03aef6585ef378efe1f396ef h2o_sto3g_adcc_mpdata.hdf5
cc957d99adaf872877b992860c9f0c9c8337f783b9c3ad20b36694391627188f h2o_sto3g_adcman_adc0.hdf5
4b36ac6cce98cf250ad268dc6e51e849acbd8c9383aa16aff233a2cf04118fcd h2o_sto3g_adcman_adc1.hdf5
@@ -68,9 +118,18 @@ cc957d99adaf872877b992860c9f0c9c8337f783b9c3ad20b36694391627188f h2o_sto3g_adcm
dbf6b078e8096f8283cc64d1de98dff91b5edebd1d2f1df4f5739cfcecd98db7 h2o_sto3g_adcman_adc2x.hdf5
7a65c053bea13cc431ad127120578d27118ee9f9c026f6402dc07953cfb96c23 h2o_sto3g_adcman_adc3.hdf5
ebc3bd158758a8e8db58ee2a3cc95e85f440db623ca06d220c04d94561d35152 h2o_sto3g_adcman_adc4.hdf5
+84e1e96fdb30971f2e5f1a9501e9445a8dce031f8e36d6e46360a132440269f7 h2o_sto3g_adcman_ea-adc0.hdf5
+fe9806e6f42104c25c4e022484e670588c343bb55761baca599e01cd98b145f9 h2o_sto3g_adcman_ea-adc2.hdf5
+10e6b9d7e3b5d088233b61dd599946f55a579c96cefd0d21feee9c4893bc88c4 h2o_sto3g_adcman_ea-adc3.hdf5
+c936ae6eb312921adcf1a3fc8bac19453eebbe29f8de4a574ac981b2ef55fffa h2o_sto3g_adcman_ip-adc0.hdf5
+abeaeacf2248d3c779a605eaf5d8313029593e22f8656c1baa5bcb0a983e4fcb h2o_sto3g_adcman_ip-adc2.hdf5
+dd39481ff664de5c228919687ea3fbcf1effaa0aaddff4750d008cb25066b956 h2o_sto3g_adcman_ip-adc3.hdf5
4b21f9230140823618c12761ac51bab626e47a82ec04dc05b0a57422fb85b9b1 h2o_sto3g_adcman_mpdata.hdf5
6c64153a6e708edad4c8a07e0ed6e5e1379e8e9c721e49f2aa0cc4ecb9817082 h2o_sto3g_hfdata.hdf5
74733b03dd12561479132a768c2b330b4b8dbf1fe60fe0b14caa4d3fd4da7fce h2o_sto3g_hfimport.hdf5
+057ef124dc4b3fdfe4958f74009500a6507c8f2bb2a24f0a9cb4bc8119334c10 h2s_6311g_hfdata.hdf5
+4354d9a8f676370454deb43116775f111d9e1a32508ef7e0adaa0cca67c1a87d h2s_sto3g_hfdata.hdf5
+93bf9f69b8ab2c524b499bc60790f245401ab0d65cc342e92479c68051b0f34d hf3_631g_hfdata.hdf5
ca45224813877e28332fd27c445320200e34447fca4e1f3dd2fe318948caf3ae hf_631g_adcc_adc0.hdf5
c5b7f42d403183e7e44289d7b3bca21cec217a9cf01fb6bf4e0ae33d17884d21 hf_631g_adcc_adc1.hdf5
d6809c1474a375141a3cd065ceabf84e26ade89e79973c6403110bb77c6a0e7d hf_631g_adcc_adc2.hdf5
@@ -86,4 +145,5 @@ ff8cba744d8820c1877104985454b611ea54e7f1bbbac683079911ef19b9dd25 hf_631g_adcman
4145630c7f3f4426608fbc2c3a883a90b810d7076ea10e6cc157b3f64bdb17f4 hf_631g_adcman_adc4.hdf5
0f2e477d37597331cf955be0d0bdaceb55604fd220e98612bb2ad3fb3fe5d50e hf_631g_adcman_mpdata.hdf5
a1eab1dd706a3f8f7532889b9a4754fa627a47ce5cfb9eb7d95a8365774850f2 hf_631g_hfdata.hdf5
+5877bf4fc4554759b2f0ba35c58cee1e9a7ae2de4d47673a98793d4769eec841 methox_sto3g_hfdata.hdf5
0e86aef221cc541cbade47e99471fa7c0bc1955c80783be4b32a8ea58902b2f6 r2methyloxirane_sto3g_hfdata.hdf5
diff --git a/adcc/tests/data/update_testdata.sh b/adcc/tests/data/update_testdata.sh
index c349888f..a304cddd 100755
--- a/adcc/tests/data/update_testdata.sh
+++ b/adcc/tests/data/update_testdata.sh
@@ -4,7 +4,7 @@
trap "exit 1" TERM
export SCRIPT_PID=$$
-SOURCE="https://wwwagdreuw.iwr.uni-heidelberg.de/adcc_test_data/0.8.1/"
+SOURCE="https://wwwagdreuw.iwr.uni-heidelberg.de/adcc_test_data/ip_ea/"
SHAFILE="SHA256SUMS"
@@ -17,6 +17,14 @@ DATAFILES=(
cn_sto3g_adcc_adc2x.hdf5
cn_sto3g_adcc_adc3.hdf5
cn_sto3g_adcc_adc4.hdf5
+ cn_sto3g_adcc_ip-adc0.hdf5
+ cn_sto3g_adcc_ip-adc2.hdf5
+ cn_sto3g_adcc_ip-adc2x.hdf5
+ cn_sto3g_adcc_ip-adc3.hdf5
+ cn_sto3g_adcc_ea-adc0.hdf5
+ cn_sto3g_adcc_ea-adc2.hdf5
+ cn_sto3g_adcc_ea-adc2x.hdf5
+ cn_sto3g_adcc_ea-adc3.hdf5
cn_sto3g_adcc_mpdata.hdf5
cn_sto3g_adcman_adc0.hdf5
cn_sto3g_adcman_adc1.hdf5
@@ -24,6 +32,12 @@ DATAFILES=(
cn_sto3g_adcman_adc2x.hdf5
cn_sto3g_adcman_adc3.hdf5
cn_sto3g_adcman_adc4.hdf5
+ cn_sto3g_adcman_ip-adc0.hdf5
+ cn_sto3g_adcman_ip-adc2.hdf5
+ cn_sto3g_adcman_ip-adc3.hdf5
+ cn_sto3g_adcman_ea-adc0.hdf5
+ cn_sto3g_adcman_ea-adc2.hdf5
+ cn_sto3g_adcman_ea-adc3.hdf5
cn_sto3g_adcman_mpdata.hdf5
cn_sto3g_hfdata.hdf5
cn_sto3g_hfimport.hdf5
@@ -39,6 +53,14 @@ DATAFILES=(
h2o_sto3g_adcc_adc2x.hdf5
h2o_sto3g_adcc_adc3.hdf5
h2o_sto3g_adcc_adc4.hdf5
+ h2o_sto3g_adcc_ip-adc0.hdf5
+ h2o_sto3g_adcc_ip-adc2.hdf5
+ h2o_sto3g_adcc_ip-adc2x.hdf5
+ h2o_sto3g_adcc_ip-adc3.hdf5
+ h2o_sto3g_adcc_ea-adc0.hdf5
+ h2o_sto3g_adcc_ea-adc2.hdf5
+ h2o_sto3g_adcc_ea-adc2x.hdf5
+ h2o_sto3g_adcc_ea-adc3.hdf5
h2o_sto3g_adcc_mpdata.hdf5
h2o_sto3g_adcman_adc0.hdf5
h2o_sto3g_adcman_adc1.hdf5
@@ -46,6 +68,12 @@ DATAFILES=(
h2o_sto3g_adcman_adc2x.hdf5
h2o_sto3g_adcman_adc3.hdf5
h2o_sto3g_adcman_adc4.hdf5
+ h2o_sto3g_adcman_ip-adc0.hdf5
+ h2o_sto3g_adcman_ip-adc2.hdf5
+ h2o_sto3g_adcman_ip-adc3.hdf5
+ h2o_sto3g_adcman_ea-adc0.hdf5
+ h2o_sto3g_adcman_ea-adc2.hdf5
+ h2o_sto3g_adcman_ea-adc3.hdf5
h2o_sto3g_adcman_mpdata.hdf5
h2o_sto3g_hfdata.hdf5
h2o_sto3g_hfimport.hdf5
@@ -72,12 +100,26 @@ DATAFILES_FULL=(
cn_ccpvdz_adcc_adc2.hdf5
cn_ccpvdz_adcc_adc2x.hdf5
cn_ccpvdz_adcc_adc3.hdf5
+ cn_ccpvdz_adcc_ip-adc0.hdf5
+ cn_ccpvdz_adcc_ip-adc2.hdf5
+ cn_ccpvdz_adcc_ip-adc2x.hdf5
+ cn_ccpvdz_adcc_ip-adc3.hdf5
+ cn_ccpvdz_adcc_ea-adc0.hdf5
+ cn_ccpvdz_adcc_ea-adc2.hdf5
+ cn_ccpvdz_adcc_ea-adc2x.hdf5
+ cn_ccpvdz_adcc_ea-adc3.hdf5
cn_ccpvdz_adcc_mpdata.hdf5
cn_ccpvdz_adcman_adc0.hdf5
cn_ccpvdz_adcman_adc1.hdf5
cn_ccpvdz_adcman_adc2.hdf5
cn_ccpvdz_adcman_adc2x.hdf5
cn_ccpvdz_adcman_adc3.hdf5
+ cn_ccpvdz_adcman_ip-adc0.hdf5
+ cn_ccpvdz_adcman_ip-adc2.hdf5
+ cn_ccpvdz_adcman_ip-adc3.hdf5
+ cn_ccpvdz_adcman_ea-adc0.hdf5
+ cn_ccpvdz_adcman_ea-adc2.hdf5
+ cn_ccpvdz_adcman_ea-adc3.hdf5
cn_ccpvdz_adcman_mpdata.hdf5
cn_ccpvdz_hfdata.hdf5
cn_ccpvdz_hfimport.hdf5
@@ -91,12 +133,26 @@ DATAFILES_FULL=(
h2o_def2tzvp_adcc_adc2.hdf5
h2o_def2tzvp_adcc_adc2x.hdf5
h2o_def2tzvp_adcc_adc3.hdf5
+ h2o_def2tzvp_adcc_ip-adc0.hdf5
+ h2o_def2tzvp_adcc_ip-adc2.hdf5
+ h2o_def2tzvp_adcc_ip-adc2x.hdf5
+ h2o_def2tzvp_adcc_ip-adc3.hdf5
+ h2o_def2tzvp_adcc_ea-adc0.hdf5
+ h2o_def2tzvp_adcc_ea-adc2.hdf5
+ h2o_def2tzvp_adcc_ea-adc2x.hdf5
+ h2o_def2tzvp_adcc_ea-adc3.hdf5
h2o_def2tzvp_adcc_mpdata.hdf5
h2o_def2tzvp_adcman_adc0.hdf5
h2o_def2tzvp_adcman_adc1.hdf5
h2o_def2tzvp_adcman_adc2.hdf5
h2o_def2tzvp_adcman_adc2x.hdf5
h2o_def2tzvp_adcman_adc3.hdf5
+ h2o_def2tzvp_adcman_ip-adc0.hdf5
+ h2o_def2tzvp_adcman_ip-adc2.hdf5
+ h2o_def2tzvp_adcman_ip-adc3.hdf5
+ h2o_def2tzvp_adcman_ea-adc0.hdf5
+ h2o_def2tzvp_adcman_ea-adc2.hdf5
+ h2o_def2tzvp_adcman_ea-adc3.hdf5
h2o_def2tzvp_adcman_mpdata.hdf5
h2o_def2tzvp_hfdata.hdf5
h2o_def2tzvp_hfimport.hdf5
diff --git a/adcc/tests/generators/dump_adcc.py b/adcc/tests/generators/dump_adcc.py
index a2003b08..10b9d9c6 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
@@ -84,11 +85,51 @@ def dump_groundstate(ground_state: LazyMp, hdf5_file: h5py.Group,
hdf5_file.attrs["adcc_version"] = adcc.__version__
-def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group,
- only_full_mode: bool,
- dump_nstates: int | None = 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: DetachedStates | AttachedStates,
+ kind_data: dict, n_states: int) -> None:
+ """Collects all properties that are unique for IP/EA-ADC
+ """
+ pass
+
+
+def dump_excited_states(
+ states: ExcitedStates | DetachedStates | AttachedStates,
+ hdf5_file: h5py.Group, only_full_mode: bool,
+ 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.
The only_full_mode flag indicates whether the underlying test case is only
@@ -96,7 +137,7 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group,
and therefore not all test data might be dumped in that case.
"""
# ensure that the calculation converged on a nonzero result
- assert states.converged # type: ignore
+ assert states.converged
assert all(abs(e) > 1e-12 for e in states.excitation_energy)
n_states = len(states.excitation_energy)
@@ -105,50 +146,29 @@ 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):
- # densities
- 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())
+ if isinstance(states, ExcitedStates):
+ # densities
+ 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())
# eigenvectors
for exdegree, block in enumerate(states.matrix.axis_blocks):
if exdegree + 1 not in eigenvectors:
eigenvectors[exdegree + 1] = []
- eigenvectors[exdegree + 1].append(getattr(
- states.excitation_vector[n], block # type: ignore
- ).to_ndarray())
+ eigenvectors[exdegree + 1].append(
+ 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:
@@ -156,34 +176,41 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group,
# only dump triples for small systems
if 3 in eigenvectors and not only_full_mode:
kind_data["eigenvectors_triples"] = np.asarray(eigenvectors[3])
- # state to state tdm: not implemented for CVS
- if not states.method.is_core_valence_separated:
- for ifrom in range(n_states - 1):
- state2state = State2States(states, initial=ifrom)
- # extract the tdms for the desired states
- tdm_bb_a = []
- tdm_bb_b = []
- for j, tdm in enumerate(state2state.transition_dm):
- if ifrom + j + 2 > n_states: # ifrom + j + 2 = ito
- break
- bb_a, bb_b = tdm.to_ao_basis(states.reference_state)
- tdm_bb_a.append(bb_a.to_ndarray())
- tdm_bb_b.append(bb_b.to_ndarray())
- kind_data[f"state_to_state/from_{ifrom}/transition_dipole_moments"] = (
- state2state.transition_dipole_moment[:n_states - ifrom - 1]
- )
- kind_data[f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_a"] = (
- np.asarray(tdm_bb_a)
- )
- kind_data[f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_b"] = (
- np.asarray(tdm_bb_b)
- )
- # ssq for unrestriced calculation
- if (
- not states.reference_state.restricted
- and not states.ground_state.has_core_occupied_space
- ):
- kind_data["state_ssq"] = states.state_ssq
+ if isinstance(states, ExcitedStates):
+ # state and transition dipole moments
+ kind_data["state_dipole_moments"] = states.state_dipole_moment[: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)
+ # state to state tdm: not implemented for CVS
+ if not states.method.is_core_valence_separated:
+ for ifrom in range(n_states - 1):
+ state2state = State2States(states, initial=ifrom)
+ # extract the tdms for the desired states
+ tdm_bb_a = []
+ tdm_bb_b = []
+ for j, tdm in enumerate(state2state.transition_dm):
+ if ifrom + j + 2 > n_states: # ifrom + j + 2 = ito
+ break
+ bb_a, bb_b = tdm.to_ao_basis(states.reference_state)
+ tdm_bb_a.append(bb_a.to_ndarray())
+ tdm_bb_b.append(bb_b.to_ndarray())
+ kind_data[
+ f"state_to_state/from_{ifrom}/transition_dipole_moments"
+ ] = (state2state.transition_dipole_moment[:n_states - ifrom - 1])
+ kind_data[
+ f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_a"
+ ] = (np.asarray(tdm_bb_a))
+ kind_data[
+ f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_b"
+ ] = (np.asarray(tdm_bb_b))
+ # ssq for unrestriced calculation
+ if (
+ not states.reference_state.restricted
+ and not states.ground_state.has_core_occupied_space
+ ):
+ kind_data["state_ssq"] = states.state_ssq
# write the data to hdf5
emplace_dict(kind_data, hdf5_file, compression="gzip")
hdf5_file.attrs["adcc_version"] = adcc.__version__
diff --git a/adcc/tests/generators/generate_adcc_data.py b/adcc/tests/generators/generate_adcc_data.py
index a94caf68..4be88d14 100644
--- a/adcc/tests/generators/generate_adcc_data.py
+++ b/adcc/tests/generators/generate_adcc_data.py
@@ -4,7 +4,8 @@
from adcc.tests.testdata_cache import testdata_cache
from adcc.tests import testcases
-from adcc.AdcMethod import AdcMethod
+from adcc.AdcMethod import AdcMethod, AdcType
+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"),
}
_small_cases_methods = {
"pp": _methods["pp"] + ("adc4",)
@@ -29,6 +33,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
gs_density_order: int | None = None,
n_states: int | None = None, n_singlets: int | None = None,
n_triplets: int | None = None, n_spin_flip: int | None = None,
+ n_doublets: int | None = None, is_alpha: bool | None = None,
dump_nstates: int | None = None, **kwargs) -> None:
"""
Generate and dump the excited states reference data for the given reference case
@@ -42,13 +47,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 (AdcType.IP, AdcType.EA):
+ spin = "alpha" if is_alpha else "beta"
+ if f"{key}/{spin}/{kind}" in hdf5_file:
+ return None
# CVS-ADC(4) not available
if "cvs" in case and method.name == "adc4":
return None
@@ -61,13 +73,20 @@ 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? # type: ignore
- 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 (AdcType.IP, AdcType.EA):
+ key = f"{key}/{spin}"
+ dump_matrix = (f"{key}/matrix" not in hdf5_file and (
+ method.adc_type is AdcType.PP or (
+ method.adc_type in (AdcType.IP, AdcType.EA) and is_alpha))
+ )
+ if dump_matrix:
+ # 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() # type: ignore # noqa: E501
dump_matrix_testdata(states.matrix, trial_vec, matrix_group)
@@ -80,6 +99,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod,
n_states: int | None = None, n_singlets: int | None = None,
n_triplets: int | None = None, n_spin_flip: int | None = None,
+ n_doublets: int | None = None, is_alpha: bool | None = None,
dump_nstates: int | None = None,
states_per_case: dict[str, dict[str, int]] | None = None,
**kwargs) -> None:
@@ -93,11 +113,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
)
@@ -149,6 +172,29 @@ def generate_h2o_sto3g():
**n_states, **kwargs
)
+ 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.to_int() < 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():
# RHF, Singlet, 43 basis functions: 5 occ, 38 virt.
@@ -166,6 +212,26 @@ def generate_h2o_def2tzvp():
**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():
# UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt
@@ -175,12 +241,39 @@ 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,
- states_per_case=None, **kwargs
+ states_per_case=None, **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.to_int() < 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.to_int() < 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
@@ -192,10 +285,31 @@ def generate_cn_ccpvdz():
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, states_per_case=None,
- **n_states
+ test_case, method=method, dump_nstates=2,
+ states_per_case=None, **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
@@ -211,6 +325,28 @@ def generate_hf_631g():
**n_states
)
+ # TODO: PP spin-flip only
+ # 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 1df5eff4..a25848e1 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"),
}
_small_cases_methods = {
"pp": _methods["pp"] + ("adc4",)
@@ -34,6 +38,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
gs_density_order: int | None = None,
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 = None, **kwargs) -> None:
"""
Generate and dump the excited state reference data for the given reference case
@@ -44,7 +50,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 is MethodLevel.ZERO:
@@ -64,9 +71,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
@@ -77,6 +85,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 = None,
states_per_case: dict[str, dict[str, int]] | None = None,
**kwargs) -> None:
@@ -90,12 +100,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
)
@@ -174,6 +188,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.to_int() < 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.
@@ -190,6 +223,20 @@ def generate_h2o_def2tzvp():
**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
@@ -204,6 +251,26 @@ def generate_cn_sto3g():
**n_states
)
+ for method in _methods["ip"]:
+ method = AdcMethod(method)
+ if method.level.to_int() < 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.to_int() < 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
@@ -218,6 +285,20 @@ def generate_cn_ccpvdz():
**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
@@ -232,6 +313,21 @@ def generate_hf_631g():
**n_states
)
+ # TODO: PP spin-flip only
+ # 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 15931c71..eea751d6 100644
--- a/adcc/tests/generators/import_qchem_data.py
+++ b/adcc/tests/generators/import_qchem_data.py
@@ -73,21 +73,35 @@ def import_excited_states(context: h5py.File, method: AdcMethod,
"""
# define the possible state kinds to import for each adc variant.
state_kinds = {
- AdcType.PP: [
- ("singlets", True), ("triplets", True), # restricted
+ AdcType.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
+ },
+ AdcType.IP: {
+ True: [""], # restricted
+ False: ["alphas", "betas"], # unrestricted
+ },
+ AdcType.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 is AdcType.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, only_full_mode=only_full_mode,
adc_type=method.adc_type, import_nstates=import_nstates,
@@ -107,7 +121,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 is AdcType.PP:
+ data[kind_map.get(kind, kind)] = states
+ elif method.adc_type in (AdcType.IP, AdcType.EA):
+ if restricted:
+ data["alpha"] = {"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}.")
@@ -141,7 +161,8 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool
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
@@ -152,10 +173,15 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool
# build the path under which to find the exicted states
tree = [f"adc_{adc_type.to_str()}", 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 is AdcType.PP:
+ assert state_kind is not None # needs to be defined for restricted
+ tree.append(state_kind)
else:
tree.append("uhf")
+ if adc_type in (AdcType.IP, AdcType.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
@@ -170,7 +196,12 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool
# context.
data_to_read = {}
for n in range(n_states):
- state_tree = tree + f"/es{n}"
+ if adc_type is AdcType.PP:
+ state_tree = tree + f"/es{n}"
+ elif adc_type is AdcType.IP:
+ state_tree = tree + f"/ip{n}"
+ elif adc_type is AdcType.EA:
+ state_tree = tree + f"/ea{n}"
# ensure that the state is converged
_, converged = _extract_dataset(context[f"{state_tree}/converged"])
if not converged:
@@ -247,10 +278,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.to_str()}", 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 is AdcType.PP:
+ assert state_kind is not None # needs to be defined for restricted
+ tree.append(state_kind)
else:
tree.extend(["uhf", "isr"])
+ if adc_type in (AdcType.IP, AdcType.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:
@@ -352,6 +388,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 a79ee1eb..14cb670a 100644
--- a/adcc/tests/generators/run_qchem.py
+++ b/adcc/tests/generators/run_qchem.py
@@ -29,6 +29,8 @@ def run_qchem(test_case: testcases.TestCase, method: AdcMethod, case: str,
run_qchem_scf: bool = False,
import_nstates: int | None = 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
@@ -122,6 +124,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
@@ -351,6 +354,8 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis:
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 | None = None,
n_frozen_core: int | None = None,
@@ -408,6 +413,10 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis:
singlet_states=singlet_states,
triplet_states=triplet_states,
sf_states=sf_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,
@@ -445,6 +454,10 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis:
ee_singlets {singlet_states}
ee_triplets {triplet_states}
sf_states {sf_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}
@@ -503,7 +516,17 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis:
"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)",
}
_isr_order_dict: dict[MethodLevel, str] = {
diff --git a/adcc/tests/guess_test.py b/adcc/tests/guess_test.py
index 70dff06e..7b9bcfa3 100644
--- a/adcc/tests/guess_test.py
+++ b/adcc/tests/guess_test.py
@@ -28,14 +28,20 @@
import adcc
import adcc.guess
+from adcc.guess import determine_spin_change, estimate_n_guesses
+from adcc.AdcMethod import AdcType
from .testdata_cache import testdata_cache
from . import testcases
# The methods to test
-singles_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"]
-doubles_methods = ["adc2", "adc2x", "adc3", "adc4"]
+singles_methods_pp = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"]
+doubles_methods_pp = ["adc2", "adc2x", "adc3", "adc4"]
+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 +49,98 @@
class TestGuess:
+ def test_determine_spin_change_pp(self):
+ method = adcc.AdcMethod("adc2")
+
+ spin = determine_spin_change(method, kind="singlet")
+ assert spin == 0.0
+
+ spin = determine_spin_change(method, kind="triplet")
+ assert spin == 0.0
+
+ spin = determine_spin_change(method, kind="spin_flip")
+ assert spin == -1.0
+
+ def test_determine_spin_change_ip(self):
+ 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):
+ 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):
+ method = adcc.AdcMethod("adc2")
+ method.adc_type = "bla"
+
+ with pytest.raises(ValueError, match="Unknown ADC type"):
+ determine_spin_change(method, kind="any")
+
+ def test_estimate_n_guesses_pp(self):
+ 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):
+ 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):
+ 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 +281,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 +434,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 +448,43 @@ 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()
- # 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
+ # Doubles guesses are constructed from the 0th order diagonal
+ if matrix.method.level.to_int() > 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
+
+ if block == "ph":
if spin_flip:
- sidcs = [idx for idx in sidcs[0]
+ 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 +492,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
@@ -266,12 +513,64 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False,
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 +627,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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
guesses = { # fewer guesses available
@@ -342,9 +679,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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
guesses = { # fewer ocvv guesses available
@@ -355,9 +692,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(AdcType.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(AdcType.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(AdcType.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(AdcType.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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
guesses = { # fewer guesses available
@@ -368,9 +737,9 @@ 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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
self.base_test_no_spin_change(
@@ -378,9 +747,46 @@ def test_doubles_cn(self, method: str, case: str):
max_guesses=5
)
- @pytest.mark.parametrize("method", singles_methods)
+ @pytest.mark.parametrize("method", singles_methods_ip)
+ @pytest.mark.parametrize("case", cn_sto3g.filter_cases(AdcType.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(AdcType.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(AdcType.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(AdcType.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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
self.base_test_spin_flip(
@@ -388,9 +794,9 @@ def test_singles_hf(self, method: str, case: str):
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):
if "cvs" in case and method == "adc4":
pytest.skip("CVS-ADC(4) not implemented")
self.base_test_spin_flip(
@@ -398,10 +804,46 @@ def test_doubles_hf(self, method: str, case: str):
max_guesses=5
)
+ @pytest.mark.parametrize("method", singles_methods_ip)
+ @pytest.mark.parametrize("case", hf_631g.filter_cases(AdcType.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(AdcType.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(AdcType.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(AdcType.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"]
@@ -423,15 +865,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])
+
+ # 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)
- def test_reference_h2o(self, method: str):
+ 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_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(matrix=matrix, ref=self.get_ref_h2o())
+ 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:
@@ -443,13 +1104,46 @@ 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}()'
- def get_ref_h2o(self):
+ @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
+ )
+
+ @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)
@@ -535,7 +1229,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"): [
@@ -565,3 +1259,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)
+ ],
+ }
diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py
index 14ffd2c8..8a38501b 100644
--- a/adcc/tests/properties_test.py
+++ b/adcc/tests/properties_test.py
@@ -62,7 +62,8 @@ class TestProperties:
@pytest.mark.parametrize("system,case,kind", cases)
def test_transition_dipole_moments(self, system: str, case: str, kind: str,
method: str, generator: str):
- if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
+ if "cvs" in case and AdcMethod(method).level.to_int() == 0 \
+ and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
refdata = testdata_cache._load_data(
@@ -95,7 +96,8 @@ def test_transition_dipole_moments(self, system: str, case: str, kind: str,
@pytest.mark.parametrize("system,case,kind", cases)
def test_oscillator_strengths(self, system: str, case: str, kind: str,
method: str, generator: str):
- if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
+ if "cvs" in case and AdcMethod(method).level.to_int() == 0 \
+ and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
refdata = testdata_cache._load_data(
@@ -126,7 +128,8 @@ def test_oscillator_strengths(self, system: str, case: str, kind: str,
@pytest.mark.parametrize("system,case,kind", cases)
def test_state_dipole_moments(self, system: str, case: str, kind: str,
method: str, generator: str):
- if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
+ if "cvs" in case and AdcMethod(method).level.to_int() == 0 \
+ and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
refdata = testdata_cache._load_data(
diff --git a/adcc/tests/testcases.py b/adcc/tests/testcases.py
index f41e320d..d2780a01 100644
--- a/adcc/tests/testcases.py
+++ b/adcc/tests/testcases.py
@@ -94,6 +94,8 @@ def filter_cases(self, adc_type: AdcType) -> tuple[str, ...]:
# since cvs is not (yet) implemented for IP.
if adc_type is AdcType.PP:
return self.cases
+ elif adc_type in (AdcType.IP, AdcType.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.")
@@ -112,14 +114,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]:
@@ -127,9 +141,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":
@@ -199,8 +213,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 08cedf3a..9c492cb3 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, AdcType
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
from typing import Optional, Union
@@ -118,11 +121,12 @@ def hfimport(self, system: Union[str, testcases.TestCase],
@cached_member_function()
def _load_data(self, system: Union[str, testcases.TestCase],
method: str, case: str, source: str,
- gs_density_order: Optional[int] = None) -> dict:
+ gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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.
"""
@@ -132,7 +136,7 @@ def _load_data(self, system: Union[str, testcases.TestCase],
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
@@ -146,54 +150,72 @@ def _load_data(self, system: Union[str, testcases.TestCase],
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 (AdcType.IP, AdcType.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}."
+ )
+ 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: Optional[int] = None) -> dict:
+ gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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.
"""
+ 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"
+ 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: Optional[int] = None) -> dict:
+ gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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.
"""
+ 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"
+ gs_density_order=gs_density_order, source="adcman",
+ is_alpha=is_alpha
)
@cached_member_function()
def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
method: str, case: str,
kind: str, source: str,
- gs_density_order: Optional[int] = None
- ) -> ExcitedStates:
+ gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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):
@@ -203,7 +225,7 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
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
@@ -211,7 +233,7 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
# 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:
@@ -239,12 +261,15 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
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}")
+ 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)
@@ -261,34 +286,49 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
evec[blocks[2]].set_from_ndarray(
adc_data["eigenvectors_triples"][i], 1e-14
)
- return ExcitedStates(states)
+
+ if matrix.method.adc_type is AdcType.PP:
+ return ExcitedStates(states)
+ elif matrix.method.adc_type is AdcType.IP:
+ return DetachedStates(states, is_alpha)
+ elif matrix.method.adc_type is AdcType.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: Optional[int] = None
- ) -> ExcitedStates:
+ case: str, gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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.
"""
+ 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"
+ 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: Optional[int] = None
- ) -> ExcitedStates:
+ case: str, gs_density_order: Optional[int] = None,
+ is_alpha: Optional[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.
"""
+ 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"
+ gs_density_order=gs_density_order, source="adcman",
+ is_alpha=is_alpha
)
@@ -303,7 +343,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()}
diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py
index 6dbc8e6d..930b94f4 100644
--- a/adcc/tests/workflow_test.py
+++ b/adcc/tests/workflow_test.py
@@ -26,21 +26,30 @@
from adcc import InputError
from .testdata_cache import testdata_cache
+from adcc.AdcMethod import AdcType
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 +61,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 +95,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 +271,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 is AdcType.PP
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == []
assert res.mospaces.frozen_core == []
assert res.mospaces.frozen_virtual == []
@@ -105,26 +280,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 is AdcType.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 is AdcType.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 is AdcType.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 is AdcType.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 is AdcType.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 is AdcType.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 +396,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"
@@ -209,11 +410,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)
@@ -222,34 +418,91 @@ 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,
+ 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_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"
+
+ refdata = testdata_cache.adcman_data(system, method=method, case=case)
+ 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_doublet(matrix, n_guesses=6, block="h",
+ 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,
- 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)
- def test_estimate_n_guesses(self):
- from adcc.workflow import estimate_n_guesses
+ def test_diagonalise_adcmatrix_ea(self):
+ from adcc.workflow import diagonalise_adcmatrix
+ system = "h2o_sto3g"
+ case = "gen"
+ method = "ea-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)
+ ref_doublets = refdata[kind]["eigenvalues"]
+ n_states = min(len(ref_doublets), 3)
- # 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)
+ matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case))
+
+ 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)
+ 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)
- def test_obtain_guesses_by_inspection(self):
+ 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")
@@ -269,8 +522,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=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_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
diff --git a/adcc/workflow.py b/adcc/workflow.py
index a9d27bf6..110a9b32 100644
--- a/adcc/workflow.py
+++ b/adcc/workflow.py
@@ -25,14 +25,18 @@
from libadcc import ReferenceState
+from typing import Optional
+
from . import solver
-from .guess import (guesses_any, guesses_singlet, guesses_spin_flip,
- guesses_triplet)
+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 .AdcMethod import AdcMethod, AdcType
+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
@@ -43,11 +47,12 @@
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_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 +75,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 +103,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
@@ -178,38 +190,79 @@ 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)
-
- # 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
-
- # Select solver to run
- if eigensolver is None:
- eigensolver = "davidson"
+ 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)
# 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
+ # 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(f"Less guesses provided via guesses (=={len(guesses)}"
+ f") than states to be computed (=={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, kind, guesses=guesses, n_guesses=n_guesses,
- n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output,
- eigensolver=eigensolver, **solverargs)
- exstates = ExcitedStates(diagres)
+ matrix, n_states, guesses, kind=kind, conv_tol=conv_tol,
+ output=output, eigensolver=eigensolver, is_alpha=is_alpha,
+ **solverargs)
+
+ if matrix.method.adc_type is AdcType.PP:
+ exstates = ExcitedStates(diagres)
+ elif matrix.method.adc_type is AdcType.IP:
+ exstates = DetachedStates(diagres, is_alpha)
+ elif matrix.method.adc_type is AdcType.EA:
+ exstates = AttachedStates(diagres, is_alpha)
+ else:
+ raise ValueError(f"Unknown ADC method: {matrix.method.name}")
+
exstates.kind = kind
exstates.spin_change = spin_change
@@ -255,20 +308,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)
@@ -285,18 +338,23 @@ 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
+ ) -> 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
- 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:
@@ -307,6 +365,16 @@ 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
+ is_alpha = True
if n_triplets is not None:
if not reference_state.restricted:
raise InputError("The n_triplets parameter may only be employed "
@@ -326,35 +394,136 @@ 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 is AdcType.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 is not AdcType.PP:
+ raise InputError("kind==singlet, kind==triplet, and kind==spin_flip "
+ "are only valid for PP-ADC calculations.")
+ if kind == "doublet" and adc_type is AdcType.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 obtain_guesses_by_inspection(matrix, n_guesses, kind,
+ n_guesses_doubles=None, is_alpha=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.
+ 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)
-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):
+ # 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, 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
+ n_guesses_doubles = n_guesses - len(guesses)
+
+ if n_guesses_doubles > 0:
+ if matrix.method.level.to_int() < 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)
+
+ if len(guesses) < n_guesses:
+ raise InputError(f"Less guesses found ({len(guesses)}) "
+ f"than requested {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:`Guesses` object containing all crucial guess information.
+ Internal function called from run_adc.
+ """
+ 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, kind="any", conv_tol=None,
+ eigensolver="davidson", output=sys.stdout,
+ 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.
+
+ matrix : AdcMatrix
+ n_states : int
+ guesses : list[AmplitudeVector]
+ A list of guess vectors
+ kind : str
"""
reference_state = matrix.reference_state
@@ -370,149 +539,50 @@ 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
)
# 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,
+ "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,
- output=output)
+ is_alpha=is_alpha, output=output)
run_eigensolver = lanczos
else:
raise InputError(f"Solver {eigensolver} unknown, try '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
- )
- guesses = obtain_guesses_by_inspection(matrix, n_guesses, kind,
- 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.")
-
solverargs.setdefault("which", "SA")
- return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol,
- callback=callback,
+ return run_eigensolver(matrix, guesses, n_ep=n_states,
+ conv_tol=conv_tol, callback=callback,
explicit_symmetrisation=explicit_symmetrisation,
**solverargs)
-def estimate_n_guesses(matrix, n_states, singles_only=True,
- n_guesses_per_state=2):
- """
- 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)
- n_guesses = min(n_guesses, n_occ_a * n_virt_a)
-
- # 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):
- """
- 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 "pphh" not in 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,
- "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")
-
- doubles_guesses = []
- if "pphh" in 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")
-
- 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,
- 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):
@@ -525,6 +595,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 is not AdcType.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:
@@ -583,8 +657,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})
diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.cc b/libadcc_src/amplitude_vector_enforce_spin_kind.cc
index e5727efc..ac3a3acb 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,131 @@ 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 +291,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 6a7a4dbd..0ac1a04e 100644
--- a/libadcc_src/amplitude_vector_enforce_spin_kind.hh
+++ b/libadcc_src/amplitude_vector_enforce_spin_kind.hh
@@ -37,6 +37,6 @@ namespace libadcc {
* @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);
+ 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 00000000..f2e616f1
--- /dev/null
+++ b/libadcc_src/fill_ea_doubles_guesses.cc
@@ -0,0 +1,75 @@
+//
+// 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 .
+//
+
+#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
diff --git a/libadcc_src/fill_ea_doubles_guesses.hh b/libadcc_src/fill_ea_doubles_guesses.hh
new file mode 100644
index 00000000..15ad4644
--- /dev/null
+++ b/libadcc_src/fill_ea_doubles_guesses.hh
@@ -0,0 +1,49 @@
+//
+// 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 .
+//
+
+#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 calc.)
+ * spin_change_twice Twice the value of 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 00000000..4ca9bc21
--- /dev/null
+++ b/libadcc_src/fill_ip_doubles_guesses.cc
@@ -0,0 +1,75 @@
+//
+// 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 .
+//
+
+#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
diff --git a/libadcc_src/fill_ip_doubles_guesses.hh b/libadcc_src/fill_ip_doubles_guesses.hh
new file mode 100644
index 00000000..2ff123c2
--- /dev/null
+++ b/libadcc_src/fill_ip_doubles_guesses.hh
@@ -0,0 +1,50 @@
+//
+// 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 .
+//
+
+#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 calc.)
+ * spin_change_twice Twice the value of 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
diff --git a/libadcc_src/fill_pp_doubles_guesses.cc b/libadcc_src/fill_pp_doubles_guesses.cc
index 2bd3ebaf..b5baec55 100644
--- a/libadcc_src/fill_pp_doubles_guesses.cc
+++ b/libadcc_src/fill_pp_doubles_guesses.cc
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2020 by the adcc authors
+// Copyright (C) 2026 by the adcc authors
//
// This file is part of adcc.
//
diff --git a/libadcc_src/fill_pp_doubles_guesses.hh b/libadcc_src/fill_pp_doubles_guesses.hh
index b2e40158..355c1df8 100644
--- a/libadcc_src/fill_pp_doubles_guesses.hh
+++ b/libadcc_src/fill_pp_doubles_guesses.hh
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2020 by the adcc authors
+// Copyright (C) 2026 by the adcc authors
//
// This file is part of adcc.
//
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 00000000..ea5e047d
--- /dev/null
+++ b/libadcc_src/guess/ea_adc_guess_d.cc
@@ -0,0 +1,483 @@
+#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; // occ.
+ orb_type[1] = false; // virt.
+ orb_type[2] = false; // virt.
+ 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
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 00000000..9a080678
--- /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
diff --git a/libadcc_src/guess/index_group_h2p.cc b/libadcc_src/guess/index_group_h2p.cc
new file mode 100644
index 00000000..e5a86ab8
--- /dev/null
+++ b/libadcc_src/guess/index_group_h2p.cc
@@ -0,0 +1,85 @@
+#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 00000000..fe25fad7
--- /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