diff --git a/adcc/AdcMatrix.py b/adcc/AdcMatrix.py
index 54bec0698..1e9fe7c64 100644
--- a/adcc/AdcMatrix.py
+++ b/adcc/AdcMatrix.py
@@ -27,6 +27,8 @@
from .LazyMp import LazyMp
from .adc_pp import matrix as ppmatrix
+from .adc_ip import matrix as ipmatrix
+from .adc_ea import matrix as eamatrix
from .timings import Timer, timed_member_call
from .AdcMethod import AdcMethod
from .functions import ones_like
@@ -73,6 +75,8 @@ class AdcMatrixlike:
_special_block_orders = {
"adc2x": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 1},
+ "ip-adc2x": {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 1},
+ "ea-adc2x": {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 1},
}
@classmethod
@@ -90,7 +94,9 @@ def _default_block_orders(cls, method: AdcMethod) -> dict[str, int]:
# - determine which spaces are available in the ADC(n) matrix
# starting from the given minimal space
min_space = {
- "pp": "ph"
+ "pp": "ph",
+ "ip": "h",
+ "ea": "p"
}.get(method.adc_type, None)
if min_space is None:
raise ValueError(f"Unknown adc type {method.adc_type} for method "
@@ -178,6 +184,10 @@ def _is_valid_space(cls, space: str, method: AdcMethod) -> bool:
# be equal or differ e.g. by +-1 (IP/EA)
if method.adc_type == "pp":
return n_particle == n_hole
+ elif method.adc_type == "ip":
+ return n_particle == n_hole - 1
+ elif method.adc_type == "ea":
+ return n_particle == n_hole + 1
raise ValueError(f"Unknown adc type {method.adc_type} for method "
f"{method.name}. Can not validate space.")
@@ -252,11 +262,19 @@ def __init__(self, method, hf_or_mp, block_orders=None, intermediates=None,
variant = None
if self.is_core_valence_separated:
variant = "cvs"
+ # Directly import block dispatch functions?
+ BLOCK_DISPATCH = {
+ "pp": ppmatrix.block,
+ "ip": ipmatrix.block,
+ "ea": eamatrix.block}
+ block_dispatch_fun = BLOCK_DISPATCH[self.method.adc_type]
blocks = {
- block: ppmatrix.block(self.ground_state, block.split("_"),
- order=order, intermediates=self.intermediates,
+ block: block_dispatch_fun(self.ground_state, block.split("_"),
+ order=order,
+ intermediates=self.intermediates,
variant=variant)
- for block, order in self.block_orders.items() if order is not None
+ for block, order in self.block_orders.items()
+ if order is not None
}
self.blocks = {bl: blocks[bl].apply for bl in blocks}
if diagonal_precomputed:
@@ -362,7 +380,8 @@ def block_apply(self, block, tensor):
with another AmplitudeVector or Tensor. Non-matching blocks
in the AmplitudeVector will be ignored.
"""
- if not isinstance(tensor, libadcc.Tensor):
+ # TODO: Allow AmplitudeVector?
+ if not isinstance(tensor, (libadcc.Tensor, AmplitudeVector)):
raise TypeError("tensor should be an adcc.Tensor")
with self.timer.record(f"apply/{block}"):
@@ -426,16 +445,30 @@ def construct_symmetrisation_for_blocks(self):
Returns a dictionary block identifier -> function
"""
ret = {}
- if self.is_core_valence_separated:
- # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a)
- ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)])
- else:
- def symmetrise_generic_adc_doubles(invec):
- # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a)
- scratch = invec.antisymmetrise([(2, 3)])
- # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a)
- return scratch.symmetrise([(0, 1), (2, 3)])
- ret["pphh"] = symmetrise_generic_adc_doubles
+ # TODO: IP/EA doubles
+ if self.method.adc_type == "pp":
+ if self.is_core_valence_separated:
+ # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a)
+ ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)])
+ else:
+ def symmetrise_generic_adc_doubles(invec):
+ # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a)
+ scratch = invec.antisymmetrise([(2, 3)])
+ # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a)
+ return scratch.symmetrise([(0, 1), (2, 3)])
+ ret["pphh"] = symmetrise_generic_adc_doubles
+ elif self.method.adc_type == "ip":
+ if not self.is_core_valence_separated:
+ def symmetrise_generic_adc_doubles(invec):
+ # doubles part is antisymmetric wrt. (i,j,a) <-> (j,i,a)
+ return invec.antisymmetrise([(0, 1)])
+ ret["phh"] = symmetrise_generic_adc_doubles
+ elif self.method.adc_type == "ea":
+ if not self.is_core_valence_separated:
+ def symmetrise_generic_adc_doubles(invec):
+ # doubles part is antisymmetric wrt. (i,a,b) <-> (i,a,b)
+ return invec.antisymmetrise([(1, 2)])
+ ret["pph"] = symmetrise_generic_adc_doubles
return ret
def dense_basis(self, axis_blocks=None, ordering="adcc"):
diff --git a/adcc/AdcMethod.py b/adcc/AdcMethod.py
index d1781e28e..347ed7c4e 100644
--- a/adcc/AdcMethod.py
+++ b/adcc/AdcMethod.py
@@ -24,10 +24,13 @@
def get_valid_methods():
valid_prefixes = ["cvs"]
+ valid_adc_types = ["ip", "ea"]
valid_bases = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
- ret = valid_bases + [p + "-" + m for p in valid_prefixes
- for m in valid_bases]
+ ret = (valid_bases
+ + [p + "-" + m for p in valid_prefixes for m in valid_bases]
+ + [t + "-" + m for t in valid_adc_types for m in valid_bases]
+ )
return ret
@@ -43,8 +46,13 @@ def __init__(self, method):
self.__base_method = split[-1]
split = split[:-1]
self.is_core_valence_separated = "cvs" in split
- # NOTE: added this to make the testdata generation ready for IP/EA
- self.adc_type = "pp"
+
+ if "ip" in split:
+ self.adc_type = "ip"
+ elif "ea" in split:
+ self.adc_type = "ea"
+ else:
+ self.adc_type = "pp"
try:
if self.__base_method == "adc2x":
@@ -59,17 +67,22 @@ def at_level(self, newlevel):
Return an equivalent method, where only the level is changed
(e.g. calling this on a CVS method returns a CVS method)
"""
+ name_str = "adc"
+ if self.adc_type != "pp":
+ name_str = self.adc_type + "-" + "adc"
if self.is_core_valence_separated:
- return AdcMethod("cvs-adc" + str(newlevel))
+ return AdcMethod("cvs-" + name_str + str(newlevel))
else:
- return AdcMethod("adc" + str(newlevel))
+ return AdcMethod(name_str + str(newlevel))
@property
def name(self):
+ name = self.__base_method
+ if self.adc_type != "pp":
+ name = self.adc_type + "-" + name
if self.is_core_valence_separated:
- return "cvs-" + self.__base_method
- else:
- return self.__base_method
+ name = "cvs-" + name
+ return name
@property
def property_method(self):
@@ -89,6 +102,8 @@ def base_method(self):
The base (full) method, i.e. with all approximations such as
CVS stripped off.
"""
+ if self.adc_type != "pp":
+ return AdcMethod(self.adc_type + "-" + self.__base_method)
return AdcMethod(self.__base_method)
def __eq__(self, other):
diff --git a/adcc/AmplitudeVector.py b/adcc/AmplitudeVector.py
index aff573b60..b08adad29 100644
--- a/adcc/AmplitudeVector.py
+++ b/adcc/AmplitudeVector.py
@@ -26,7 +26,9 @@ class AmplitudeVector(dict):
def __init__(self, **kwargs):
"""
Construct an AmplitudeVector. Typical use cases are
- ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``.
+ ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``. For IP-ADC
+ ``AmplitudeVector(h=tensor_singles, phh=tensor_doubles)``, and for
+ EA-ADC ``AmplitudeVector(p=tensor_singles, pph=tensor_doubles)``
"""
super().__init__(**kwargs)
diff --git a/adcc/ChargedExcitations.py b/adcc/ChargedExcitations.py
new file mode 100644
index 000000000..35d98266a
--- /dev/null
+++ b/adcc/ChargedExcitations.py
@@ -0,0 +1,307 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2019 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+import numpy as np
+from scipy import constants
+import warnings
+
+from .import adc_ip, adc_ea
+from .ElectronicStates import TableColumn, ElectronicStates, _timer_name
+from .functions import dot
+from .misc import cached_member_function
+
+
+class ChargedExcitation(ElectronicStates):
+
+ @property
+ def pole_strength(self) -> np.ndarray:
+ """Array of pole strengths of all computed states"""
+ return np.array([
+ self._pole_strength(i) for i in range(self.size)
+ ])
+
+
+ @cached_member_function(timer=_timer_name, separate_timings_by_args=False)
+ def _pole_strength(self, state_n: int) -> np.ndarray:
+ """Computes the pole strength for a single state"""
+ evec = self.excitation_vector[state_n]
+ return self._module.pole_strength(
+ self.property_method, self.ground_state, evec,
+ self.matrix.intermediates)
+
+ def describe_helper(self, pole_strengths=True, state_dipole_moments=False,
+ block_norms=True, excitation_type="energy", ssq=False):
+ """
+ Creates and returns the to be printed columns
+
+ Parameters
+ ----------
+ pole_strengths : bool optional
+ Show oscillator strengths, by default ``True``.
+
+ state_dipole_moments : bool, optional
+ Show state dipole moments, by default ``False``.
+
+ block_norms : bool, optional
+ Show the norms of the n particle (n-1) hole blocks of the charged
+ excited states, by default ``True``.
+
+ excitation_type : str, optional
+ Defines the name of the energy property.
+ 'ionization potential'/'electron affinity' for IP/EA
+ ssq : bool, optional
+ Show the values of the excited states, by default ``False``.
+ """
+ has_dipole = "electric_dipole" in self.operators.available
+ # Collect the columns to print
+ columns: list[TableColumn] = []
+ values: list[str] = []
+ # count the number of states
+ values.extend(str(i) for i in range(self.size))
+ columns.append(TableColumn(header="#", values=values.copy(), unit=""))
+ values.clear()
+ # excitation energy in a.u. and eV
+ eV = constants.value("Hartree energy in eV")
+ values.extend(f"{e:^13.7g} {e * eV:^13.7g}" for e in self.excitation_energy)
+ columns.append(TableColumn(
+ header=excitation_type, values=values.copy(),
+ unit="(au) (eV)"
+ ))
+ values.clear()
+ # the pole strengths
+ if pole_strengths:
+ values.extend(f"{pstr:^8.4f}" for pstr in self.pole_strength)
+ columns.append(TableColumn(
+ header="pole str", values=values.copy(), unit="(au)"
+ ))
+ values.clear()
+ # vector norm
+ blocks = self.matrix.axis_blocks
+ if block_norms and len(blocks) > 0:
+ values.extend(f"{dot(vec.get(blocks[0]), vec.get(blocks[0])):^9.4f}"
+ for vec in self.excitation_vector)
+ columns.append(TableColumn(
+ header="|v1|^2", values=values.copy(), unit=""
+ ))
+ values.clear()
+ if block_norms and len(blocks) > 1:
+ values.extend(f"{dot(vec.get(blocks[1]), vec.get(blocks[1])):^9.4f}"
+ for vec in self.excitation_vector)
+ columns.append(TableColumn(
+ header="|v2|^2", values=values.copy(), unit=""
+ ))
+ values.clear()
+ # the state dipole moment
+ if state_dipole_moments and has_dipole:
+ warnings.warn("Dipole moments of charged species are gauge "
+ "dependent.")
+ for dm in self.state_dipole_moment:
+ dmx, dmy, dmz = dm
+ values.append(
+ f"{dmx:^8.4f} {dmy:^8.4f} {dmz:^8.4f}"
+ f"{np.linalg.norm(dm):^8.4f}"
+ )
+ columns.append(TableColumn(
+ header="state dipole moment", values=values.copy(),
+ unit="x(au) y(au) z(au) abs(au)"
+ ))
+ values.clear()
+ # values
+ if ssq and not self.reference_state.restricted:
+ values.extend(f"{ssq:^9.4f}"
+ for ssq in self.state_ssq)
+ columns.append(TableColumn(
+ header="", values=values.copy(), unit="(au)"
+ ))
+ values.clear()
+
+ return columns
+
+
+class DetachedStates(ChargedExcitation):
+ _module = adc_ip
+
+ def __init__(self, data, is_alpha: bool, method: str = None,
+ property_method: str = None):
+ self.is_alpha = is_alpha
+ super().__init__(data, method, property_method)
+
+ if self.method.adc_type != "ip":
+ raise ValueError("DetachedStates computes excited state properties"
+ " for IP-ADC. Got the non-IP-ADC method "
+ f"{self.method.name}")
+
+ def describe(self, pole_strengths=True, state_dipole_moments=False,
+ block_norms=True, ssq=False):
+ """
+ Return a string providing a human-readable description of the class
+
+ Parameters
+ ----------
+ pole_strengths : bool optional
+ Show oscillator strengths, by default ``True``.
+
+ state_dipole_moments : bool, optional
+ Show state dipole moments, by default ``False``.
+
+ block_norms : bool, optional
+ Show the norms of the n particle (n+1) hole blocks of the charged
+ excited states, by default ``True``.
+ """
+ assert (self.matrix.axis_blocks == ["h"]
+ or self.matrix.axis_blocks == ["h", "phh"])
+ columns = self.describe_helper(
+ pole_strengths=pole_strengths,
+ state_dipole_moments=state_dipole_moments,
+ block_norms=block_norms,
+ excitation_type="ionization potential",
+ ssq=ssq)
+
+ # Format the state information: kind, spin_change,
+ # alpha/beta detachment, and convergence
+ state_info = []
+ if hasattr(self, "kind") and self.kind:
+ state_info.append(self.kind)
+ spin_type = "alpha" if self.is_alpha else "beta"
+ state_info.append(f"(ΔMS={self.spin_change:+.1f}), "
+ f"{spin_type} detachment")
+ if hasattr(self, "converged"):
+ conv = "converged" if self.converged else "NOT CONVERGED"
+ if state_info: # add separator to previous entry
+ state_info[-1] += ","
+ state_info.append(conv)
+ state_info = " ".join(state_info)
+ return self._describe(columns, state_info)
+
+ def to_qcvars(self, properties=False, recurse=False):
+ """
+ Return a dictionary with property keys compatible to a Psi4 wavefunction
+ or a QCEngine Atomicresults object.
+ """
+ name = self.method.name.upper()
+
+ qcvars = {
+ "EXCITATION KIND": self.kind.upper(),
+ "NUMBER OF IONIZED STATES": len(self.excitation_energy),
+ f"{name} ITERATIONS": self.n_iter,
+ f"{name} IONIZATION POTENTIALS": self.excitation_energy,
+ }
+
+ if properties:
+ qcvars.update({
+ # Transition properties
+ f"{name} POLE STRENGTHS": self.pole_strength,
+ #
+ # State properties
+ f"{name} STATE DIPOLES": self.state_dipole_moment
+ })
+
+ if recurse:
+ mpvars = self.ground_state.to_qcvars(properties, recurse=True,
+ maxlevel=self.method.level)
+ qcvars.update(mpvars)
+ return qcvars
+
+
+class AttachedStates(ChargedExcitation):
+ _module = adc_ea
+
+ def __init__(self, data, is_alpha: bool, method: str = None,
+ property_method: str = None):
+ self.is_alpha = is_alpha
+ super().__init__(data, method, property_method)
+
+ if self.method.adc_type != "ea":
+ raise ValueError("DetachedStates computes excited state properties"
+ " for EA-ADC. Got the non-EA-ADC method "
+ f"{self.method.name}")
+
+ def describe(self, pole_strengths=True, state_dipole_moments=False,
+ block_norms=True, ssq=False):
+ """
+ Return a string providing a human-readable description of the class
+
+ Parameters
+ ----------
+ pole_strengths : bool optional
+ Show oscillator strengths, by default ``True``.
+
+ state_dipole_moments : bool, optional
+ Show state dipole moments, by default ``False``.
+
+ block_norms : bool, optional
+ Show the norms of the n particle (n+1) hole blocks of the charged
+ excited states, by default ``True``.
+ """
+ assert (self.matrix.axis_blocks == ["p"]
+ or self.matrix.axis_blocks == ["p", "pph"])
+ columns = self.describe_helper(
+ pole_strengths=pole_strengths,
+ state_dipole_moments=state_dipole_moments,
+ block_norms=block_norms,
+ excitation_type="electron affinity",
+ ssq=ssq)
+
+ # Format the state information: kind, spin_change,
+ # alpha/beta detachment, and convergence
+ state_info = []
+ if hasattr(self, "kind") and self.kind:
+ state_info.append(self.kind)
+ spin_type = "alpha" if self.is_alpha else "beta"
+ state_info.append(f"(ΔMS={self.spin_change:+.1f}), "
+ f"{spin_type} attachment")
+ if hasattr(self, "converged"):
+ conv = "converged" if self.converged else "NOT CONVERGED"
+ if state_info: # add separator to previous entry
+ state_info[-1] += ","
+ state_info.append(conv)
+ state_info = " ".join(state_info)
+ return self._describe(columns, state_info)
+
+ def to_qcvars(self, properties=False, recurse=False):
+ """
+ Return a dictionary with property keys compatible to a Psi4 wavefunction
+ or a QCEngine Atomicresults object.
+ """
+ name = self.method.name.upper()
+
+ qcvars = {
+ "EXCITATION KIND": self.kind.upper(),
+ "NUMBER OF ELECTRON ATTACHED STATES": len(self.excitation_energy),
+ f"{name} ITERATIONS": self.n_iter,
+ f"{name} ELECTRON AFFINITIES": self.excitation_energy,
+ }
+
+ if properties:
+ qcvars.update({
+ # Transition properties
+ f"{name} POLE STRENGTHS": self.pole_strength,
+ #
+ # State properties
+ f"{name} STATE DIPOLES": self.state_dipole_moment
+ })
+
+ if recurse:
+ mpvars = self.ground_state.to_qcvars(properties, recurse=True,
+ maxlevel=self.method.level)
+ qcvars.update(mpvars)
+ return qcvars
\ No newline at end of file
diff --git a/adcc/ElectronicStates.py b/adcc/ElectronicStates.py
index 0ff92f447..23983a971 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))
@@ -109,7 +110,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
@@ -455,7 +456,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
@@ -481,25 +482,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}"
@@ -744,6 +731,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 ab3e82d86..5fa324cbc 100644
--- a/adcc/ExcitedStates.py
+++ b/adcc/ExcitedStates.py
@@ -147,7 +147,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)
@@ -155,7 +155,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/State2States.py b/adcc/State2States.py
index 257c2e5a7..b65b0896d 100644
--- a/adcc/State2States.py
+++ b/adcc/State2States.py
@@ -22,7 +22,7 @@
## ---------------------------------------------------------------------
import numpy as np
-from . import adc_pp
+from . import adc_pp, adc_ip, adc_ea
from .ElectronicStates import _timer_name
from .ElectronicTransition import ElectronicTransition
from .misc import cached_member_function
@@ -65,6 +65,10 @@ def __init__(self, data, method=None, property_method=None, initial=0):
# the module according to the method.
if self.method.adc_type == "pp":
self._module = adc_pp
+ elif self.method.adc_type == "ip":
+ self._module = adc_ip
+ elif self.method.adc_type == "ea":
+ self._module = adc_ea
else:
raise ValueError(f"Unknown adc_type {self.method.adc_type}.")
diff --git a/adcc/__init__.py b/adcc/__init__.py
index 872963c00..d96501da2 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.16.1"
@@ -141,6 +148,56 @@ def cvs_adc3(*args, **kwargs):
return run_adc(*args, **kwargs, method="cvs-adc3")
+@with_runadc_doc
+def ip_adc0(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ip-adc0")
+
+
+@with_runadc_doc
+def ip_adc1(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ip-adc1")
+
+
+@with_runadc_doc
+def ip_adc2(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ip-adc2")
+
+
+@with_runadc_doc
+def ip_adc2x(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ip-adc2x")
+
+
+@with_runadc_doc
+def ip_adc3(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ip-adc3")
+
+
+@with_runadc_doc
+def ea_adc0(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ea-adc0")
+
+
+@with_runadc_doc
+def ea_adc1(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ea-adc1")
+
+
+@with_runadc_doc
+def ea_adc2(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ea-adc2")
+
+
+@with_runadc_doc
+def ea_adc2x(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ea-adc2x")
+
+
+@with_runadc_doc
+def ea_adc3(*args, **kwargs):
+ return run_adc(*args, **kwargs, method="ea-adc3")
+
+
def banner(colour=sys.stdout.isatty()):
"""Return a nice banner describing adcc and its components
diff --git a/adcc/adc_ea/__init__.py b/adcc/adc_ea/__init__.py
new file mode 100644
index 000000000..22f213541
--- /dev/null
+++ b/adcc/adc_ea/__init__.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from .state_diffdm import state_diffdm
+from .state_diffdm_2p import state_diffdm_2p
+from .pole_strength import pole_strength
+from .state2state_transition_dm import state2state_transition_dm
+
+"""
+Submodule, which contains rather lengthy low-level kernels
+(e.g. matrix-vector products or working equations), which are called
+from the high-level objects in the adcc main module.
+"""
+
+__all__ = ["state_diffdm", "state_diffdm_2p", "state2state_transition_dm",
+ "pole_strength"]
diff --git a/adcc/adc_ea/matrix.py b/adcc/adc_ea/matrix.py
new file mode 100644
index 000000000..63deeb6c6
--- /dev/null
+++ b/adcc/adc_ea/matrix.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+from collections import namedtuple
+
+from adcc import block as b
+from adcc.functions import direct_sum, einsum
+from adcc.Intermediates import Intermediates, register_as_intermediate
+from adcc.AmplitudeVector import AmplitudeVector
+
+#
+# Dispatch routine lives in 'adc_pp/matrix.py'
+#
+__all__ = ["block"]
+
+AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"])
+
+
+def block(ground_state, spaces, order, variant=None, intermediates=None):
+ """
+ Gets ground state, potentially intermediates, spaces (ph, pphh and so on)
+ and the perturbation theory order for the block,
+ variant is "cvs" or sth like that.
+
+ It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space,
+ while one would probably want in the long run that one can have an "o2" space,
+ but not do CVS.
+ """
+ reference_state = ground_state.reference_state
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ fn = b.get_block_name(spaces, order, variant,
+ ground_state.has_core_occupied_space)
+
+ if fn not in globals():
+ raise ValueError("Could not dispatch: "
+ f"spaces={spaces} order={order} variant={variant}. "
+ "Probably the secular matrix is not implemented for "
+ "the requested method.")
+ return globals()[fn](reference_state, ground_state, intermediates)
+
+
+#
+# 0th order main
+#
+def block_p_p_0(hf, mp, intermediates):
+ # M_{11}
+ def apply(ampl):
+ return AmplitudeVector(p=einsum("ab,b->a", hf.fvv, ampl.p))
+ diagonal = AmplitudeVector(p=hf.fvv.diagonal())
+ return AdcBlock(apply, diagonal)
+
+
+def diagonal_pph_pph_0(hf):
+ res = direct_sum("-i+a+b->iab",
+ hf.foo.diagonal(), hf.fvv.diagonal(), hf.fvv.diagonal())
+ return AmplitudeVector(pph=res.symmetrise(1, 2))
+
+
+def block_pph_pph_0(hf, mp, intermediates):
+ # M_{22}
+ def apply(ampl):
+ return AmplitudeVector(pph=(
+ - einsum("jab,ij->iab", ampl.pph, hf.foo)
+ + 2 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2)
+ ))
+ return AdcBlock(apply, diagonal_pph_pph_0(hf))
+
+
+#
+# 1st order main
+#
+def block_p_p_1(hf, mp, intermediates):
+ # M_{11}, same as ADC(0)
+ return block_p_p_0(hf, mp, intermediates)
+
+
+def diagonal_pph_pph_1(hf):
+ # TODO
+ pass
+
+
+def block_pph_pph_1(hf, mp, intermediates):
+ # M_{22}
+ def apply(ampl):
+ return AmplitudeVector(pph=(
+ - einsum("jab,ij->iab", ampl.pph, hf.foo)
+ + 2 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2)
+ + 0.5 * einsum("abcd,icd->iab", hf.vvvv, ampl.pph)
+ - 2 * einsum("icka,kcb->iab", hf.ovov, ampl.pph
+ ).antisymmetrise(1, 2)
+ ))
+ return AdcBlock(apply, diagonal_pph_pph_0(hf))
+
+
+#
+# 1st order coupling
+#
+def block_p_pph_1(hf, mp, intermediates):
+ # M_{12}
+ def apply(ampl):
+ return AmplitudeVector(p=(
+ - 1 / sqrt(2) * einsum("jabc,jbc->a", hf.ovvv, ampl.pph)))
+ return AdcBlock(apply, 0)
+
+
+def block_pph_p_1(hf, mp, intermediates):
+ # M_{21}
+ def apply(ampl):
+ return AmplitudeVector(pph=(
+ - 1 / sqrt(2) * einsum("icab,c->iab", hf.ovvv, ampl.p)))
+ return AdcBlock(apply, 0)
+
+
+#
+# 2nd order main
+#
+def block_p_p_2(hf, mp, intermediates):
+ # M_{11}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i1 = intermediates.adc2_i1
+ diagonal = AmplitudeVector(p=i1.diagonal())
+
+ def apply(ampl):
+ return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p))
+ return AdcBlock(apply, diagonal)
+
+
+#
+# 2nd order coupling
+#
+def block_p_pph_2(hf, mp, intermediates):
+ # M_{12}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i2 = - intermediates.adc3_pib
+
+ def apply(ampl):
+ return AmplitudeVector(p=(
+ + 1 / sqrt(2) * einsum("jabc,jbc->a", i2, ampl.pph)))
+ return AdcBlock(apply, 0)
+
+
+def block_pph_p_2(hf, mp, intermediates):
+ # M_{21}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i2 = - intermediates.adc3_pib
+
+ def apply(ampl):
+ return AmplitudeVector(pph=(
+ + 1 / sqrt(2) * einsum("icab,c->iab", i2, ampl.p)))
+ return AdcBlock(apply, 0)
+
+
+#
+# 3rd order main
+#
+def block_p_p_3(hf, mp, intermediates):
+ # M_{11}
+ i1 = intermediates.adc3_ea_i1
+ diagonal = AmplitudeVector(p=i1.diagonal())
+
+ def apply(ampl):
+ return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p))
+ return AdcBlock(apply, diagonal)
+
+
+#
+# Intermediates
+#
+
+@register_as_intermediate
+def adc3_ea_i1(hf, mp, intermediates):
+ return (
+ hf.fvv + (
+ + 0.5 * einsum("ijac,ijbc->ab", mp.t2oo, hf.oovv)
+ - 0.25 * einsum("ijbc,ijac->ab", mp.t2oo, mp.t2eri(b.oovv, b.oo))
+ + 0.5 * einsum("ijbc,ijca->ab", mp.t2oo, mp.t2eri(b.oovv, b.vv))
+ + einsum("ijbc,jiac->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov))
+ - 2 * einsum("ijbc,jica->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov))
+ ).symmetrise()
+ + intermediates.sigma_vv
+ )
+
+
+@register_as_intermediate
+def sigma_vv(hf, mp, intermediates):
+ # Static self-energy, oo part \Sigma_{ij}(\infty)
+ p0 = mp.mp2_diffdm
+ return (einsum("iajb,ij->ab", hf.ovov, p0.oo)
+ + 2 * einsum("iacb,ic->ab", hf.ovvv, p0.ov)
+ + einsum("acbd,cd->ab", hf.vvvv, p0.vv)).symmetrise()
diff --git a/adcc/adc_ea/pole_strength.py b/adcc/adc_ea/pole_strength.py
new file mode 100644
index 000000000..9f5bd1654
--- /dev/null
+++ b/adcc/adc_ea/pole_strength.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2019 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like, dot, direct_sum
+from adcc.Intermediates import Intermediates, register_as_intermediate
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def pole_strength_ea_adc0(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+
+ # "Calculate" the spectroscopic amplitude x
+ xa = amplitude.p
+
+ return dot(xa, xa)
+
+
+def pole_strength_ea_adc2(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+ check_doubles_amplitudes([b.o, b.v, b.v], amplitude)
+ u1, u2 = amplitude.p, amplitude.pph
+
+ f11 = intermediates.ea_adc2_f11
+ f12 = - mp.mp2_diffdm.ov # -t_ia
+ f22 = intermediates.ea_adc2_f22
+
+ # Calculate the spectroscopic amplitude x
+ xi = einsum("ia,a->i", f12, u1) + einsum("ijab,jab->i", f22, u2)
+ xa = einsum("b,ba->a", u1, f11)
+
+ return dot(xi, xi) + dot(xa, xa)
+
+
+def pole_strength_ea_adc3(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+ check_doubles_amplitudes([b.o, b.v, b.v], amplitude)
+ u1, u2 = amplitude.p, amplitude.pph
+
+ f11 = intermediates.ea_adc3_f11
+ # TODO: when mp3_diffdm is implemented, intermediate can be directly reused
+ # to avoid redundancy
+ # f12 = - intermediates.mp3_diffdm.ov
+ f12 = intermediates.ea_adc3_f12
+ f22 = intermediates.ea_adc3_f22
+
+ # Calculate the spectroscopic amplitude x
+ xi = einsum("ia,a->i", f12, u1) + einsum("ijab,jab->i", f22, u2)
+ xa = einsum("b,ba->a", u1, f11)
+
+ return dot(xi, xi) + dot(xa, xa)
+
+
+#
+# Intermediates
+#
+
+@register_as_intermediate
+def ea_adc2_f11(hf, mp, intermediates):
+ # effective transition moments, vv part f_ab
+ # Build Kronecker delta
+ d_vv = zeros_like(hf.fvv)
+ d_vv.set_mask("aa", 1.0)
+
+ t2 = mp.t2(b.oovv)
+
+ return d_vv - 0.25 * einsum("ijbc,ijac->ab", t2, t2)
+
+
+@register_as_intermediate
+def ea_adc2_f22(hf, mp, intermediates):
+ # effective transition moments, oovv part f_ijab
+ return 1/sqrt(2) * mp.t2(b.oovv)
+
+
+@register_as_intermediate
+def ea_adc3_f11(hf, mp, intermediates):
+ # effective transition moments, vv part f_ab
+ # Build Kronecker delta
+ d_vv = zeros_like(hf.fvv)
+ d_vv.set_mask("aa", 1.0)
+
+ df = mp.df(b.ov)
+ df2 = direct_sum("ib+jc->ijbc", df, df).symmetrise((2, 3))
+
+ t2 = mp.t2(b.oovv)
+
+ return (d_vv
+ - 0.25 * einsum("ijbc,ijac->ab", t2, t2)
+ + (+ 0.25 * einsum("ijac,ijbc->ab", t2,
+ mp.t2eri(b.oovv, b.vv) / df2)
+ + 0.25 * einsum("ijac,ijbc->ab", t2,
+ mp.t2eri(b.oovv, b.oo) / df2)
+ + einsum("ijac,ijbc->ab", t2, mp.t2eri(b.oovv, b.ov) / df2)
+ - einsum("ijac,jibc->ab", t2, mp.t2eri(b.oovv, b.ov) / df2))
+ )
+
+
+@register_as_intermediate
+def ea_adc3_f12(hf, mp, intermediates):
+ # effective transition moments, ov part f_ia
+ # Intermediates are defined in /adcc/adc_ip/pole_strength.py
+ return (- mp.mp2_diffdm.ov + (intermediates.sigma_ov
+ + intermediates.m_3_plus
+ + intermediates.m_3_minus
+ ) / mp.df)
+
+
+@register_as_intermediate
+def ea_adc3_f22(hf, mp, intermediates):
+ # effective transition moments, oovv part f_ijab
+ df = mp.df(b.ov)
+ df2 = direct_sum("ia+jb->ijab", df, df).symmetrise((2, 3))
+
+ return (1/sqrt(2) * mp.t2(b.oovv)
+ + 1/sqrt(2) * (0.5 * (mp.t2eri(b.oovv, b.oo)
+ + mp.t2eri(b.oovv, b.vv))
+ + ((mp.t2eri(b.oovv, b.ov)
+ ).antisymmetrise(2, 3)).antisymmetrise(0, 1)
+ ) / df2
+ )
+
+
+DISPATCH = {
+ "ea-adc0": pole_strength_ea_adc0,
+ "ea-adc1": pole_strength_ea_adc0,
+ "ea-adc2": pole_strength_ea_adc2,
+ "ea-adc2x": pole_strength_ea_adc2,
+ "ea-adc3": pole_strength_ea_adc3,
+}
+
+
+def pole_strength(method, ground_state, amplitude, intermediates=None):
+ """Compute the pole strength of the electron attached state for the
+ provided ADC method from the spectroscopic amplitude x.
+
+ Parameters
+ ----------
+ method: adc.Method
+ Provide a method at which to compute the MTMs
+ ground_state : adcc.LazyMp
+ The MP ground state
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+
+ Returns
+ -------
+ Scalar
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+ if method.name not in DISPATCH:
+ raise NotImplementedError("pole_strength is not "
+ f"implemented for {method.name}.")
+
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret
diff --git a/adcc/adc_ea/state2state_transition_dm.py b/adcc/adc_ea/state2state_transition_dm.py
new file mode 100644
index 000000000..0b7991e81
--- /dev/null
+++ b/adcc/adc_ea/state2state_transition_dm.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2018 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.OneParticleDensity import OneParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def s2s_tdm_ea_adc0(mp, amplitude_l, amplitude_r, intermediates):
+ check_singles_amplitudes([b.v], amplitude_l, amplitude_r)
+ ul1 = amplitude_l.p
+ ur1 = amplitude_r.p
+
+ dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.NOSYMMETRY)
+ dm.vv = einsum("a,b->ab", ul1, ur1)
+ return dm
+
+
+def s2s_tdm_ea_adc2(mp, amplitude_l, amplitude_r, intermediates):
+ check_doubles_amplitudes([b.o, b.v, b.v], amplitude_l, amplitude_r)
+ dm = s2s_tdm_ea_adc0(mp, amplitude_l, amplitude_r, intermediates)
+
+ ul1, ul2 = amplitude_l.p, amplitude_l.pph
+ ur1, ur2 = amplitude_r.p, amplitude_r.pph
+
+ t2 = mp.t2(b.oovv)
+ p0 = mp.mp2_diffdm
+ p1_vv = dm.vv.evaluate() # ADC(1) diffdm
+
+ # Zeroth order doubles contributions
+ p2_oo = -einsum("jab,iab->ij", ul2, ur2)
+ p2_vv = 2 * einsum("iac,ibc->ab", ul2, ur2)
+ p_ov = sqrt(2) * einsum("b,iba->ia", ul1, ur2)
+ p_vo = sqrt(2) * einsum("iba,b->ai", ul2, ur1)
+
+ # ADC(2) ISR intermediate (TODO Move to intermediates)
+# ru1 = einsum("i,ijab->jab", u1, t2).evaluate()
+
+ # Compute second-order contributions to the density matrix
+ dm.oo = ( # ea_adc2_p_oo
+ + p2_oo
+ + einsum("ikc,jkc->ij", einsum("a,ikac->ikc", ul1, t2),
+ einsum("b,jkbc->jkc", ur1, t2))
+ )
+
+ dm.vv = ( # ea_adc2_p_vv
+ + p1_vv + p2_vv
+ - 0.5 * einsum("c,ac,b->ab", ul1, p0.vv, ur1)
+ - 0.5 * einsum("a,bc,c->ab", ul1, p0.vv, ur1)
+ + 0.5 * einsum("ijb,ija->ab", einsum("c,ijcb->ijb", ul1, t2),
+ einsum("d,ijad->ija", ur1, t2))
+ )
+
+ dm.ov = ( # ea_adc2_p_ov
+ + p_ov
+ + 1/sqrt(2) * (
+ + einsum("jbc,ijbc,a->ia", ul2, t2, ur1)
+ + 2 * einsum("jc,ijac->ia", einsum("jcb,b->jc", ul2, ur1), t2))
+ - einsum("b,ib,a->ia", ul1, p0.ov, ur1)
+ )
+
+ dm.vo = ( # ea_adc2_p_vo
+ + p_vo
+ + 1/sqrt(2) * (
+ + einsum("a,ijbc,jbc->ai", ul1, t2, ur2)
+ + 2 * einsum("jc,ijac->ai", einsum("b,jcb->jc", ul1, ur2), t2))
+ - einsum("b,ib,a->ai", ur1, p0.ov, ul1)
+ # switched indices because p0.ov is used instead of p0.vo
+ )
+ return dm
+
+
+DISPATCH = {
+ "ea-adc0": s2s_tdm_ea_adc0,
+ "ea-adc1": s2s_tdm_ea_adc0, # same as ADC(0)
+ "ea-adc2": s2s_tdm_ea_adc2,
+ "ea-adc2x": s2s_tdm_ea_adc2, # same as ADC(2)
+}
+
+
+def state2state_transition_dm(method, ground_state, amplitude_from,
+ amplitude_to, intermediates=None):
+ """
+ Compute the state to state transition density matrix
+ state in the MO basis using the intermediate-states representation.
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude_from : AmplitudeVector
+ The amplitude vector of the state to start from
+ amplitude_to : AmplitudeVector
+ The amplitude vector of the state to excite to
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude_from, AmplitudeVector):
+ raise TypeError("amplitude_from should be an AmplitudeVector object.")
+ if not isinstance(amplitude_to, AmplitudeVector):
+ raise TypeError("amplitude_to should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state2state_transition_dm is not "
+ f"implemented for {method.name}.")
+ else:
+ # final state is on the bra side/left (complex conjugate)
+ # see ref https://doi.org/10.1080/00268976.2013.859313, appendix A2
+ ret = DISPATCH[method.name](ground_state, amplitude_to, amplitude_from,
+ intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ea/state_diffdm.py b/adcc/adc_ea/state_diffdm.py
new file mode 100644
index 000000000..160aa2fac
--- /dev/null
+++ b/adcc/adc_ea/state_diffdm.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.OneParticleDensity import OneParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def diffdm_ea_adc0(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+ u1 = amplitude.p
+
+ dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN)
+ dm.vv = einsum("a,b->ab", u1, u1)
+ return dm
+
+
+def diffdm_ea_adc2(mp, amplitude, intermediates):
+ dm = diffdm_ea_adc0(mp, amplitude, intermediates) # Get ADC(0/1) result
+ check_doubles_amplitudes([b.o, b.v, b.v], amplitude)
+ u1, u2 = amplitude.p, amplitude.pph
+
+ t2 = mp.t2(b.oovv)
+ p0 = mp.mp2_diffdm
+ p1_vv = dm.vv.evaluate() # ADC(1) diffdm
+
+ # Zeroth order doubles contributions
+ p2_oo = -einsum("jab,iab->ij", u2, u2)
+ p2_vv = 2 * einsum("iac,ibc->ab", u2, u2)
+ p_ov = sqrt(2) * einsum("b,iba->ia", u1, u2)
+
+ # ADC(2) ISR intermediate (TODO Move to intermediates)
+ # ru1 = einsum("i,ijab->jab", u1, t2).evaluate()
+
+ # Compute second-order contributions to the density matrix
+ dm.oo = ( # ea_adc2_p_oo
+ + p2_oo
+ + einsum("ikc,jkc->ij", einsum("a,ikac->ikc", u1, t2),
+ einsum("b,jkbc->jkc", u1, t2))
+ )
+
+ dm.vv = ( # ea_adc2_p_vv
+ + p1_vv + p2_vv
+ - 0.5 * einsum("c,ac,b->ab", u1, p0.vv, u1)
+ - 0.5 * einsum("a,bc,c->ab", u1, p0.vv, u1)
+ + 0.5 * einsum("ijb,ija->ab", einsum("c,ijcb->ijb", u1, t2),
+ einsum("d,ijad->ija", u1, t2))
+ )
+
+ dm.ov = ( # ea_adc2_p_ov
+ + p_ov
+ + 1/sqrt(2) * (
+ + einsum("jbc,ijbc,a->ia", u2, t2, u1)
+ + 2 * einsum("jc,ijac->ia", einsum("jcb,b->jc", u2, u1), t2))
+ - einsum("b,ib,a->ia", u1, p0.ov, u1)
+ )
+ return dm
+
+
+# dict controlling the dispatch of the state_diffdm function
+DISPATCH = {
+ "ea-adc0": diffdm_ea_adc0,
+ "ea-adc1": diffdm_ea_adc0, # same as ADC(0)
+ "ea-adc2": diffdm_ea_adc2,
+ "ea-adc2x": diffdm_ea_adc2, # same as ADC(2)
+}
+
+
+def state_diffdm(method, ground_state, amplitude, intermediates=None):
+ """
+ Compute the one-particle difference density matrix of an excited state
+ in the MO basis.
+
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude, AmplitudeVector):
+ raise TypeError("amplitude should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state_diffdm is not implemented "
+ f"for {method.name}.")
+ else:
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ea/state_diffdm_2p.py b/adcc/adc_ea/state_diffdm_2p.py
new file mode 100644
index 000000000..a26b5e4b2
--- /dev/null
+++ b/adcc/adc_ea/state_diffdm_2p.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2026 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.TwoParticleDensity import TwoParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+from math import sqrt
+
+
+def diffdm_ea_adc0_2p(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+ u1 = amplitude.p
+
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN)
+
+ # TODO: Store intermediate?
+ # p1_vv = einsum("a,b->ab", u1, u1).evaluate()
+
+ dm.ovov = (
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 1.0 * einsum("ab,ij->iajb", einsum("a,b->ab", u1, u1), d_oo)
+ )
+ return dm
+
+
+def diffdm_ea_adc1_2p(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.v], amplitude)
+ u1 = amplitude.p
+
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN)
+
+ # ADC(1) diffdm
+ t2 = mp.t2(b.oovv)
+
+ dm.oovv = (
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 2.0 * einsum("ija,b->ijab", einsum("c,ijac->ija", u1, t2), u1).antisymmetrise(2, 3)
+ )
+ return dm
+
+
+def diffdm_ea_adc2_2p(mp, amplitude, intermediates):
+ dm = diffdm_ea_adc1_2p(mp, amplitude, intermediates) # Get ADC(1) result
+ check_doubles_amplitudes([b.o, b.v, b.v], amplitude)
+ u1, u2 = amplitude.p, amplitude.pph
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ t2 = mp.t2(b.oovv)
+ td2 = mp.td2(b.oovv)
+ p0 = mp.mp2_diffdm
+
+ dm.oooo += (
+ 4.0 * (
+ # N^4: O^2V^2 / N^4: O^4
+ + 1.0 * einsum("il,jk->ijkl", einsum("iab,lab->il", u2, u2), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 1.0 * einsum("ik,jl->ijkl", einsum("kmc,imc->ik", einsum("b,kmbc->kmc", u1, t2), einsum("a,imac->imc", u1, t2)), d_oo)
+ ).antisymmetrise(0, 1).antisymmetrise(2, 3)
+ # N^5: O^4V^1 / N^4: O^2V^2
+ - 1.0 * einsum("klc,ijc->ijkl", einsum("b,klbc->klc", u1, t2), einsum("a,ijac->ijc", u1, t2))
+ )
+ dm.ooov += (
+ 2.0 * (
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + sqrt(2) * einsum("ia,jk->ijka", einsum("b,iab->ia", u1, u2), d_oo)
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + 1 * einsum("ia,jk->ijka", einsum("i,a->ia", einsum("b,ib->i", u1, p0.ov), u1), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + sqrt(2) * einsum("ja,ik->ijka", einsum("lb,jlab->ja", einsum("c,lbc->lb", u1, u2), t2), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 1.0 / sqrt(2) * einsum("ja,ik->ijka", einsum("j,a->ja", einsum("lbc,jlbc->j", u2, t2), u1), d_oo)
+ ).antisymmetrise(0, 1)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ + sqrt(2) * einsum("kb,ijab->ijka", einsum("c,kbc->kb", u1, u2), t2)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ + 1.0 / sqrt(2) * einsum("ijk,a->ijka", einsum("kbc,ijbc->ijk", u2, t2), u1)
+
+ )
+ dm.oovv += (
+ 2.0 * einsum("ija,b->ijab", einsum("c,ijac->ija", u1, td2), u1).antisymmetrise(2, 3)
+ )
+ dm.ovov += (
+ # N^5: O^2V^3 / N^4: O^2V^2
+ - 2.0 * einsum("jac,ibc->iajb", u2, u2)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 1.0 * einsum("ab,ij->iajb", einsum("a,b->ab", u1, u1), p0.oo)
+ # N^4: O^1V^3 / N^4: O^2V^2
+ + 2.0 * einsum("ab,ij->iajb", einsum("kac,kbc->ab", u2, u2), d_oo)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ + 1.0 * einsum("ikb,jka->iajb", einsum("d,ikbd->ikb", u1, t2), einsum("c,jkac->jka", u1, t2))
+ # N^4: O^2V^2 / N^4: O^2V^2
+ - 0.5 * einsum("ab,ij->iajb", einsum("klb,kla->ab", einsum("d,klbd->klb", u1, t2), einsum("c,klac->kla", u1, t2)), d_oo)
+ + 2.0 * (
+ # N^5: O^3V^2 / N^4: O^2V^2
+ - 1 * einsum("ijb,a->iajb", einsum("jkc,ikbc->ijb", einsum("d,jkcd->jkc", u1, t2), t2), u1)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ - 0.5 * einsum("ab,ij->iajb", einsum("b,a->ab", einsum("c,bc->b", u1, p0.vv), u1), d_oo)
+ ).symmetrise([(0, 2), (1, 3)])
+ )
+ dm.ovvv += (
+ (
+ # N^4: O^1V^3 / N^4: O^1V^3
+ + 1 * einsum("ac,ib->iabc", einsum("a,c->ac", u1, u1), p0.ov)
+ # N^5: O^2V^3 / N^4: O^1V^3
+ + sqrt(2) * einsum("iac,b->iabc", einsum("jad,ijcd->iac", u2, t2), u1)
+ ).antisymmetrise(2, 3)
+ # N^4: O^1V^3 / N^4: O^1V^3
+ - sqrt(2) * einsum("a,ibc->iabc", u1, u2)
+ # N^5: O^2V^3 / N^4: O^1V^3
+ + sqrt(2) * einsum("ja,ijbc->iabc", einsum("d,jad->ja", u1, u2), t2)
+ )
+ dm.vvvv += (
+ # N^5: O^1V^4 / N^4: V^4
+ + 2.0 * einsum("iab,icd->abcd", u2, u2)
+ + 1.0 * (
+ # N^4: V^4 / N^4: V^4
+ + 4.0 * einsum("ac,bd->abcd", einsum("a,c->ac", u1, u1), p0.vv)
+ ).antisymmetrise(0, 1).antisymmetrise(2, 3)
+ # N^5: O^2V^3 / N^4: V^4
+ + 1.0 * (
+ 2.0 * einsum("bcd,a->abcd", einsum("ijb,ijcd->bcd", einsum("e,ijbe->ijb", u1, t2), t2), u1)
+ ).antisymmetrise(0, 1).symmetrise([(0, 2), (1, 3)])
+ )
+ return dm
+
+
+# dict controlling the dispatch of the state_diffdm function
+DISPATCH = {
+ "ea-adc0": diffdm_ea_adc0_2p,
+ "ea-adc1": diffdm_ea_adc1_2p,
+ "ea-adc2": diffdm_ea_adc2_2p,
+ "ea-adc2x": diffdm_ea_adc2_2p, # same as ADC(2)
+}
+
+
+def state_diffdm_2p(method, ground_state, amplitude, intermediates=None):
+ """
+ Compute the two-particle difference density matrix of an excited state
+ in the MO basis.
+
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude, AmplitudeVector):
+ raise TypeError("amplitude should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state_diffdm_2p is not implemented "
+ f"for {method.name}.")
+ else:
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ea/util.py b/adcc/adc_ea/util.py
new file mode 100644
index 000000000..951a5212c
--- /dev/null
+++ b/adcc/adc_ea/util.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+
+
+def check_singles_amplitudes(spaces, *amplitudes):
+ check_have_singles_block(*amplitudes)
+ check_singles_subspaces(spaces, *amplitudes)
+
+
+def check_doubles_amplitudes(spaces, *amplitudes):
+ check_have_doubles_block(*amplitudes)
+ check_doubles_subspaces(spaces, *amplitudes)
+
+
+def check_have_singles_block(*amplitudes):
+ if any("p" not in amplitude.keys() for amplitude in amplitudes):
+ raise ValueError("ADC(0) level and "
+ "beyond expects an excitation amplitude with a "
+ "singles part.")
+
+
+def check_have_doubles_block(*amplitudes):
+ if any("pph" not in amplitude.keys() for amplitude in amplitudes):
+ raise ValueError("ADC(2) level and "
+ "beyond expects an excitation amplitude with a "
+ "singles and a doubles part.")
+
+
+def check_singles_subspaces(spaces, *amplitudes):
+ for amplitude in amplitudes:
+ u1 = amplitude.p
+ if u1.subspaces != spaces:
+ raise ValueError("Mismatch in subspaces singles part "
+ f"(== {u1.subspaces}), where {spaces} "
+ "was expected.")
+
+
+def check_doubles_subspaces(spaces, *amplitudes):
+ for amplitude in amplitudes:
+ u2 = amplitude.pph
+ if u2.subspaces != spaces:
+ raise ValueError("Mismatch in subspaces doubles part "
+ f"(== {u2.subspaces}), where "
+ f"{spaces} was expected.")
diff --git a/adcc/adc_ip/__init__.py b/adcc/adc_ip/__init__.py
new file mode 100644
index 000000000..22f213541
--- /dev/null
+++ b/adcc/adc_ip/__init__.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from .state_diffdm import state_diffdm
+from .state_diffdm_2p import state_diffdm_2p
+from .pole_strength import pole_strength
+from .state2state_transition_dm import state2state_transition_dm
+
+"""
+Submodule, which contains rather lengthy low-level kernels
+(e.g. matrix-vector products or working equations), which are called
+from the high-level objects in the adcc main module.
+"""
+
+__all__ = ["state_diffdm", "state_diffdm_2p", "state2state_transition_dm",
+ "pole_strength"]
diff --git a/adcc/adc_ip/matrix.py b/adcc/adc_ip/matrix.py
new file mode 100644
index 000000000..9b2a56606
--- /dev/null
+++ b/adcc/adc_ip/matrix.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+from collections import namedtuple
+
+from adcc import block as b
+from adcc.functions import direct_sum, einsum, zeros_like
+from adcc.Intermediates import Intermediates, register_as_intermediate
+from adcc.AmplitudeVector import AmplitudeVector
+
+#
+# Dispatch routine lives in 'adc_pp/matrix.py'
+#
+__all__ = ["block"]
+
+AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"])
+
+
+def block(ground_state, spaces, order, variant=None, intermediates=None):
+ """
+ Gets ground state, potentially intermediates, spaces (ph, pphh and so on)
+ and the perturbation theory order for the block,
+ variant is "cvs" or sth like that.
+
+ It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space,
+ while one would probably want in the long run that one can have an "o2" space,
+ but not do CVS.
+ """
+ reference_state = ground_state.reference_state
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ fn = b.get_block_name(spaces, order, variant,
+ ground_state.has_core_occupied_space)
+
+ if fn not in globals():
+ raise ValueError("Could not dispatch: "
+ f"spaces={spaces} order={order} variant={variant}. "
+ "Probably the secular matrix is not implemented for "
+ "the requested method.")
+ return globals()[fn](reference_state, ground_state, intermediates)
+
+
+#
+# 0th order main
+#
+def block_h_h_0(hf, mp, intermediates):
+ # M_{11}
+ def apply(ampl):
+ return AmplitudeVector(h=-einsum("ij,j->i", hf.foo, ampl.h))
+ diagonal = AmplitudeVector(h=-hf.foo.diagonal())
+ return AdcBlock(apply, diagonal)
+
+
+def diagonal_phh_phh_0(hf):
+ fCC = hf.fcc if hf.has_core_occupied_space else hf.foo
+ res = direct_sum("-i-J+a->iJa",
+ hf.foo.diagonal(), fCC.diagonal(), hf.fvv.diagonal())
+ return AmplitudeVector(phh=res)
+
+
+def block_phh_phh_0(hf, mp, intermediates):
+ # M_{22}
+ def apply(ampl):
+ return AmplitudeVector(phh=(
+ + einsum("ab,ijb->ija", hf.fvv, ampl.phh)
+ - 2 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1)
+ ))
+ return AdcBlock(apply, diagonal_phh_phh_0(hf))
+
+
+#
+# 1st order main
+#
+def block_h_h_1(hf, mp, intermediates):
+ # M_{11}, same as ADC(0)
+ return block_h_h_0(hf, mp, intermediates)
+
+
+def diagonal_phh_phh_1(hf):
+ fCC = hf.fcc if hf.has_core_occupied_space else hf.foo
+ i1 = direct_sum("-i-J+a->iJa",
+ hf.foo.diagonal(), fCC.diagonal(), hf.fvv.diagonal())
+
+ # Build Kronecker delta
+ d_vv = zeros_like(hf.fvv)
+ d_vv.set_mask("aa", 1.0)
+
+ i2 = einsum("ijij,aa->ija", hf.oooo, d_vv)
+ res = i1 + i2
+ return AmplitudeVector(phh=res.symmetrise(0, 1))
+
+
+def block_phh_phh_1(hf, mp, intermediates):
+ # M_{22}
+ def apply(ampl):
+ return AmplitudeVector(phh=(
+ + einsum("ac,ijc->ija", hf.fvv, ampl.phh)
+ - 2 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1)
+ + 0.5 * einsum("ijkl,kla->ija", hf.oooo, ampl.phh)
+ - 2 * einsum("kaic,kjc->ija", hf.ovov, ampl.phh
+ ).antisymmetrise(0, 1)
+ ))
+ return AdcBlock(apply, diagonal_phh_phh_1(hf))
+
+
+#
+# 1st order coupling
+#
+def block_h_phh_1(hf, mp, intermediates):
+ # M_{12}
+ def apply(ampl):
+ return AmplitudeVector(h=(
+ + 1 / sqrt(2) * einsum("jkib,jkb->i", hf.ooov, ampl.phh)))
+ return AdcBlock(apply, 0)
+
+
+def block_phh_h_1(hf, mp, intermediates):
+ # M_{21}
+ def apply(ampl):
+ return AmplitudeVector(phh=(
+ + 1 / sqrt(2) * einsum("ijka,k->ija", hf.ooov, ampl.h)))
+ return AdcBlock(apply, 0)
+
+
+#
+# 2nd order main
+#
+def block_h_h_2(hf, mp, intermediates):
+ # M_{11}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i1 = - intermediates.adc2_i2
+ diagonal = AmplitudeVector(h=i1.diagonal())
+
+ def apply(ampl):
+ return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h))
+ return AdcBlock(apply, diagonal)
+
+
+#
+# 2nd order coupling
+#
+def block_h_phh_2(hf, mp, intermediates):
+ # M_{12}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i2 = intermediates.adc3_pia
+
+ def apply(ampl):
+ return AmplitudeVector(h=(
+ + 1 / sqrt(2) * einsum("jkib,jkb->i", i2, ampl.phh)))
+ return AdcBlock(apply, 0)
+
+
+def block_phh_h_2(hf, mp, intermediates):
+ # M_{21}
+ # Intermediate can be found in 'adc_pp/matrix.py'
+ i2 = intermediates.adc3_pia
+
+ def apply(ampl):
+ return AmplitudeVector(phh=(
+ + 1 / sqrt(2) * einsum("ijka,k->ija", i2, ampl.h)))
+ return AdcBlock(apply, 0)
+
+
+#
+# 3rd order main
+#
+def block_h_h_3(hf, mp, intermediates):
+ # M_{11}
+ i1 = intermediates.adc3_ip_i1
+ diagonal = AmplitudeVector(h=i1.diagonal())
+
+ def apply(ampl):
+ return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h))
+ return AdcBlock(apply, diagonal)
+
+
+#
+# Intermediates
+#
+
+@register_as_intermediate
+def adc3_ip_i1(hf, mp, intermediates):
+ return (
+ - hf.foo + (
+ + 0.5 * einsum("ikab,jkab->ij", mp.t2oo, hf.oovv)
+ - 0.25 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.vv))
+ + 0.5 * einsum("jkab,ikba->ij", mp.t2oo, mp.t2eri(b.oovv, b.oo))
+ + einsum("jkab,kiab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov))
+ - 2 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov))
+ ).symmetrise()
+ - intermediates.sigma_oo
+ )
+
+
+@register_as_intermediate
+def sigma_oo(hf, mp, intermediates):
+ # Static self-energy, oo part \Sigma_{ij}(\infty)
+ p0 = mp.mp2_diffdm
+ return (einsum("ikjl,kl->ij", hf.oooo, p0.oo)
+ + 2 * einsum("ikja,ka->ij", hf.ooov, p0.ov)
+ + einsum("iajb,ab->ij", hf.ovov, p0.vv)).symmetrise()
diff --git a/adcc/adc_ip/pole_strength.py b/adcc/adc_ip/pole_strength.py
new file mode 100644
index 000000000..fc1ec6b8b
--- /dev/null
+++ b/adcc/adc_ip/pole_strength.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2019 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like, dot, direct_sum
+from adcc.Intermediates import Intermediates, register_as_intermediate
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def pole_strength_ip_adc0(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.o], amplitude)
+
+ # "Calculate" the spectroscopic amplitude x
+ xi = amplitude.h
+
+ return dot(xi, xi)
+
+
+def pole_strength_ip_adc2(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.o], amplitude)
+ check_doubles_amplitudes([b.o, b.o, b.v], amplitude)
+ u1, u2 = amplitude.h, amplitude.phh
+
+ f11 = intermediates.ip_adc2_f11
+ f12 = mp.mp2_diffdm.ov # t_ia
+ f22 = intermediates.ip_adc2_f22
+
+ # Calculate the spectroscopic amplitude x
+ xi = einsum("j,ji->i", u1, f11)
+ xa = einsum("j,ja->a", u1, f12) + einsum("ijb,ijba->a", u2, f22)
+
+ return dot(xi, xi) + dot(xa, xa)
+
+
+def pole_strength_ip_adc3(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.o], amplitude)
+ check_doubles_amplitudes([b.o, b.o, b.v], amplitude)
+ u1, u2 = amplitude.h, amplitude.phh
+
+ f11 = intermediates.ip_adc3_f11
+ # TODO: when mp3_diffdm is implemented, intermediate can be directly reused
+ # to avoid redundancy
+ # f12 = intermediates.mp3_diffdm.ov
+ f12 = intermediates.ip_adc3_f12
+ f22 = intermediates.ip_adc3_f22
+
+ # Calculate the spectroscopic amplitude x
+ xi = einsum("j,ji->i", u1, f11)
+ xa = einsum("j,ja->a", u1, f12) + einsum("ijb,ijba->a", u2, f22)
+
+ return dot(xi, xi) + dot(xa, xa)
+
+
+#
+# Intermediates
+#
+
+@register_as_intermediate
+def ip_adc2_f11(hf, mp, intermediates):
+ # effective transition moments, oo part f_ij
+ # Build Kronecker delta
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1.0)
+
+ t2 = mp.t2(b.oovv)
+
+ return d_oo - 0.25 * einsum("ilab,jlab->ij", t2, t2)
+
+
+@register_as_intermediate
+def ip_adc2_f22(hf, mp, intermediates):
+ # effective transition moments, oovv part f_ijab
+ return - 1/sqrt(2) * mp.t2(b.oovv)
+
+
+@register_as_intermediate
+def ip_adc3_f11(hf, mp, intermediates):
+ # effective transition moments, oo part f_ij
+ # Build Kronecker delta
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1.0)
+
+ df = mp.df(b.ov)
+ df2 = direct_sum("ia+kb->ikab", df, df).symmetrise((0, 1))
+
+ t2 = mp.t2(b.oovv)
+
+ return (d_oo
+ - 0.25 * einsum("ilab,jlab->ij", t2, t2)
+ + (+ 0.25 * einsum("jkab,ikab->ij", t2,
+ mp.t2eri(b.oovv, b.vv) / df2)
+ + 0.25 * einsum("jkab,ikab->ij", t2,
+ mp.t2eri(b.oovv, b.oo) / df2)
+ + einsum("jkab,ikab->ij", t2, mp.t2eri(b.oovv, b.ov) / df2)
+ - einsum("jkab,kiab->ij", t2, mp.t2eri(b.oovv, b.ov) / df2))
+ )
+
+
+@register_as_intermediate
+def ip_adc3_f12(hf, mp, intermediates):
+ # effective transition moments, ov part f_ia
+ return mp.mp2_diffdm.ov - (intermediates.sigma_ov
+ + intermediates.m_3_plus
+ + intermediates.m_3_minus
+ ) / mp.df(b.ov)
+
+
+@register_as_intermediate
+def ip_adc3_f22(hf, mp, intermediates):
+ # effective transition moments, oovv part f_ijab
+ df = mp.df(b.ov)
+ df2 = direct_sum("ia+jb->ijab", df, df).symmetrise((2, 3))
+
+ return (- 1/sqrt(2) * mp.t2(b.oovv)
+ + 1/sqrt(2) * (0.5 * (mp.t2eri(b.oovv, b.oo)
+ + mp.t2eri(b.oovv, b.vv))
+ + ((mp.t2eri(b.oovv, b.ov)
+ ).antisymmetrise(2, 3)).antisymmetrise(0, 1)
+ ) / df2
+ )
+
+
+# TODO: Intermediates are also necessary in LazyMP, avoid redundancy
+# TODO: Proper testing against Q-Chem of the pole strengths
+@register_as_intermediate
+def sigma_ov(hf, mp, intermediates):
+ # Static self-energy, oo part \Sigma_{ij}(\infty)
+ p0 = mp.mp2_diffdm
+ return (+ einsum("jika,jk->ia", hf.ooov, p0.oo)
+ + einsum("ijab,jb->ia", hf.oovv, p0.ov)
+ - einsum("ibja,jb->ia", hf.ovov, p0.ov)
+ + einsum("ibac,bc->ia", hf.ovvv, p0.vv))
+
+
+@register_as_intermediate
+def m_3_plus(hf, mp, intermediates):
+ # Intermediate M_ia^{(3)+}, parts of the dynamic self-energy
+ return (+ 1 * einsum("ijbc,jabc->ia", mp.t2oo, mp.t2eri(b.ovvv, b.ov))
+ + 0.5 * einsum("ijbc,jabc->ia", mp.td2(b.oovv), hf.ovvv)
+ - 0.25 * einsum(
+ "ijbc,jabc->ia", mp.t2oo, mp.t2eri(b.ovvv, b.oo))
+ )
+
+
+@register_as_intermediate
+def m_3_minus(hf, mp, intermediates):
+ # Intermediate M_ia^{(3)-}, parts of the dynamic self-energy
+ return (+ 0.5 * einsum("jkab,jkib->ia", mp.td2(b.oovv), hf.ooov)
+ - 1 * einsum("jkab,jkib->ia", mp.t2oo, mp.t2eri(b.ooov, b.ov))
+ - 0.25 * einsum(
+ "jkab,jkib->ia", mp.t2oo, mp.t2eri(b.ooov, b.vv)
+ )
+ )
+
+DISPATCH = {
+ "ip-adc0": pole_strength_ip_adc0,
+ "ip-adc1": pole_strength_ip_adc0,
+ "ip-adc2": pole_strength_ip_adc2,
+ "ip-adc2x": pole_strength_ip_adc2,
+ "ip-adc3": pole_strength_ip_adc3,
+}
+
+
+def pole_strength(method, ground_state, amplitude, intermediates=None):
+ """Compute the pole strength of the ionized state for the
+ provided ADC method from the spectroscopic amplitude x.
+
+ Parameters
+ ----------
+ method: adc.Method
+ Provide a method at which to compute the MTMs
+ ground_state : adcc.LazyMp
+ The MP ground state
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+
+ Returns
+ -------
+ Scalar
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+ if method.name not in DISPATCH:
+ raise NotImplementedError("pole_strength is not "
+ f"implemented for {method.name}.")
+
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret
diff --git a/adcc/adc_ip/state2state_transition_dm.py b/adcc/adc_ip/state2state_transition_dm.py
new file mode 100644
index 000000000..0d3923958
--- /dev/null
+++ b/adcc/adc_ip/state2state_transition_dm.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2018 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.OneParticleDensity import OneParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def s2s_tdm_ip_adc0(mp, amplitude_l, amplitude_r, intermediates):
+ check_singles_amplitudes([b.o], amplitude_l, amplitude_r)
+ ul1 = amplitude_l.h
+ ur1 = amplitude_r.h
+
+ dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.NOSYMMETRY)
+ dm.oo = -einsum("j,i->ij", ul1, ur1)
+ return dm
+
+
+def s2s_tdm_ip_adc2(mp, amplitude_l, amplitude_r, intermediates):
+ check_doubles_amplitudes([b.o, b.o, b.v], amplitude_l, amplitude_r)
+ dm = s2s_tdm_ip_adc0(mp, amplitude_l, amplitude_r, intermediates)
+
+ ul1, ul2 = amplitude_l.h, amplitude_l.phh
+ ur1, ur2 = amplitude_r.h, amplitude_r.phh
+
+ t2 = mp.t2(b.oovv)
+ p0 = mp.mp2_diffdm
+ p1_oo = dm.oo.evaluate() # ADC(1) diffdm
+
+ # Zeroth order doubles contributions
+ p2_oo = 2 * einsum("kja,ika->ij", ul2, ur2)
+ p2_vv = einsum("ija,ijb->ab", ul2, ur2)
+ p_ov = sqrt(2) * einsum("j,ija->ia", ul1, ur2)
+ p_vo = sqrt(2) * einsum("ija,j->ai", ul2, ur1)
+
+ # ADC(2) ISR intermediate (TODO Move to intermediates)
+# ru1 = einsum("i,ijab->jab", u1, t2).evaluate()
+
+ # Compute second-order contributions to the density matrix
+ dm.oo = ( # ip_adc2_p_oo
+ + p1_oo + p2_oo
+ - 0.5 * einsum("k,jk,i->ij", ul1, p0.oo, ur1)
+ - 0.5 * einsum("j,ki,k->ij", ul1, p0.oo, ur1)
+ + 0.5 * einsum("iab,jab->ij", einsum("k,kiab->iab", ul1, t2),
+ einsum("l,ljab->jab", ur1, t2))
+ )
+
+ dm.vv = ( # ip_adc2_p_vv
+ + p2_vv
+ - einsum("kcb,kca->ab", einsum("i,kicb->kcb", ul1, t2),
+ einsum("j,kjca->kca", ur1, t2))
+ )
+
+ dm.ov = ( # ip_adc2_p_ov
+ + p_ov
+ + 1/sqrt(2) * (
+ + einsum("klb,klba,i->ia", ul2, t2, ur1)
+ + 2 * einsum("kb,ikba->ia", einsum("klb,l->kb", ul2, ur1), t2))
+ - einsum("k,ka,i->ia", ul1, p0.ov, ur1)
+ )
+
+ dm.vo = ( # ip_adc2_p_vo
+ + p_vo
+ + 1/sqrt(2) * (
+ + einsum("i,klba,klb->ai", ul1, t2, ur2)
+ + 2 * einsum("lb,liba->ai", einsum("k,klb->lb", ul1, ur2), t2))
+ - einsum("k,ka,i->ai", ur1, p0.ov, ul1)
+ # switched indices because p0_ov is used instead of p0_vo
+ )
+ return dm
+
+
+DISPATCH = {
+ "ip-adc0": s2s_tdm_ip_adc0,
+ "ip-adc1": s2s_tdm_ip_adc0, # same as ADC(0)
+ "ip-adc2": s2s_tdm_ip_adc2,
+ "ip-adc2x": s2s_tdm_ip_adc2, # same as ADC(2)
+}
+
+
+def state2state_transition_dm(method, ground_state, amplitude_from,
+ amplitude_to, intermediates=None):
+ """
+ Compute the state to state transition density matrix
+ state in the MO basis using the intermediate-states representation.
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude_from : AmplitudeVector
+ The amplitude vector of the state to start from
+ amplitude_to : AmplitudeVector
+ The amplitude vector of the state to excite to
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude_from, AmplitudeVector):
+ raise TypeError("amplitude_from should be an AmplitudeVector object.")
+ if not isinstance(amplitude_to, AmplitudeVector):
+ raise TypeError("amplitude_to should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state2state_transition_dm is not "
+ f"implemented for {method.name}.")
+ else:
+ # final state is on the bra side/left (complex conjugate)
+ # see ref https://doi.org/10.1080/00268976.2013.859313, appendix A2
+ ret = DISPATCH[method.name](ground_state, amplitude_to, amplitude_from,
+ intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ip/state_diffdm.py b/adcc/adc_ip/state_diffdm.py
new file mode 100644
index 000000000..d31bfc93e
--- /dev/null
+++ b/adcc/adc_ip/state_diffdm.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from math import sqrt
+
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.OneParticleDensity import OneParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+
+
+def diffdm_ip_adc0(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.o], amplitude)
+ u1 = amplitude.h
+
+ dm = OneParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN)
+ dm.oo = -einsum("j,i->ij", u1, u1)
+ return dm
+
+
+def diffdm_ip_adc2(mp, amplitude, intermediates):
+ dm = diffdm_ip_adc0(mp, amplitude, intermediates) # Get ADC(0/1) result
+ check_doubles_amplitudes([b.o, b.o, b.v], amplitude)
+ u1, u2 = amplitude.h, amplitude.phh
+
+ t2 = mp.t2(b.oovv)
+ p0 = mp.mp2_diffdm
+ p1_oo = dm.oo.evaluate() # ADC(1) diffdm
+
+ # Zeroth order doubles contributions
+ p2_oo = 2 * einsum("kja,ika->ij", u2, u2)
+ p2_vv = einsum("ija,ijb->ab", u2, u2)
+ p_ov = sqrt(2) * einsum("j,ija->ia", u1, u2)
+
+ # ADC(2) ISR intermediate (TODO Move to intermediates)
+ # ru1 = einsum("i,ijab->jab", u1, t2).evaluate()
+
+ # Compute second-order contributions to the density matrix
+ dm.oo = ( # ip_adc2_p_oo
+ + p1_oo + p2_oo
+ - 0.5 * einsum("k,jk,i->ij", u1, p0.oo, u1)
+ - 0.5 * einsum("j,ki,k->ij", u1, p0.oo, u1)
+ + 0.5 * einsum("iab,jab->ij", einsum("k,kiab->iab", u1, t2),
+ einsum("l,ljab->jab", u1, t2))
+ )
+
+ dm.vv = ( # ip_adc2_p_vv
+ + p2_vv
+ - einsum("kcb,kca->ab", einsum("i,kicb->kcb", u1, t2),
+ einsum("j,kjca->kca", u1, t2))
+ )
+
+ dm.ov = ( # ip_adc2_p_ov
+ + p_ov
+ + 1/sqrt(2) * (
+ + einsum("klb,klba,i->ia", u2, t2, u1)
+ + 2 * einsum("kb,ikba->ia", einsum("klb,l->kb", u2, u1), t2))
+ - einsum("k,ka,i->ia", u1, p0.ov, u1)
+ )
+ return dm
+
+
+# dict controlling the dispatch of the state_diffdm function
+DISPATCH = {
+ "ip-adc0": diffdm_ip_adc0,
+ "ip-adc1": diffdm_ip_adc0, # same as ADC(0)
+ "ip-adc2": diffdm_ip_adc2,
+ "ip-adc2x": diffdm_ip_adc2, # same as ADC(2)
+}
+
+
+def state_diffdm(method, ground_state, amplitude, intermediates=None):
+ """
+ Compute the one-particle difference density matrix of an excited state
+ in the MO basis.
+
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude, AmplitudeVector):
+ raise TypeError("amplitude should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state_diffdm is not implemented "
+ f"for {method.name}.")
+ else:
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ip/state_diffdm_2p.py b/adcc/adc_ip/state_diffdm_2p.py
new file mode 100644
index 000000000..b7385a0b3
--- /dev/null
+++ b/adcc/adc_ip/state_diffdm_2p.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2026 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+from adcc import block as b
+from adcc.LazyMp import LazyMp
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like
+from adcc.Intermediates import Intermediates
+from adcc.AmplitudeVector import AmplitudeVector
+from adcc.TwoParticleDensity import TwoParticleDensity
+from adcc.NParticleOperator import OperatorSymmetry
+
+from .util import check_doubles_amplitudes, check_singles_amplitudes
+from math import sqrt
+
+
+def diffdm_ip_adc0_2p(mp, amplitude, intermediates):
+ check_singles_amplitudes([b.o], amplitude)
+ u1 = amplitude.h
+
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ dm = TwoParticleDensity(mp, symmetry=OperatorSymmetry.HERMITIAN)
+
+ dm.oooo = (
+ + 4.0 * (
+ # N^4: O^4 / N^4: O^4
+ + 1 * einsum("il,jk->ijkl", einsum("i,l->il", u1, u1), d_oo)
+ ).antisymmetrise(0, 1).antisymmetrise(2, 3)
+ )
+ return dm
+
+
+def diffdm_ip_adc1_2p(mp, amplitude, intermediates):
+ dm = diffdm_ip_adc0_2p(mp, amplitude, intermediates) # Get ADC(0) result
+ u1 = amplitude.h
+
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ t2 = mp.t2(b.oovv)
+
+ dm.oovv += (
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 2.0 * einsum("iab,j->ijab", einsum("k,ikab->iab", u1, t2), u1).antisymmetrise(0, 1)
+ )
+ return dm
+
+
+def diffdm_ip_adc2_2p(mp, amplitude, intermediates):
+ dm = diffdm_ip_adc1_2p(mp, amplitude, intermediates) # Get ADC(1) result
+ check_doubles_amplitudes([b.o, b.o, b.v], amplitude)
+ u1, u2 = amplitude.h, amplitude.phh
+ hf = mp.reference_state
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+
+ t2 = mp.t2(b.oovv)
+ td2 = mp.td2(b.oovv)
+ p0 = mp.mp2_diffdm
+
+ dm.oooo += (
+ # N^5: O^4V^1 / N^4: O^4
+ + 2.0 * einsum("ija,kla->ijkl", u2, u2)
+ + 4.0 * (
+ # N^4: O^4 / N^4: O^4
+ + 1.0 * einsum("il,jk->ijkl", einsum("i,l->il", u1, u1), p0.oo)
+ # N^4: O^3V^1 / N^4: O^4
+ - 2.0 * einsum("ik,jl->ijkl", einsum("ima,kma->ik", u2, u2), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 0.5 * einsum("ik,jl->ijkl", einsum("kab,iab->ik", einsum("n,knab->kab", u1, t2), einsum("m,imab->iab", u1, t2)), d_oo)
+ ).antisymmetrise(0, 1).antisymmetrise(2, 3)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ + 1.0 * (
+ 2.0 * einsum("jkl,i->ijkl", einsum("jab,klab->jkl", einsum("m,jmab->jab", u1, t2), t2), u1)
+ ).antisymmetrise(0, 1).symmetrise([(0, 2), (1, 3)])
+ + 1.0 * (
+ # N^4: O^4 / N^4: O^4
+ + 4.0 * einsum("il,jk->ijkl", einsum("l,i->il", einsum("m,lm->l", u1, p0.oo), u1), d_oo)
+ ).antisymmetrise(0, 1).antisymmetrise(2, 3).symmetrise([(0, 2), (1, 3)])
+ )
+ dm.ooov += (
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + sqrt(2) * einsum("k,ija->ijka", u1, u2)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ - sqrt(2) * einsum("kb,ijab->ijka", einsum("l,klb->kb", u1, u2), t2)
+ + 2.0 * (
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + 1.0 * einsum("jk,ia->ijka", einsum("j,k->jk", u1, u1), p0.ov)
+ # N^5: O^3V^2 / N^4: O^2V^2
+ + sqrt(2) * einsum("ika,j->ijka", einsum("klb,ilab->ika", u2, t2), u1)
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + sqrt(2) * einsum("ja,ik->ijka", einsum("l,jla->ja", u1, u2), d_oo)
+ # N^4: O^3V^1 / N^4: O^3V^1
+ + 1.0 * einsum("ia,jk->ijka", einsum("a,i->ia", einsum("l,la->a", u1, p0.ov), u1), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + sqrt(2) * einsum("ja,ik->ijka", einsum("mb,jmab->ja", einsum("l,lmb->mb", u1, u2), t2), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 0.5 * sqrt(2) * einsum("ia,jk->ijka", einsum("a,i->ia", einsum("lmb,lmab->a", u2, t2), u1), d_oo)
+ ).antisymmetrise(0, 1)
+ )
+ dm.oovv += (
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 2.0 * einsum("iab,j->ijab", einsum("k,ikab->iab", u1, td2), u1).antisymmetrise(0, 1)
+ )
+ dm.ovov += (
+ # N^5: O^3V^2 / N^4: O^2V^2
+ - 2.0 * einsum("jka,ikb->iajb", u2, u2)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ + 1.0 * einsum("ab,ij->iajb", einsum("kla,klb->ab", u2, u2), d_oo)
+ # N^4: O^2V^2 / N^4: O^2V^2
+ - 1.0 * einsum("ij,ab->iajb", einsum("i,j->ij", u1, u1), p0.vv)
+ # N^5: O^2V^3 / N^4: O^2V^2
+ + 1.0 * einsum("ibc,jac->iajb", einsum("l,ilbc->ibc", u1, t2), einsum("k,jkac->jac", u1, t2))
+ # N^4: O^1V^3 / N^4: O^2V^2
+ + 1.0 * einsum("ab,ij->iajb", einsum("lbc,lac->ab", einsum("m,lmbc->lbc", u1, t2), einsum("k,klac->lac", u1, t2)), d_oo)
+ # N^5: O^2V^3 / N^4: O^2V^2
+ - 2.0 * einsum("jab,i->iajb", einsum("kbc,jkac->jab", einsum("l,klbc->kbc", u1, t2), t2), u1).symmetrise([(0, 2), (1, 3)])
+ )
+ dm.ovvv += (
+ # N^5: O^2V^3 / N^4: O^1V^3
+ - sqrt(2) * einsum("ja,ijbc->iabc", einsum("k,jka->ja", u1, u2), t2)
+ # N^5: O^2V^3 / N^4: O^1V^3
+ - 0.5 * sqrt(2) * einsum("abc,i->iabc", einsum("jka,jkbc->abc", u2, t2), u1)
+ )
+ dm.vvvv += (
+ # N^5: O^1V^4 / N^4: V^4
+ - 1.0 * einsum("kcd,kab->abcd", einsum("j,jkcd->kcd", u1, t2), einsum("i,ikab->kab", u1, t2))
+ )
+ return dm
+
+
+# dict controlling the dispatch of the state_diffdm function
+DISPATCH = {
+ "ip-adc0": diffdm_ip_adc0_2p,
+ "ip-adc1": diffdm_ip_adc1_2p,
+ "ip-adc2": diffdm_ip_adc2_2p,
+ "ip-adc2x": diffdm_ip_adc2_2p, # same as ADC(2)
+}
+
+
+def state_diffdm_2p(method, ground_state, amplitude, intermediates=None):
+ """
+ Compute the two-particle difference density matrix of an excited state
+ in the MO basis.
+
+ Parameters
+ ----------
+ method : str, AdcMethod
+ The method to use for the computation (e.g. "adc2")
+ ground_state : LazyMp
+ The ground state upon which the excitation was based
+ amplitude : AmplitudeVector
+ The amplitude vector
+ intermediates : adcc.Intermediates
+ Intermediates from the ADC calculation to reuse
+ """
+ if not isinstance(method, AdcMethod):
+ method = AdcMethod(method)
+ if not isinstance(ground_state, LazyMp):
+ raise TypeError("ground_state should be a LazyMp object.")
+ if not isinstance(amplitude, AmplitudeVector):
+ raise TypeError("amplitude should be an AmplitudeVector object.")
+ if intermediates is None:
+ intermediates = Intermediates(ground_state)
+
+ if method.name not in DISPATCH:
+ raise NotImplementedError("state_diffdm_2p is not implemented "
+ f"for {method.name}.")
+ else:
+ ret = DISPATCH[method.name](ground_state, amplitude, intermediates)
+ return ret.evaluate()
diff --git a/adcc/adc_ip/util.py b/adcc/adc_ip/util.py
new file mode 100644
index 000000000..8b13a9459
--- /dev/null
+++ b/adcc/adc_ip/util.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2020 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+
+
+def check_singles_amplitudes(spaces, *amplitudes):
+ check_have_singles_block(*amplitudes)
+ check_singles_subspaces(spaces, *amplitudes)
+
+
+def check_doubles_amplitudes(spaces, *amplitudes):
+ check_have_doubles_block(*amplitudes)
+ check_doubles_subspaces(spaces, *amplitudes)
+
+
+def check_have_singles_block(*amplitudes):
+ if any("h" not in amplitude.keys() for amplitude in amplitudes):
+ raise ValueError("ADC(0) level and "
+ "beyond expects an excitation amplitude with a "
+ "singles part.")
+
+
+def check_have_doubles_block(*amplitudes):
+ if any("phh" not in amplitude.keys() for amplitude in amplitudes):
+ raise ValueError("ADC(2) level and "
+ "beyond expects an excitation amplitude with a "
+ "singles and a doubles part.")
+
+
+def check_singles_subspaces(spaces, *amplitudes):
+ for amplitude in amplitudes:
+ u1 = amplitude.h
+ if u1.subspaces != spaces:
+ raise ValueError("Mismatch in subspaces singles part "
+ f"(== {u1.subspaces}), where {spaces} "
+ "was expected.")
+
+
+def check_doubles_subspaces(spaces, *amplitudes):
+ for amplitude in amplitudes:
+ u2 = amplitude.phh
+ if u2.subspaces != spaces:
+ raise ValueError("Mismatch in subspaces doubles part "
+ f"(== {u2.subspaces}), where "
+ f"{spaces} was expected.")
diff --git a/adcc/adc_pp/matrix.py b/adcc/adc_pp/matrix.py
index de36b66a4..2a9befdb6 100644
--- a/adcc/adc_pp/matrix.py
+++ b/adcc/adc_pp/matrix.py
@@ -60,24 +60,12 @@ def block(ground_state, spaces, order, variant=None, intermediates=None):
while one would probably want in the long run that one can have an "o2" space,
but not do CVS.
"""
- if isinstance(variant, str):
- variant = [variant]
- elif variant is None:
- variant = []
reference_state = ground_state.reference_state
if intermediates is None:
intermediates = Intermediates(ground_state)
- if ground_state.has_core_occupied_space and "cvs" not in variant:
- raise ValueError("Cannot run a general (non-core-valence approximated) "
- "ADC method on top of a ground state with a "
- "core-valence separation.")
- if not ground_state.has_core_occupied_space and "cvs" in variant:
- raise ValueError("Cannot run a core-valence approximated ADC method on "
- "top of a ground state without a "
- "core-valence separation.")
-
- fn = "_".join(["block"] + variant + spaces + [str(order)])
+ fn = b.get_block_name(spaces, order, variant,
+ ground_state.has_core_occupied_space)
if fn not in globals():
raise ValueError("Could not dispatch: "
diff --git a/adcc/block.py b/adcc/block.py
index f5f0ae96a..5124aa85c 100644
--- a/adcc/block.py
+++ b/adcc/block.py
@@ -176,3 +176,25 @@ 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.
+ Does some sanity checks.
+ """
+ if isinstance(variant, str):
+ variant = [variant]
+ elif variant is None:
+ variant = []
+
+ if has_core_occupied_space and "cvs" not in variant:
+ raise ValueError("Cannot run a general (non-core-valence approximated) "
+ "ADC method on top of a ground state with a "
+ "core-valence separation.")
+ if not has_core_occupied_space and "cvs" in variant:
+ raise ValueError("Cannot run a core-valence approximated ADC method on "
+ "top of a ground state without a "
+ "core-valence separation.")
+
+ return "_".join(["block"] + variant + spaces + [str(order)])
diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py
index b2690876b..8fb0aaae4 100644
--- a/adcc/guess/__init__.py
+++ b/adcc/guess/__init__.py
@@ -22,25 +22,29 @@
## ---------------------------------------------------------------------
from .guess_zero import guess_symmetries, guess_zero
from .guesses_from_diagonal import guesses_from_diagonal
+from .util import estimate_n_guesses, determine_spin_change
-__all__ = ["guess_zero", "guesses_from_diagonal",
+__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 +54,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").
+ is_alpha Is the detached/attached electron alpha spin for the respective
+ IP-/EA-ADC calculation.
+ kwargs Any other argument understood by guesses_from_diagonal.
+ """
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_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.
"""
- return guesses_from_diagonal(matrix, n_guesses, block=block,
- **guess_kwargs_kind("singlet"), **kwargs)
+ if matrix.method.adc_type == "ip":
+ spin_change = -0.5
+ elif matrix.method.adc_type == "ea":
+ spin_change = 0.5
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block,
+ is_alpha=is_alpha, spin_change=spin_change,
+ spin_block_symmetrisation= get_spin_block_symmetrisation("doublet"),
+ **kwargs
+ )
def guesses_triplet(matrix, n_guesses, block="ph", **kwargs):
@@ -66,18 +101,23 @@ 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").
+ is_alpha Is the detached/attached electron alpha spin for the respective
+ IP-/EA-ADC calculation.
+ kwargs Any other argument understood by guesses_from_diagonal.
"""
- return guesses_from_diagonal(matrix, n_guesses, block=block,
- **guess_kwargs_kind("triplet"), **kwargs)
+ 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 +126,17 @@ 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").
+ is_alpha Is the detached/attached electron alpha spin for the respective
+ IP-/EA-ADC calculation.
+ kwargs Any other argument understood by guesses_from_diagonal.
"""
- return guesses_from_diagonal(matrix, n_guesses, block=block,
- **guess_kwargs_kind("spin_flip"), **kwargs)
+ return guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_change=-1,
+ spin_block_symmetrisation= get_spin_block_symmetrisation("spin_flip"),
+ **kwargs
+ )
\ No newline at end of file
diff --git a/adcc/guess/guess_zero.py b/adcc/guess/guess_zero.py
index 67791b9be..4ed50dc21 100644
--- a/adcc/guess/guess_zero.py
+++ b/adcc/guess/guess_zero.py
@@ -34,14 +34,14 @@ def guess_zero(matrix, spin_change=0, spin_block_symmetrisation="none"):
spin_change The spin change to enforce in an excitation.
Typical values are 0 (singlet/triplet/any) and -1 (spin-flip).
spin_block_symmetrisation
- Symmetrisation to enforce between equivalent spin blocks, which
- all yield the desired spin_change. E.g. if spin_change == 0,
- then both the alpha->alpha and beta->beta blocks of the singles
- part of the excitation vector achieve a spin change of 0.
- The symmetry specified with this parameter will then be imposed
- between the a-a and b-b blocks. Valid values are "none",
- "symmetric" and "antisymmetric", where "none" enforces
- no particular symmetry.
+ Symmetrisation to enforce between equivalent spin blocks,
+ which all yield the desired spin_change. E.g. if
+ spin_change == 0, then both the alpha->alpha and beta->beta
+ blocks of the singles part of the excitation vector achieve a
+ spin change of 0. The symmetry specified with this parameter
+ will then be imposed between the a-a and b-b blocks. Valid
+ values are "none", "symmetric" and "antisymmetric", where
+ "none" enforces no particular symmetry.
"""
return AmplitudeVector(**{
block: Tensor(sym) for block, sym in guess_symmetries(
@@ -60,14 +60,14 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"):
spin_change The spin change to enforce in an excitation.
Typical values are 0 (singlet/triplet/any) and -1 (spin-flip).
spin_block_symmetrisation
- Symmetrisation to enforce between equivalent spin blocks, which
- all yield the desired spin_change. E.g. if spin_change == 0,
- then both the alpha->alpha and beta->beta blocks of the singles
- part of the excitation vector achieve a spin change of 0.
- The symmetry specified with this parameter will then be imposed
- between the a-a and b-b blocks. Valid values are "none",
- "symmetric" and "antisymmetric", where "none" enforces
- no particular symmetry.
+ Symmetrisation to enforce between equivalent spin blocks,
+ which all yield the desired spin_change. E.g. if
+ spin_change == 0, then both the alpha->alpha and beta->beta
+ blocks of the singles part of the excitation vector achieve a
+ spin change of 0. The symmetry specified with this parameter
+ will then be imposed between the a-a and b-b blocks. Valid
+ values are "none", "symmetric" and "antisymmetric", where
+ "none" enforces no particular symmetry.
"""
if not isinstance(matrix, AdcMatrixlike):
raise TypeError("matrix needs to be of type AdcMatrixlike")
@@ -80,52 +80,60 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"):
"ADC calculations on top of restricted reference "
"states.")
if int(spin_change * 2) / 2 != spin_change:
- raise ValueError("Only integer or half-integer spin_change is allowed. "
- "You passed {}".format(spin_change))
-
- max_spin_change = 0
- if "ph" in matrix.axis_blocks:
- max_spin_change = 1
- if "pphh" in matrix.axis_blocks:
- max_spin_change = 2
- if spin_change > max_spin_change:
- raise ValueError("spin_change for singles guesses may only be in the "
- f"range [{-max_spin_change}, {max_spin_change}] and "
+ raise ValueError("Only integer or half-integer spin_change is allowed."
+ " You passed {}".format(spin_change))
+
+ max_spin_change = 0.5 * len(matrix.axis_blocks[-1])
+ 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 for may only be one of "
+ f"{valid_spin_changes}, and "
f"not {spin_change}.")
symmetries = {}
- if "ph" in matrix.axis_blocks:
- symmetries["ph"] = guess_symmetry_singles(
+ block = matrix.axis_blocks[0]
+ symmetries[block] = guess_symmetry_singles(
+ matrix, spin_change=spin_change,
+ spin_block_symmetrisation=spin_block_symmetrisation, block=block
+ )
+ if len(matrix.axis_blocks) >= 2:
+ block = matrix.axis_blocks[1]
+ symmetries[block] = guess_symmetry_doubles(
matrix, spin_change=spin_change,
- spin_block_symmetrisation=spin_block_symmetrisation
- )
- if "pphh" in matrix.axis_blocks:
- symmetries["pphh"] = guess_symmetry_doubles(
- matrix, spin_change=spin_change,
- spin_block_symmetrisation=spin_block_symmetrisation
+ spin_block_symmetrisation=spin_block_symmetrisation, block=block
)
return symmetries
def guess_symmetry_singles(matrix, spin_change=0,
- spin_block_symmetrisation="none"):
- symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces["ph"]))
+ spin_block_symmetrisation="none", block="ph"):
+ symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces[block]))
symmetry.irreps_allowed = ["A"]
+
if spin_change != 0 and spin_block_symmetrisation != "none":
raise NotImplementedError("spin_symmetrisation != 'none' only "
"implemented for spin_change == 0")
- elif spin_block_symmetrisation == "symmetric":
- symmetry.spin_block_maps = [("aa", "bb", 1)]
- symmetry.spin_blocks_forbidden = ["ab", "ba"]
- elif spin_block_symmetrisation == "antisymmetric":
- symmetry.spin_block_maps = [("aa", "bb", -1)]
+
+ if spin_block_symmetrisation in ("symmetric", "antisymmetric"):
+ # PP-ADC
+ fac = 1 if spin_block_symmetrisation == "symmetric" else -1
+ symmetry.spin_block_maps = [("aa", "bb", fac)]
symmetry.spin_blocks_forbidden = ["ab", "ba"]
+
+ elif matrix.method.adc_type != "pp" and matrix.reference_state.restricted:
+ # IP- and EA-ADC
+ # attach/detach alpha electron
+ # forbidden beta blocks (["a"]) not needed because for a
+ # restricted reference only alpha states will be computed
+ symmetry.spin_blocks_forbidden = ["b"]
+
return symmetry
def guess_symmetry_doubles(matrix, spin_change=0,
- spin_block_symmetrisation="none"):
- spaces_d = matrix.axis_spaces["pphh"]
+ spin_block_symmetrisation="none", block="pphh"):
+ spaces_d = matrix.axis_spaces[block]
symmetry = Symmetry(matrix.mospaces, "".join(spaces_d))
symmetry.irreps_allowed = ["A"]
@@ -133,32 +141,80 @@ def guess_symmetry_doubles(matrix, spin_change=0,
raise NotImplementedError("spin_symmetrisation != 'none' only "
"implemented for spin_change == 0")
- if spin_change == 0 \
- and spin_block_symmetrisation in ("symmetric", "antisymmetric"):
- fac = 1 if spin_block_symmetrisation == "symmetric" else -1
- # Spin mapping between blocks where alpha and beta are just mirrored
- symmetry.spin_block_maps = [("aaaa", "bbbb", fac),
- ("abab", "baba", fac),
- ("abba", "baab", fac)]
-
- # Mark blocks which change spin as forbidden
- symmetry.spin_blocks_forbidden = ["aabb", # spin_change +2
- "bbaa", # spin_change -2
- "aaab", # spin_change +1
- "aaba", # spin_change +1
- "abaa", # spin_change -1
- "baaa", # spin_change -1
- "abbb", # spin_change +1
- "babb", # spin_change +1
- "bbab", # spin_change -1
- "bbba"] # spin_change -1
-
- # Add index permutation symmetry:
- permutations = ["ijab"]
- if spaces_d[0] == spaces_d[1]:
- permutations.append("-jiab")
- if spaces_d[2] == spaces_d[3]:
- permutations.append("-ijba")
- if len(permutations) > 1:
- symmetry.permutations = permutations
+ if matrix.method.adc_type == "pp":
+ # PP-ADC
+ if spin_block_symmetrisation in ("symmetric", "antisymmetric"):
+ fac = 1 if spin_block_symmetrisation == "symmetric" else -1
+ # Spin mapping between blocks where alpha and beta are mirrored
+ symmetry.spin_block_maps = [("aaaa", "bbbb", fac),
+ ("abab", "baba", fac),
+ ("abba", "baab", fac)]
+
+ # Mark blocks which change spin as forbidden
+ symmetry.spin_blocks_forbidden = ["aabb", # spin_change -2
+ "bbaa", # spin_change +2
+ "aaab", # spin_change -1
+ "aaba", # spin_change -1
+ "abaa", # spin_change +1
+ "baaa", # spin_change +1
+ "abbb", # spin_change -1
+ "babb", # spin_change -1
+ "bbab", # spin_change +1
+ "bbba"] # spin_change +1
+
+ # Add index permutation symmetry:
+ permutations = ["ijab"]
+ if spaces_d[0] == spaces_d[1]:
+ permutations.append("-jiab")
+ if spaces_d[2] == spaces_d[3]:
+ permutations.append("-ijba")
+ if len(permutations) > 1:
+ symmetry.permutations = permutations
+
+ elif matrix.method.adc_type == "ip":
+ # IP-ADC
+ # No spin mapping between blocks
+ symmetry.spin_block_maps = []
+
+ # Mark blocks which change spin incorrectly as forbidden
+ if matrix.reference_state.restricted: # kind=doublet
+ # detach alpha electron
+ # forbidden beta ionization blocks not needed because for a
+ # restricted reference only alpha states will be computed
+ symmetry.spin_blocks_forbidden = ["aab", # spin_change -3/2
+ "bba", # spin_change +3/2
+ "aba", # spin_change +1/2
+ "baa", # spin_change +1/2
+ "bbb"] # spin_change +1/2
+
+ # Add index permutation symmetry:
+ permutations = ["ija"]
+ if spaces_d[0] == spaces_d[1]:
+ permutations.append("-jia")
+ if len(permutations) > 1:
+ symmetry.permutations = permutations
+
+ else:
+ # EA-ADC
+ # No spin mapping between blocks
+ symmetry.spin_block_maps = []
+
+ # Mark blocks which change spin incorrectly as forbidden
+ if matrix.reference_state.restricted: # kind=doublet
+ # attach alpha electron
+ # forbidden beta attachment blocks not needed because for a
+ # restricted reference only alpha states will be computed
+ symmetry.spin_blocks_forbidden = ["baa", # spin_change +3/2
+ "abb", # spin_change -3/2
+ "aab", # spin_change -1/2
+ "aba", # spin_change -1/2
+ "bbb"] # spin_change -1/2
+
+ # Add index permutation symmetry:
+ permutations = ["iab"]
+ if spaces_d[1] == spaces_d[2]:
+ permutations.append("-iba")
+ if len(permutations) > 1:
+ symmetry.permutations = permutations
+
return symmetry
diff --git a/adcc/guess/guesses_from_diagonal.py b/adcc/guess/guesses_from_diagonal.py
index 2d94f9720..a8224178f 100644
--- a/adcc/guess/guesses_from_diagonal.py
+++ b/adcc/guess/guesses_from_diagonal.py
@@ -31,7 +31,8 @@
from .guess_zero import guess_zero
-def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0,
+def guesses_from_diagonal(matrix, n_guesses, block="ph", kind=None,
+ is_alpha=None, spin_change=0,
spin_block_symmetrisation="none",
degeneracy_tolerance=1e-14, max_diagonal_value=1000):
"""
@@ -45,23 +46,26 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0,
vectors are returned if this many could not be found.
block Diagonal block to use for obtaining the guesses
(typically "ph" or "pphh").
+ is_alpha Is the detached/attached electron alpha spin for the
+ respective IP-/EA-ADC calculation.
spin_change The spin change to enforce in an excitation.
- Typical values are 0 (singlet/triplet/any) and -1 (spin-flip).
+ Typical values are 0 (singlet/triplet/any), +/- 0.5 (doublet)
+ and -1 (spin-flip).
spin_block_symmetrisation
- Symmetrisation to enforce between equivalent spin blocks, which
- all yield the desired spin_change. E.g. if spin_change == 0,
- then both the alpha->alpha and beta->beta blocks of the singles
- part of the excitation vector achieve a spin change of 0.
- The symmetry specified with this parameter will then be imposed
- between the a-a and b-b blocks. Valid values are "none",
- "symmetric" and "antisymmetric", where "none" enforces
- no particular symmetry.
+ Symmetrisation to enforce between equivalent spin blocks,
+ which all yield the desired spin_change. E.g. if
+ spin_change == 0, then both the alpha->alpha and beta->beta
+ blocks of the singles part of the excitation vector achieve a
+ spin change of 0. The symmetry specified with this parameter
+ will then be imposed between the a-a and b-b blocks. Valid
+ values are "none", "symmetric" and "antisymmetric", where
+ "none" enforces no particular symmetry.
degeneracy_tolerance
Tolerance for two entries of the diagonal to be considered
degenerate, i.e. identical.
max_diagonal_value
- Maximal diagonal value, which is considered as a valid candidate
- to form a guess.
+ Maximal diagonal value, which is considered as a valid
+ candidate to form a guess.
"""
if not isinstance(matrix, AdcMatrixlike):
raise TypeError("matrix needs to be of type AdcMatrixlike")
@@ -74,8 +78,8 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0,
"ADC calculations on top of restricted reference "
"states.")
if int(spin_change * 2) / 2 != spin_change:
- raise ValueError("Only integer or half-integer spin_change is allowed. "
- "You passed {}".format(spin_change))
+ raise ValueError("Only integer or half-integer spin_change is allowed."
+ " You passed {}".format(spin_change))
if block not in matrix.axis_blocks:
raise ValueError("The passed ADC matrix does not have the block '{}.'"
@@ -83,14 +87,15 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0,
if n_guesses == 0:
return []
- if block == "ph":
+ if block in ["h", "p", "ph"]:
guessfunction = guesses_from_diagonal_singles
- elif block == "pphh":
+ elif block in ["phh", "pph", "pphh"]:
guessfunction = guesses_from_diagonal_doubles
else:
- raise ValueError(f"Don't know how to generate guesses for block {block}")
+ raise ValueError("Don't know how to generate guesses for block "
+ f"{block}")
- return guessfunction(matrix, n_guesses, spin_change,
+ return guessfunction(matrix, n_guesses, block, kind, is_alpha, spin_change,
spin_block_symmetrisation, degeneracy_tolerance,
max_diagonal_value)
@@ -127,8 +132,8 @@ def spin_change(self):
("v", "a"): +0.5, # add alpha
("v", "b"): -0.5, # add beta
}
- return int(sum(mapping_spin_change[(space[0], spin)]
- for space, spin in zip(self.subspaces, self.spin_block)))
+ return sum(mapping_spin_change[(space[0], spin)]
+ for space, spin in zip(self.subspaces, self.spin_block))
def __repr__(self):
return f"({self.index} {self.value})"
@@ -197,11 +202,12 @@ def telem_nospin(telem):
return res
-def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0,
+def guesses_from_diagonal_singles(matrix, n_guesses, block="ph", kind=None,
+ is_alpha=None, spin_change=0,
spin_block_symmetrisation="none",
degeneracy_tolerance=1e-14,
max_diagonal_value=1000):
- motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces["ph"])
+ motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces[block])
if n_guesses == 0:
return []
@@ -216,12 +222,12 @@ def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0,
# Also it filters out too large diagonal entries (which are essentially
# hopeless to give useful excitations)
def pred_singles(telem):
- return (ret[0].ph.is_allowed(telem.index)
+ return (ret[0].get(block).is_allowed(telem.index)
and telem.spin_change == spin_change
and abs(telem.value) <= max_diagonal_value)
elements = find_smallest_matching_elements(
- pred_singles, matrix.diagonal().ph, motrans, n_guesses,
+ pred_singles, matrix.diagonal().get(block), motrans, n_guesses,
degeneracy_tolerance=degeneracy_tolerance
)
if len(elements) == 0:
@@ -245,25 +251,25 @@ def telem_nospin(telem):
group = list(group)
if len(group) == 1: # Just add the single vector
- ret[ivec].ph[group[0].index] = 1.0
+ ret[ivec].get(block)[group[0].index] = 1.0
ivec += 1
elif len(group) == 2:
# Since these two are grouped together, their
# spatial parts must be identical.
# Add the positive linear combination ...
- ret[ivec].ph[group[0].index] = 1
- ret[ivec].ph[group[1].index] = 1
+ ret[ivec].get(block)[group[0].index] = 1
+ ret[ivec].get(block)[group[1].index] = 1
ivec += 1
# ... and the negative linear combination
if ivec < n_guesses:
- ret[ivec].ph[group[0].index] = +1
- ret[ivec].ph[group[1].index] = -1
+ ret[ivec].get(block)[group[0].index] = +1
+ ret[ivec].get(block)[group[1].index] = -1
ivec += 1
else:
raise AssertionError("group size > 3 should not occur "
- "when setting up single guesse.")
+ "when setting up single guesses.")
assert ivec <= n_guesses
# Resize in case less guesses found than requested
@@ -271,7 +277,8 @@ def telem_nospin(telem):
return [evaluate(v / np.sqrt(v @ v)) for v in ret[:ivec]]
-def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0,
+def guesses_from_diagonal_doubles(matrix, n_guesses, block="pphh", kind=None,
+ is_alpha=None, spin_change=0,
spin_block_symmetrisation="none",
degeneracy_tolerance=1e-14,
max_diagonal_value=1000):
@@ -283,24 +290,41 @@ def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0,
spin_block_symmetrisation=spin_block_symmetrisation)
for _ in range(n_guesses)]
- # Build delta-Fock matrices
- spaces_d = matrix.axis_spaces["pphh"]
- df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2])
- df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3])
-
- guesses_d = [gv.pphh for gv in ret] # Extract doubles parts
spin_change_twice = int(spin_change * 2)
assert spin_change_twice / 2 == spin_change
- n_found = libadcc.fill_pp_doubles_guesses(
- guesses_d, matrix.mospaces, df02, df13,
- spin_change_twice, degeneracy_tolerance
- )
+ # Extract doubles parts
+ guesses_d = [gv.get(block) for gv in ret]
+
+ if matrix.method.adc_type == "pp":
+ # PP-ADC
+ spaces_d = matrix.axis_spaces[block]
+ # Build delta-Fock matrices
+ df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2])
+ df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3])
+ n_found = libadcc.fill_pp_doubles_guesses(
+ guesses_d, matrix.mospaces, df02, df13,
+ spin_change_twice, degeneracy_tolerance
+ )
+ else:
+ # IP- and EA-ADC
+ # Build Fock matrices and multiply occ. orbitals with -1 to invert the
+ # order for simplicity in the C++ code
+ d_o = matrix.reference_state.foo.diagonal() * (-1)
+ d_v = matrix.reference_state.fvv.diagonal()
+ doublet = (kind == "doublet")
+ is_restricted = matrix.reference_state.restricted
+ double_guess_func = {"ip": libadcc.fill_ip_doubles_guesses,
+ "ea": libadcc.fill_ea_doubles_guesses}
+ n_found = double_guess_func[matrix.method.adc_type](
+ guesses_d, matrix.mospaces, d_o, d_v, is_alpha, is_restricted,
+ doublet, spin_change_twice, degeneracy_tolerance)
+
# Resize in case less guesses found than requested
ret = ret[:n_found]
# Filter out elements above the noted diagonal value
- diagonal_elements = [ret_d.pphh.dot(matrix.diagonal().pphh * ret_d.pphh)
- for ret_d in ret]
+ diagonal_elements = [ret_d.get(block).dot(matrix.diagonal().get(block)
+ * ret_d.get(block)) for ret_d in ret]
return [ret[i] for (i, elem) in enumerate(diagonal_elements)
if elem <= max_diagonal_value]
diff --git a/adcc/guess/util.py b/adcc/guess/util.py
new file mode 100644
index 000000000..aa0eb7ebc
--- /dev/null
+++ b/adcc/guess/util.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2018 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+
+from ..AdcMethod import AdcMethod
+from typing import Optional
+
+
+def determine_spin_change(method: AdcMethod, kind: str,
+ is_alpha: Optional[bool] = None) -> int | float:
+ if method.adc_type == "pp":
+ if kind == "spin_flip":
+ return -1
+ else:
+ return 0
+ elif method.adc_type == "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 == "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 method: {method.name}")
+
+
+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 < 2 and matrix.method.adc_type != "pp":
+ # Adjustment for IP- and EA-ADC(0/1) calculations
+ estimate = (n_occ_a if matrix.method.adc_type == "ip"
+ else n_virt_a)
+ n_guesses = min(n_guesses, estimate)
+
+ # Adjust if we overshoot the maximal number of sensible singles block
+ # guesses, but make sure we get at least n_states guesses
+ return max(n_states, n_guesses)
\ No newline at end of file
diff --git a/adcc/projection.py b/adcc/projection.py
index d6eee2580..b3b7fa749 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 d49e55bf0..01a16b031 100644
--- a/adcc/solver/explicit_symmetrisation.py
+++ b/adcc/solver/explicit_symmetrisation.py
@@ -76,6 +76,8 @@ class IndexSpinSymmetrisation(IndexSymmetrisation):
def __init__(self, matrix, enforce_spin_kind="singlet"):
super().__init__(matrix)
self.enforce_spin_kind = enforce_spin_kind
+ # Bool to distinguish IP/EA if 'spin_kind=='doublet'
+ self.is_ip_adc = matrix.method.adc_type == "ip"
def symmetrise(self, new_vectors):
if isinstance(new_vectors, AmplitudeVector):
@@ -87,12 +89,13 @@ def symmetrise(self, new_vectors):
for vec in new_vectors:
# Only work on the doubles part
# the other blocks are not yet implemented
- # or nothing needs to be done ("ph" block)
- if "pphh" in vec.blocks:
+ # or nothing needs to be done ("ph"/"h"/"p" block)
+ if len(vec.blocks) > 1:
# TODO: Note that the "d" is needed here because the C++ side
# does not yet understand ph and pphh
amplitude_vector_enforce_spin_kind(
- vec.pphh, "d", self.enforce_spin_kind
+ vec.get(vec.blocks[1]), "d", self.enforce_spin_kind,
+ self.is_ip_adc
)
return new_vectors
diff --git a/adcc/tests/AdcMatrix_test.py b/adcc/tests/AdcMatrix_test.py
index 325eb2f30..b23247fa1 100644
--- a/adcc/tests/AdcMatrix_test.py
+++ b/adcc/tests/AdcMatrix_test.py
@@ -68,7 +68,7 @@ def test_default_block_orders(self):
assert block_orders == ref
assert block_orders == cvs_block_orders
- def test_validate_block_orders(self):
+ def test_validate_block_orders_pp(self):
valid_block_orders = (
{"ph_ph": 0}, # adc0
{"ph_ph": 1, "ph_pphh": None, "ppphhh_ppphhh": None}, # adc1
@@ -90,7 +90,7 @@ def test_validate_block_orders(self):
block_orders, AdcMethod("adc0")
)
- def test_validate_space(self):
+ def test_validate_space_pp(self):
# some valid PP-ADC methods
assert AdcMatrixlike._is_valid_space(
"ph", AdcMethod("adc0")
@@ -112,15 +112,65 @@ def test_validate_space(self):
"phh", AdcMethod("adc0")
)
+ def test_validate_block_orders_ip_ea(self):
+ # Valid IP block orders
+ valid_ip = (
+ {"h_h": 0}, # ip-adc0
+ {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 0}, # ip-adc2
+ )
+ for block_orders in valid_ip:
+ AdcMatrixlike._validate_block_orders(
+ block_orders, AdcMethod("ip-adc2")
+ )
+
+ # Valid EA block orders
+ valid_ea = (
+ {"p_p": 0}, # ea-adc0
+ {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 0}, # ea-adc2
+ )
+ for block_orders in valid_ea:
+ AdcMatrixlike._validate_block_orders(
+ block_orders, AdcMethod("ea-adc2")
+ )
+
+ # Invalid mixed blocks
+ with pytest.raises(ValueError):
+ AdcMatrixlike._validate_block_orders(
+ {"ph_ph": 0}, AdcMethod("ip-adc2")
+ )
+
+ def test_validate_space_ip(self):
+ ip = AdcMethod("ip-adc0")
+ assert AdcMatrixlike._is_valid_space("h", ip)
+ assert AdcMatrixlike._is_valid_space("phh", ip)
+ assert AdcMatrixlike._is_valid_space("ppphhhh", ip)
+ assert not AdcMatrixlike._is_valid_space("ph", ip)
+ assert not AdcMatrixlike._is_valid_space("p", ip)
+
+ def test_validate_space_ea(self):
+ ea = AdcMethod("ea-adc0")
+ assert AdcMatrixlike._is_valid_space("p", ea)
+ assert AdcMatrixlike._is_valid_space("pph", ea)
+ assert AdcMatrixlike._is_valid_space("pppphhh", ea)
+ assert not AdcMatrixlike._is_valid_space("ph", ea)
+ assert not AdcMatrixlike._is_valid_space("h", ea)
+
h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop()
-methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
+pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
+ip_methods = ["ip-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]]
+ea_methods = ["ea-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]]
+
+cases = [(m, c) for c in ["gen", "cvs"] for m in pp_methods]
+# No CVS for IP-ADC (yet)
+# Test only for is_alpha=True for simplicity
+cases += [(m, c) for c in ["gen"] for m in ip_methods]
+cases += [(m, c) for c in ["gen"] for m in ea_methods]
# Distinct implementations of the matrix equations only exist for the cases
# "gen" and "cvs".
-@pytest.mark.parametrize("method", methods)
-@pytest.mark.parametrize("case", ["gen", "cvs"])
+@pytest.mark.parametrize("method,case", cases)
@pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"])
class TestAdcMatrix:
def load_matrix_data(self, system: str, case: str, method: str) -> dict:
@@ -171,16 +221,18 @@ def test_matvec(self, system: str, case: str, method: str):
if matrix.reference_state.restricted:
if matrix.method.adc_type == "pp":
kind = "singlet"
+ elif matrix.method.adc_type in ("ip", "ea"):
+ kind = "doublet"
else:
raise ValueError(f"Unknown adc type {matrix.method.adc_type}.")
else:
kind = "any" # we don't do the test for spin flip
trial_vec = self.construct_trial_vec(system, case, method, kind)
result = matrix @ trial_vec
- assert_allclose(matdata["matvec_singles"], result.ph.to_ndarray(),
+ assert_allclose(matdata["matvec_singles"], result.get(matrix.axis_blocks[0]).to_ndarray(),
rtol=1e-10, atol=1e-12)
if "matvec_doubles" in matdata:
- assert_allclose(matdata["matvec_doubles"], result.pphh.to_ndarray(),
+ assert_allclose(matdata["matvec_doubles"], result.get(matrix.axis_blocks[1]).to_ndarray(),
rtol=1e-10, atol=1e-12)
def test_compute_block(self, system: str, case: str, method: str):
@@ -191,6 +243,8 @@ def test_compute_block(self, system: str, case: str, method: str):
if matrix.reference_state.restricted:
if matrix.method.adc_type == "pp":
kind = "singlet"
+ elif matrix.method.adc_type in ("ip", "ea"):
+ kind = "doublet"
else:
raise ValueError(f"Unknwon adc type {matrix.method.adc_type}.")
else:
@@ -209,10 +263,89 @@ def test_compute_block(self, system: str, case: str, method: str):
atol=1e-12
)
+ def test_hermiticity(self, system, case, method):
+ matrix = self.construct_matrix(system, case, method)
+
+ # Only test for Hermitian ADC variants
+ # (Projected matrix may not preserve symmetry fully)
+ spin_change = 0
+ if matrix.method.adc_type == "ip":
+ spin_change = -0.5
+ elif matrix.method.adc_type == "ea":
+ spin_change = 0.5
+
+ v = adcc.guess_zero(matrix, spin_change=spin_change)
+ w = adcc.guess_zero(matrix, spin_change=spin_change)
+
+ v.set_random()
+ w.set_random()
+
+ Av = matrix @ v
+ Aw = matrix @ w
+
+ lhs = v.dot(Aw)
+ rhs = Av.dot(w)
+
+ assert abs(lhs - rhs) < 1e-10
+
class TestAdcMatrixInterface:
- @pytest.mark.parametrize("method", methods)
- @pytest.mark.parametrize("case", h2o_sto3g.cases)
+ @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods)
+ @pytest.mark.parametrize("system", ["h2o_sto3g"])
+ @pytest.mark.parametrize("case", ["gen"]) # no CVS for IP/EA
+ def test_axis_structure_all_types(self, system, case, method):
+ reference_state = testdata_cache.refstate(system=system, case=case)
+ ground_state = adcc.LazyMp(reference_state)
+
+ matrix = adcc.AdcMatrix(method, ground_state)
+
+ assert matrix.ndim == 2
+ assert matrix.shape[0] == matrix.shape[1]
+ assert len(matrix) == matrix.shape[0]
+
+ blocks = matrix.axis_blocks
+ assert isinstance(blocks, list)
+ assert len(blocks) >= 1
+
+ # Block ordering must follow excitation rank
+ lengths = [len(b) for b in blocks]
+ assert lengths == sorted(lengths)
+
+ # Axis dictionaries must match blocks
+ assert sorted(matrix.axis_spaces.keys(), key=len) == blocks
+ assert sorted(matrix.axis_lengths.keys(), key=len) == blocks
+
+ # Validate block labels by ADC type
+ adc_type = matrix.method.adc_type
+
+ if adc_type == "pp":
+ assert all(set(b).issubset({"p", "h"}) for b in blocks)
+ assert blocks[0].count("p") == 1
+ assert blocks[0].count("h") == 1
+
+ elif adc_type == "ip":
+ # First block must remove one electron
+ assert blocks[0].count("h") == 1
+ assert blocks[0].count("p") == 0
+
+ elif adc_type == "ea":
+ # First block must add one electron
+ assert blocks[0].count("p") == 1
+ assert blocks[0].count("h") == 0
+
+ else:
+ raise AssertionError(f"Unknown ADC type {adc_type}")
+
+ # Validate axis lengths consistency
+ for block in blocks:
+ assert matrix.axis_lengths[block] > 0
+
+ # Reference consistency
+ assert matrix.reference_state == reference_state
+ assert matrix.mospaces == reference_state.mospaces
+
+ @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods)
+ @pytest.mark.parametrize("case", ["gen"]) # No CVS for IP/EA
@pytest.mark.parametrize("system", ["h2o_sto3g"])
def test_properties(self, system: str, case: str, method: str):
reference_state = testdata_cache.refstate(system=system, case=case)
@@ -225,8 +358,14 @@ def test_properties(self, system: str, case: str, method: str):
assert matrix.is_core_valence_separated == ("cvs" in case)
# check that the blocks are correct
blocks = matrix.axis_blocks
+ assert isinstance(blocks, list)
+ assert len(blocks) >= 1
if matrix.method.adc_type == "pp":
assert blocks == ["ph", "pphh", "ppphhh"][:matrix.method.level // 2 + 1]
+ elif matrix.method.adc_type == "ip":
+ assert blocks == ["h", "phh", "pphhh"][:matrix.method.level // 2 + 1]
+ elif matrix.method.adc_type == "ea":
+ assert blocks == ["p", "pph", "ppphh"][:matrix.method.level // 2 + 1]
else:
raise NotImplementedError(f"Unknown adc type {matrix.method.adc_type}.")
assert sorted(matrix.axis_spaces.keys(), key=len) == blocks
@@ -268,19 +407,28 @@ def test_properties(self, system: str, case: str, method: str):
assert matrix.mospaces == reference_state.mospaces
assert isinstance(matrix.timer, adcc.timings.Timer)
- def test_intermediates_adc2(self):
+ @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"])
+ def test_intermediates_adc2(self, method: str):
ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen"))
- matrix = adcc.AdcMatrix("adc2", ground_state)
+ matrix = adcc.AdcMatrix(method, ground_state)
assert isinstance(matrix.intermediates, Intermediates)
intermediates = Intermediates(ground_state)
matrix.intermediates = intermediates
assert matrix.intermediates == intermediates
- def test_matvec_adc2(self):
+ @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"])
+ def test_matvec_adc2(self, method: str):
ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen"))
- matrix = adcc.AdcMatrix("adc2", ground_state)
+ matrix = adcc.AdcMatrix(method, ground_state)
+ blocks = matrix.axis_blocks
- vectors = [adcc.guess_zero(matrix) for _ in range(3)]
+ spin_change = 0
+ if matrix.method.adc_type == "ip":
+ spin_change = -0.5
+ elif matrix.method.adc_type == "ea":
+ spin_change = 0.5
+
+ vectors = [adcc.guess_zero(matrix, spin_change=spin_change) for _ in range(3)]
for vec in vectors:
vec.set_random()
v, w, x = vectors
@@ -293,32 +441,32 @@ def test_matvec_adc2(self):
# @ operator (1 vector)
resv = matrix @ v
diffv = refv - resv
- assert diffv.ph.dot(diffv.ph) < 1e-12
- assert diffv.pphh.dot(diffv.pphh) < 1e-12
+ assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12
+ assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12
# @ operator (multiple vectors)
resv, resw, resx = matrix @ [v, w, x]
diffs = [refv - resv, refw - resw, refx - resx]
for i in range(3):
- assert diffs[i].ph.dot(diffs[i].ph) < 1e-12
- assert diffs[i].pphh.dot(diffs[i].pphh) < 1e-12
+ assert diffs[i].get(blocks[0]).dot(diffs[i].get(blocks[0])) < 1e-12
+ assert diffs[i].get(blocks[1]).dot(diffs[i].get(blocks[1])) < 1e-12
# compute matvec
resv = matrix.matvec(v)
diffv = refv - resv
- assert diffv.ph.dot(diffv.ph) < 1e-12
- assert diffv.pphh.dot(diffv.pphh) < 1e-12
+ assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12
+ assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12
resv = matrix.rmatvec(v)
diffv = refv - resv
- assert diffv.ph.dot(diffv.ph) < 1e-12
- assert diffv.pphh.dot(diffv.pphh) < 1e-12
+ assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12
+ assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12
# Test apply
- resv.ph = matrix.block_apply("ph_ph", v.ph)
- resv.ph += matrix.block_apply("ph_pphh", v.pphh)
+ resv[blocks[0]] = matrix.block_apply(f"{blocks[0]}_{blocks[0]}", v.get(blocks[0]))
+ resv[blocks[0]] += matrix.block_apply(f"{blocks[0]}_{blocks[1]}", v.get(blocks[1]))
refv = matrix.matvec(v)
- diffv = resv.ph - refv.ph
+ diffv = resv.get(blocks[0]) - refv.get(blocks[0])
assert diffv.dot(diffv) < 1e-12
def test_extra_term(self):
@@ -389,40 +537,47 @@ def apply(invec):
@pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"])
+@pytest.mark.parametrize("method", ["adc3", "ip-adc3", "ea-adc3"])
class TestAdcMatrixShifted:
- def construct_matrices(self, system, shift):
+ def construct_matrices(self, system:str, method: str, shift: float):
reference_state = testdata_cache.refstate(system, case="gen")
ground_state = adcc.LazyMp(reference_state)
- matrix = adcc.AdcMatrix("adc3", ground_state)
+ matrix = adcc.AdcMatrix(method, ground_state)
shifted = AdcMatrixShifted(matrix, shift)
return matrix, shifted
- def test_diagonal(self, system: str):
+ def test_diagonal(self, system: str, method: str):
shift = -0.3
- matrix, shifted = self.construct_matrices(system, shift)
+ matrix, shifted = self.construct_matrices(system, method, shift)
- for block in ("ph", "pphh"):
+ for block in matrix.axis_blocks:
odiag = matrix.diagonal()[block].to_ndarray()
sdiag = shifted.diagonal()[block].to_ndarray()
assert np.max(np.abs(sdiag - shift - odiag)) < 1e-12
- def test_matmul(self, system: str):
+ def test_matmul(self, system: str, method: str):
shift = -0.3
- matrix, shifted = self.construct_matrices(system, shift)
+ matrix, shifted = self.construct_matrices(system, method, shift)
+ blocks = matrix.axis_blocks
- vec = adcc.guess_zero(matrix)
+ spin_change = 0
+ if matrix.method.adc_type == "ip":
+ spin_change = -0.5
+ elif matrix.method.adc_type == "ea":
+ spin_change = 0.5
+
+ vec = adcc.guess_zero(matrix, spin_change=spin_change)
vec.set_random()
ores = matrix @ vec
sres = shifted @ vec
- assert ores.ph.describe_symmetry() == sres.ph.describe_symmetry()
- assert ores.pphh.describe_symmetry() == sres.pphh.describe_symmetry()
+ for block in blocks:
+ assert ores.get(block).describe_symmetry() == sres.get(
+ block).describe_symmetry()
- diff_s = sres.ph - ores.ph - shift * vec.ph
- diff_d = sres.pphh - ores.pphh - shift * vec.pphh
- assert np.max(np.abs(diff_s.to_ndarray())) < 1e-12
- assert np.max(np.abs(diff_d.to_ndarray())) < 1e-12
+ diff = sres.get(block) - ores.get(block) - shift * vec.get(block)
+ assert np.max(np.abs(diff.to_ndarray())) < 1e-12
# TODO Test block_view, block_apply
diff --git a/adcc/tests/ChargedExcitations_test.py b/adcc/tests/ChargedExcitations_test.py
new file mode 100644
index 000000000..41a17325b
--- /dev/null
+++ b/adcc/tests/ChargedExcitations_test.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+import pytest
+from numpy.testing import assert_allclose
+
+from adcc.AdcMethod import AdcMethod
+from adcc.ChargedExcitations import DetachedStates, AttachedStates
+
+from .testdata_cache import testdata_cache
+
+
+# ---------------------------------------------------------------------
+# Shared parametrization (reuse your existing case table if available)
+# ---------------------------------------------------------------------
+
+cases_ip_ea = [
+ ("h2o_sto3g", "ip-adc2", "gen", "doublet"),
+ ("h2o_sto3g", "ea-adc2", "gen", "doublet"),
+]
+
+
+# ---------------------------------------------------------------------
+# 1. Basic construction + size consistency
+# ---------------------------------------------------------------------
+
+@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea)
+def test_ip_ea_basic_interface(system, method, case, kind):
+ adc_type = AdcMethod(method).adc_type
+
+ if adc_type == "ip":
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+ assert isinstance(state, DetachedStates)
+ elif adc_type == "ea":
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+ assert isinstance(state, AttachedStates)
+ else:
+ raise AssertionError("Unexpected ADC type")
+
+ # size matches number of excitation vectors
+ assert state.size == len(state.excitation_vector)
+ assert state.size == len(state.excitation_energy)
+
+
+# ---------------------------------------------------------------------
+# 2. Pole strength is well-defined and matches state count
+# ---------------------------------------------------------------------
+
+@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea)
+def test_ip_ea_pole_strength(system, method, case, kind):
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+
+ ps = state.pole_strength
+
+ assert len(ps) == state.size
+ assert (ps >= 0).all() # positive pole_strenghts
+
+
+# ---------------------------------------------------------------------
+# 3. QC variable export consistency
+# ---------------------------------------------------------------------
+
+@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea)
+def test_ip_ea_qcvars_export(system, method, case, kind):
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+
+ qcvars = state.to_qcvars(properties=False)
+
+ adc_type = AdcMethod(method).adc_type
+
+ if adc_type == "ip":
+ assert any("IONIZATION POTENTIALS" in key for key in qcvars)
+ elif adc_type == "ea":
+ assert any("ELECTRON AFFINITIES" in key for key in qcvars)
+
+ assert any("NUMBER" in key for key in qcvars)
+
+
+# ---------------------------------------------------------------------
+# 4. describe() runs without error and contains expected wording
+# ---------------------------------------------------------------------
+
+@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea)
+def test_ip_ea_describe(system, method, case, kind):
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+
+ adc_type = AdcMethod(method).adc_type
+ # Only test restricted alpha
+ if adc_type == "ip":
+ state.spin_change = -0.5
+ elif adc_type == "ea":
+ state.spin_change = 0.5
+
+ desc = state.describe()
+
+ assert state.kind in desc.lower()
+
+ if adc_type == "ip":
+ assert "ionization" in desc.lower()
+ assert "detachment" in desc.lower()
+ elif adc_type == "ea":
+ assert "affinity" in desc.lower()
+ assert "attachment" in desc.lower()
+
+
+# ---------------------------------------------------------------------
+# 5. IP/EA hermiticity-style sanity check
+# ---------------------------------------------------------------------
+#
+# Hermiticity of the ADC matrix itself belongs in:
+# tests/test_adc_matrix.py
+#
+# NOT here.
+#
+# However, what *is* appropriate here:
+# Energies must be real-valued.
+#
+
+@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea)
+def test_ip_ea_energies_real(system, method, case, kind):
+ state = testdata_cache.adcc_states(
+ system=system,
+ method=method,
+ case=case,
+ kind=kind,
+ is_alpha=True,
+ )
+
+ assert_allclose(state.excitation_energy.imag, 0.0)
diff --git a/adcc/tests/adc_ea/__init__.py b/adcc/tests/adc_ea/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/adcc/tests/adc_ea/state_diffdm_test.py b/adcc/tests/adc_ea/state_diffdm_test.py
new file mode 100644
index 000000000..f6d8299d8
--- /dev/null
+++ b/adcc/tests/adc_ea/state_diffdm_test.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2026 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+import numpy as np
+import pytest
+from math import sqrt
+
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like
+from adcc.adc_ea.state_diffdm_2p import state_diffdm_2p
+from adcc.adc_ea.state_diffdm import state_diffdm
+from adcc.MoSpaces import split_spaces
+
+from .. import testcases
+from ..testdata_cache import testdata_cache
+
+
+test_cases = testcases.get_by_filename("h2o_sto3g", "cn_sto3g")
+cases = [(case.file_name, c, kind, is_alpha)
+ for case in test_cases for c in ["gen"] for kind in case.kinds.ea
+ for is_alpha in ([True] if case.restricted else [True, False])]
+methods = ["ea-adc0", "ea-adc2"] # TODO: Test diffdm_ea_adc2_2p
+
+
+@pytest.mark.parametrize("method", methods)
+@pytest.mark.parametrize("system,case,kind,is_alpha", cases)
+class TestStateDiffDm:
+ def calculate_adcn_electron_affinity(self, state):
+ hf = state.reference_state
+ mp = state.ground_state
+ n_states = len(state.excitation_energy)
+ excitation_energy = np.zeros((n_states))
+ method = state.method
+ level = method.level
+
+ method_order_minus_one = None
+ if level - 1 >= 0:
+ method_order_minus_one = AdcMethod("ea-adc" + str(level - 1))
+
+ for ea in range(n_states):
+ evec = state.excitation_vector[ea]
+ # TODO switch to ISR(3) implemntation
+ if method.level == 3:
+ raise NotImplementedError(
+ "State density not implemented for EA-ADC(3)")
+ else:
+ dens_1p = state_diffdm(method, mp, evec)
+
+ # one particle
+ # fock operator part
+ excitation_energy[ea] = einsum("pq,pq", hf.foo, dens_1p.oo)
+ excitation_energy[ea] += einsum("pq,pq", hf.fvv, dens_1p.vv)
+
+ if method_order_minus_one is not None:
+ # two particle part
+ dens_2p = state_diffdm_2p(method_order_minus_one, mp, evec)
+ # go for ISR(1)-d for ADC(2)
+ if method_order_minus_one.level == 1:
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+ dens_2p.ooov += (
+ + sqrt(2) * einsum("ia,jk->ijka", einsum("b,iab->ia", evec.p, evec.pph), d_oo)
+ )
+ dens_2p.ovvv += (
+ - sqrt(2) * einsum("a,ibc->iabc", evec.p, evec.pph)
+ )
+ for block in dens_2p.blocks:
+ # compute
+ # 1/4 [(1 - P_pq) (1 - P_rs) 1 / (n_occ - 1) delta_qs]
+ # * D^pq_rs
+ # = 1 / (n_occ - 1) D^pq_rq
+ s1, s2, s3, s4 = split_spaces(block)
+ if s2 == s4:
+ eri_1p = np.einsum(
+ "piqi->pq", hf.eri(f"{s1}o1{s3}o1").to_ndarray()
+ )
+ n_occ = hf.foo.shape[1]
+ excitation_energy[ea] -= 1 / (n_occ - 1) * np.einsum(
+ "pr,pqrq->",
+ eri_1p,
+ dens_2p[block].to_ndarray()
+ )
+ # and the full 2e part
+ excitation_energy[ea] += 0.25 * einsum(
+ "pqrs,pqrs", dens_2p[block], hf.eri(block)
+ )
+ return excitation_energy
+
+
+ def test_ea_adcn(self, method: str, system: str, case: str, kind: str,
+ is_alpha: bool):
+ state = testdata_cache.adcc_states(
+ system=system, method=method, kind=kind, case=case,
+ is_alpha=is_alpha
+ )
+ ref = state.excitation_energy_uncorrected
+ adcn = self.calculate_adcn_electron_affinity(state)
+ np.testing.assert_allclose(adcn, ref, atol=1e-12)
diff --git a/adcc/tests/adc_ip/__init__.py b/adcc/tests/adc_ip/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/adcc/tests/adc_ip/state_diffdm_test.py b/adcc/tests/adc_ip/state_diffdm_test.py
new file mode 100644
index 000000000..286d6e73d
--- /dev/null
+++ b/adcc/tests/adc_ip/state_diffdm_test.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+## ---------------------------------------------------------------------
+##
+## Copyright (C) 2026 by the adcc authors
+##
+## This file is part of adcc.
+##
+## adcc is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## adcc is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with adcc. If not, see .
+##
+## ---------------------------------------------------------------------
+import numpy as np
+import pytest
+from math import sqrt
+
+from adcc.AdcMethod import AdcMethod
+from adcc.functions import einsum, zeros_like
+from adcc.adc_ip.state_diffdm_2p import state_diffdm_2p
+from adcc.adc_ip.state_diffdm import state_diffdm
+from adcc.MoSpaces import split_spaces
+
+from .. import testcases
+from ..testdata_cache import testdata_cache
+
+
+test_cases = testcases.get_by_filename("h2o_sto3g", "cn_sto3g")
+cases = [(case.file_name, c, kind, is_alpha)
+ for case in test_cases for c in ["gen"] for kind in case.kinds.ip
+ for is_alpha in ([True] if case.restricted else [True, False])]
+methods = ["ip-adc0", "ip-adc2"] # TODO: Test diffdm_ip_adc2_2p
+
+
+@pytest.mark.parametrize("method", methods)
+@pytest.mark.parametrize("system,case,kind,is_alpha", cases)
+class TestStateDiffDm:
+ def calculate_adcn_ionization_potential(self, state):
+ hf = state.reference_state
+ mp = state.ground_state
+ n_states = len(state.excitation_energy)
+ excitation_energy = np.zeros((n_states))
+ method = state.method
+ level = method.level
+
+ method_order_minus_one = None
+ if level - 1 >= 0:
+ method_order_minus_one = AdcMethod("ip-adc" + str(level - 1))
+
+ for ip in range(n_states):
+ evec = state.excitation_vector[ip]
+ # TODO switch to ISR(3) implemntation
+ if method.level == 3:
+ raise NotImplementedError(
+ "State density not implemented for IP-ADC(3)")
+ else:
+ dens_1p = state_diffdm(method, mp, evec)
+
+ # one particle
+ # fock operator part
+ excitation_energy[ip] = einsum("pq,pq", hf.foo, dens_1p.oo)
+ excitation_energy[ip] += einsum("pq,pq", hf.fvv, dens_1p.vv)
+
+ if method_order_minus_one is not None:
+ # two particle part
+ dens_2p = state_diffdm_2p(method_order_minus_one, mp, evec)
+ # go for ISR(1)-d for ADC(2)
+ if method_order_minus_one.level == 1:
+ d_oo = zeros_like(hf.foo)
+ d_oo.set_mask("ii", 1)
+ dens_2p.ooov += (
+ + sqrt(2) * einsum("k,ija->ijka", evec.h, evec.phh)
+ + sqrt(2) * einsum(
+ "ja,ik->ijka", einsum(
+ "l,jla->ja", evec.h, evec.phh), d_oo)
+ )
+ for block in dens_2p.blocks:
+ # compute
+ # 1/4 [(1 - P_pq) (1 - P_rs) 1 / (n_occ - 1) delta_qs]
+ # * D^pq_rs
+ # = 1 / (n_occ - 1) D^pq_rq
+ s1, s2, s3, s4 = split_spaces(block)
+ if s2 == s4:
+ eri_1p = np.einsum(
+ "piqi->pq", hf.eri(f"{s1}o1{s3}o1").to_ndarray()
+ )
+ n_occ = hf.foo.shape[1]
+ excitation_energy[ip] -= 1 / (n_occ - 1) * np.einsum(
+ "pr,pqrq->",
+ eri_1p,
+ dens_2p[block].to_ndarray()
+ )
+ # and the full 2e part
+ excitation_energy[ip] += 0.25 * einsum(
+ "pqrs,pqrs", dens_2p[block], hf.eri(block)
+ )
+ return excitation_energy
+
+
+ def test_ip_adcn(self, method: str, system: str, case: str, kind: str,
+ is_alpha: bool):
+ state = testdata_cache.adcc_states(
+ system=system, method=method, kind=kind, case=case,
+ is_alpha=is_alpha
+ )
+ ref = state.excitation_energy_uncorrected
+ adcn = self.calculate_adcn_ionization_potential(state)
+ np.testing.assert_allclose(adcn, ref, atol=1e-12)
diff --git a/adcc/tests/adc_pp/state_diffdm_test.py b/adcc/tests/adc_pp/state_diffdm_test.py
index 24c07cd51..c96ff1c6c 100644
--- a/adcc/tests/adc_pp/state_diffdm_test.py
+++ b/adcc/tests/adc_pp/state_diffdm_test.py
@@ -226,7 +226,7 @@ def state_diffdm_adc3(self, mp, amplitude) -> OneParticleDensity:
).symmetrise()
return dm
- def test_adcn(self, method: str, system: str, case: str, kind: str):
+ def test_pp_adcn(self, method: str, system: str, case: str, kind: str):
state = testdata_cache.adcc_states(
system=system, method=method, kind=kind, case=case
)
diff --git a/adcc/tests/generators/dump_adcc.py b/adcc/tests/generators/dump_adcc.py
index 1d859e34b..4b4e92b65 100644
--- a/adcc/tests/generators/dump_adcc.py
+++ b/adcc/tests/generators/dump_adcc.py
@@ -2,6 +2,7 @@
from adcc.AdcMatrix import AdcMatrix
from adcc.AmplitudeVector import AmplitudeVector
from adcc.ExcitedStates import ExcitedStates
+from adcc.ChargedExcitations import DetachedStates, AttachedStates
from adcc.hdf5io import emplace_dict
from adcc.LazyMp import LazyMp
from adcc.State2States import State2States
@@ -9,6 +10,7 @@
import numpy as np
import h5py
+from typing import Union
def dump_groundstate(ground_state: LazyMp, hdf5_file: h5py.Group,
only_full_mode: bool) -> None:
@@ -65,15 +67,56 @@ 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,
- 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: Union[DetachedStates, AttachedStates],
+ kind_data: dict, n_states: int) -> None:
+ """Collects all properties that are unique for IP/EA-ADC
+ """
+ kind_data["pole_strengths"] = (
+ states.pole_strength[:n_states]
+ )
+
+
+def dump_excited_states(
+ states: Union[ExcitedStates,DetachedStates, AttachedStates],
+ hdf5_file: h5py.Group, dump_nstates: int = None) -> None:
+ """
+ Dump the (charged) excited states data to the given hdf5 file/group.
The number of states to dump can be given by dump_nstates. By default all states
are dumped.
"""
# 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)
@@ -82,8 +125,6 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group,
dm_bb_a = [] # State diffdm AO basis alpha part
dm_bb_b = [] # State diffdm AO basis beta part.
- tdm_bb_a = [] # Ground to Excited state tdm AO basis alpha part
- tdm_bb_b = [] # Ground to Excited state tdm AO basis beta part
# split the eigenvectors according to their excitation degree for all states
eigenvectors: dict[int, list] = {}
for n in range(n_states):
@@ -91,41 +132,27 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group,
bb_a, bb_b = states.state_diffdm[n].to_ao_basis(states.reference_state)
dm_bb_a.append(bb_a.to_ndarray())
dm_bb_b.append(bb_b.to_ndarray())
- bb_a, bb_b = states.transition_dm[n].to_ao_basis(states.reference_state)
- tdm_bb_a.append(bb_a.to_ndarray())
- tdm_bb_b.append(bb_b.to_ndarray())
# eigenvectors
for exdegree, block in enumerate(states.matrix.axis_blocks):
if exdegree + 1 not in eigenvectors:
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:
diff --git a/adcc/tests/generators/generate_adcc_data.py b/adcc/tests/generators/generate_adcc_data.py
index 1b3f37cb1..837b9ba43 100644
--- a/adcc/tests/generators/generate_adcc_data.py
+++ b/adcc/tests/generators/generate_adcc_data.py
@@ -5,6 +5,7 @@
from adcc.tests import testcases
from adcc.AdcMethod import AdcMethod
+from adcc.AdcMatrix import AdcMatrixlike
from adcc.LazyMp import LazyMp
from adcc.workflow import run_adc, validate_state_parameters
from adcc import copy as adcc_copy
@@ -17,8 +18,11 @@
# the base methods for each adc_type for which to generate data
# the different cases (cvs, fc, ...) are handled in the generate functions.
+# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0
_methods = {
- "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3")
+ "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"),
+ "ip": ("ip-adc0", "ip-adc2", "ip-adc2x", "ip-adc3"),
+ "ea": ("ea-adc0", "ea-adc2", "ea-adc2x", "ea-adc3"),
}
@@ -26,6 +30,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
gs_density_order: int | None = 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
@@ -39,13 +44,20 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
# the kind at this point to check if we need to perform a calculation
# for this purpose we need the reference state
hf = testdata_cache.refstate(system=test_case, case=case)
- _, kind = validate_state_parameters(
- hf, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets,
- n_spin_flip=n_spin_flip
+ matrix = AdcMatrixlike()
+ matrix.reference_state = hf
+ matrix.method = method
+ _, kind, is_alpha = validate_state_parameters(
+ matrix, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets,
+ n_spin_flip=n_spin_flip, n_doublets=n_doublets, is_alpha=is_alpha
)
key = f"{case}/{gs_density_order}"
if f"{key}/{kind}" in hdf5_file:
return None
+ if method.adc_type in ("ip", "ea"):
+ spin = "alpha" if is_alpha else "beta"
+ if f"{key}/{spin}/{kind}" in hdf5_file:
+ return None
print(f"Generating {method.name} data for {case} {test_case.file_name}.")
# prepend cvs to the method if needed (otherwise we will get an error)
if "cvs" in case and not method.is_core_valence_separated:
@@ -55,13 +67,16 @@ 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 ("ip", "ea"):
+ key = f"{key}/{spin}"
+ if f"{key}/matrix" not in hdf5_file and spin == "alpha":
+ # the matrix data is only dumped once for each case and only for alpha.
+ # I think it does not make sense to dump the data once for a singlet
+ # and once for a triplet trial vector as well as an alpha and a beta one.
matrix_group = hdf5_file.create_group(f"{key}/matrix")
trial_vec = adcc_copy(states.excitation_vector[0]).set_random() # type: ignore # noqa: E501
dump_matrix_testdata(states.matrix, trial_vec, matrix_group)
@@ -73,6 +88,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:
@@ -86,11 +102,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
)
@@ -141,6 +160,29 @@ def generate_h2o_sto3g():
test_case, method=method, dump_nstates=2, states_per_case=per_case,
**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 < 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():
@@ -158,6 +200,26 @@ def generate_h2o_def2tzvp():
test_case, method=method, dump_nstates=2, states_per_case=None,
**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():
@@ -168,12 +230,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 < 2:
+ n_states = {n_states: 2}
+ else:
+ n_states = {n_states: 3}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ is_alpha=is_alpha, **n_states
+ )
+
+ for method in _methods["ea"]:
+ method = AdcMethod(method)
+ for n_states in \
+ testcases.kinds_to_nstates(test_case.kinds[method.adc_type]):
+ if method.level < 2:
+ n_states = {n_states: 2}
+ else:
+ n_states = {n_states: 3}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ is_alpha=is_alpha, **n_states
+ )
+
def generate_cn_ccpvdz():
# UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt
@@ -189,6 +278,27 @@ def generate_cn_ccpvdz():
**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
@@ -204,6 +314,27 @@ def generate_hf_631g():
**n_states
)
+ for is_alpha in [True, False]:
+ for method in _methods["ip"]:
+ method = AdcMethod(method)
+ for n_states in \
+ testcases.kinds_to_nstates(test_case.kinds[method.adc_type]):
+ n_states = {n_states: 3}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ is_alpha=is_alpha, **n_states
+ )
+
+ for method in _methods["ea"]:
+ method = AdcMethod(method)
+ for n_states in \
+ testcases.kinds_to_nstates(test_case.kinds[method.adc_type]):
+ n_states = {n_states: 3}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ is_alpha=is_alpha, **n_states
+ )
+
def main():
generate_h2o_sto3g()
diff --git a/adcc/tests/generators/generate_adcman_data.py b/adcc/tests/generators/generate_adcman_data.py
index 088980585..c725faef4 100644
--- a/adcc/tests/generators/generate_adcman_data.py
+++ b/adcc/tests/generators/generate_adcman_data.py
@@ -12,8 +12,12 @@
# the base methods for each adc_type for which to generate data
# the different cases (cvs, fc, ...) are handled in the generate functions.
+# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0
+# ip/ea-adc2x not implemented in Q-Chem
_methods = {
- "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3")
+ "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"),
+ "ip": ("ip-adc0", "ip-adc2", "ip-adc3"),
+ "ea": ("ea-adc0", "ea-adc2", "ea-adc3"),
}
# Since it seems not possible to only perform an adcman MPn calculation,
# the ground state data has to be extracted from an adc(n) calculation.
@@ -35,6 +39,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
@@ -45,7 +51,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str,
datadir = Path(__file__).parent.parent / _testdata_dirname
datafile = datadir / test_case.adcdata_file_name("adcman", method.name)
hdf5_file = h5py.File(datafile, "a") # Read/write if exists, create otherwise
- if f"{case}/{gs_density_order}" in hdf5_file:
+ key = f"{case}/{gs_density_order}"
+ if key in hdf5_file:
return None
# skip cvs-adc(0), since it is not available in qchem.
if "cvs" in case and method.level == 0:
@@ -61,9 +68,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
@@ -74,6 +82,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:
@@ -87,12 +97,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
)
@@ -171,6 +185,25 @@ def generate_h2o_sto3g():
states_per_case=states.get(method.name, None), **n_states
)
+ for method in _methods["ip"]:
+ method = AdcMethod(method)
+ n_states = {"n_ip_states": (3, 0)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ states_per_case=states.get(method.name, None), **n_states
+ )
+
+ for method in _methods["ea"]:
+ method = AdcMethod(method)
+ if method.level < 2:
+ n_states = {"n_ea_states": (1, 0)}
+ else:
+ n_states = {"n_ea_states": (3, 0)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2,
+ states_per_case=states.get(method.name, None), **n_states
+ )
+
def generate_h2o_def2tzvp():
# RHF, Singlet, 43 basis functions: 5 occ, 38 virt.
@@ -187,6 +220,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
@@ -201,6 +248,26 @@ def generate_cn_sto3g():
**n_states
)
+ for method in _methods["ip"]:
+ method = AdcMethod(method)
+ if method.level < 2:
+ n_states = {"n_ip_states": (2, 2)}
+ else:
+ n_states = {"n_ip_states": (3, 3)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2, **n_states
+ )
+
+ for method in _methods["ea"]:
+ method = AdcMethod(method)
+ if method.level < 2:
+ n_states = {"n_ea_states": (2, 2)}
+ else:
+ n_states = {"n_ea_states": (3, 3)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2, **n_states
+ )
+
def generate_cn_ccpvdz():
# UHF, Doublet
@@ -215,6 +282,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
@@ -229,6 +310,20 @@ def generate_hf_631g():
**n_states
)
+ for method in _methods["ip"]:
+ method = AdcMethod(method)
+ n_states = {"n_ip_states": (3, 3)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2, **n_states
+ )
+
+ for method in _methods["ea"]:
+ method = AdcMethod(method)
+ n_states = {"n_ea_states": (3, 3)}
+ generate_adc_all(
+ test_case, method=method, dump_nstates=2, **n_states
+ )
+
def generate_formaldehyde_pe():
for test_case in testcases.get(n_expected_cases=2, name="formaldehyde"):
diff --git a/adcc/tests/generators/import_qchem_data.py b/adcc/tests/generators/import_qchem_data.py
index b9f2840fd..fdbbbe9ff 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 = {
- "pp": [
- ("singlets", True), ("triplets", True), # restricted
+ "pp": {
+ True: ["singlets", "triplets"], # restricted
# uhf and spin flip are located in the same ".../uhf/..." subtree
- ("any_or_spinflip", False), # unrestricted
- ]
+ False: ["any_or_spinflip"], # unrestricted
+ },
+ "ip": {
+ True: [""], # restricted
+ False: ["alphas", "betas"], # unrestricted
+ },
+ "ea": {
+ True: [""], # restricted
+ False: ["alphas", "betas"], # unrestricted
+ },
}
# Of course the kinds have to have a slightly different name in adcc....
- kind_map = {"singlets": "singlet", "triplets": "triplet"}
+ kind_map = {"singlets": "singlet", "triplets": "triplet",
+ "alphas": "alpha", "betas": "beta"}
# also the adcc methods have to be translated
- method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn
- if method_name.endswith("adc2"): # adc2 -> adc2s
- method_name += "s"
+ if method.adc_type == "pp":
+ method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn
+ if method_name.endswith("adc2"): # adc2 -> adc2s (Only for PP)
+ method_name += "s"
+ else:
+ method_name: str = method.name.split('-')[-1] # No cvs (yet)
+ restricted = "rhf" in context[f"adc_{method.adc_type}"][method_name].keys()
+
# go through the different possible state kinds and import the states.
data = {}
- for kind, restricted in state_kinds[method.adc_type]:
+ for kind in state_kinds[method.adc_type][restricted]:
states = _import_excited_states(
context, method=method_name, 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 == "pp":
+ data[kind_map.get(kind, kind)] = states
+ elif method.adc_type in ("ip", "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
@@ -149,13 +170,18 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool
tensors are exported too. The dimensions can be found by adding the given
prefix to the context tree of the object.
"""
- # build the path under which to find the exicted states
+ # build the path under which to find the excited states
tree = [f"adc_{adc_type}", method]
if restricted:
- assert state_kind is not None # needs to be defined for restricted calcs
- tree.extend(["rhf", state_kind])
+ tree.append("rhf")
+ if adc_type == "pp":
+ assert state_kind is not None # needs to be defined for restricted calcs
+ tree.append(state_kind)
else:
tree.append("uhf")
+ if adc_type in ("ip", "ea"):
+ assert state_kind is not None
+ tree.append(state_kind)
tree.append("0") # we assume that we only have a single irrep!!
tree = "/".join(tree)
# check that we have states to read and return if not
@@ -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 == "pp":
+ state_tree = tree + f"/es{n}"
+ elif adc_type == "ip":
+ state_tree = tree + f"/ip{n}"
+ elif adc_type == "ea":
+ state_tree = tree + f"/ea{n}"
# ensure that the state is converged
_, converged = _extract_dataset(context[f"{state_tree}/converged"])
if not converged:
@@ -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}", method]
if restricted:
- assert state_kind is not None # needs to be defined for restricted calcs
- tree.extend(["rhf", "isr", state_kind])
+ tree.extend(["rhf", "isr"])
+ if adc_type == "pp":
+ assert state_kind is not None # needs to be defined for restricted calcs
+ tree.append(state_kind)
else:
tree.extend(["uhf", "isr"])
+ if adc_type in ("ip", "ea"):
+ assert state_kind is not None
+ tree.append(state_kind)
tree.append("0-0") # we assume that we only have a single irrep!
tree = "/".join(tree)
if tree not in context:
@@ -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 559f81da1..62f5f9f81 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
@@ -120,6 +122,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
@@ -349,6 +352,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,
@@ -405,6 +410,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,
@@ -441,6 +450,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}
@@ -497,7 +510,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)",
}
_gs_density_order_dict = {
diff --git a/adcc/tests/guess_test.py b/adcc/tests/guess_test.py
index 6463500c0..ea4f1b9e8 100644
--- a/adcc/tests/guess_test.py
+++ b/adcc/tests/guess_test.py
@@ -34,8 +34,12 @@
# The methods to test
-singles_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
-doubles_methods = ["adc2", "adc2x", "adc3"]
+singles_methods_pp = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
+doubles_methods_pp = ["adc2", "adc2x", "adc3"]
+singles_methods_ip = ["ip-adc0", "ip-adc1", "ip-adc2", "ip-adc2x", "ip-adc3"]
+doubles_methods_ip = ["ip-adc2", "ip-adc2x", "ip-adc3"]
+singles_methods_ea = ["ea-adc0", "ea-adc1", "ea-adc2", "ea-adc2x", "ea-adc3"]
+doubles_methods_ea = ["ea-adc2", "ea-adc2x", "ea-adc3"]
# the testcases
h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop()
cn_sto3g = testcases.get_by_filename("cn_sto3g").pop()
@@ -43,6 +47,110 @@
class TestGuess:
+ def test_determine_spin_change_pp(self):
+ from adcc.guess import determine_spin_change
+
+ method = adcc.AdcMethod("adc2")
+
+ spin = determine_spin_change(method, kind="singlet")
+ assert spin == 0.0
+
+ spin = determine_spin_change(method, kind="spin_flip")
+ assert spin == -1.0
+
+
+ def test_determine_spin_change_ip(self):
+ from adcc.guess import determine_spin_change
+
+ method = adcc.AdcMethod("ip-adc2")
+
+ spin_alpha = determine_spin_change(method, kind="any", is_alpha=True)
+ spin_beta = determine_spin_change(method, kind="any", is_alpha=False)
+
+ assert spin_alpha == -0.5
+ assert spin_beta == +0.5
+
+ with pytest.raises(TypeError):
+ determine_spin_change(method, kind="any", is_alpha=None)
+
+ def test_determine_spin_change_ea(self):
+ from adcc.guess import determine_spin_change
+
+ method = adcc.AdcMethod("ea-adc2")
+
+ spin_alpha = determine_spin_change(method, kind="any", is_alpha=True)
+ spin_beta = determine_spin_change(method, kind="any", is_alpha=False)
+
+ assert spin_alpha == +0.5
+ assert spin_beta == -0.5
+
+ with pytest.raises(TypeError):
+ determine_spin_change(method, kind="any", is_alpha=None)
+
+ def test_determine_spin_change_unknown_adc_type(self):
+ from adcc.guess import determine_spin_change
+
+ method = adcc.AdcMethod("adc2")
+ method.adc_type = "bla"
+
+ with pytest.raises(ValueError, match="Unknown ADC method"):
+ determine_spin_change(method, kind="any")
+
+ def test_estimate_n_guesses_pp(self):
+ from adcc.guess import estimate_n_guesses
+
+ refstate = testdata_cache.refstate("h2o_sto3g", case="gen")
+ ground_state = adcc.LazyMp(refstate)
+ matrix = adcc.AdcMatrix("adc2", ground_state)
+
+ # Check minimal number of guesses is 4 and at some point
+ # we get more than four guesses
+ assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True)
+ assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True)
+ for i in range(3, 20):
+ assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True)
+
+ def test_estimate_n_guesses_ip(self):
+ from adcc.guess import estimate_n_guesses
+
+ refstate = testdata_cache.refstate("h2o_sto3g", case="gen")
+ ground_state = adcc.LazyMp(refstate)
+ matrix = adcc.AdcMatrix("ip-adc2", ground_state)
+
+ # Check minimal number of guesses is 4 and at some point
+ # we get more than four guesses
+ assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True)
+ assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True)
+ for i in range(3, 20):
+ assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True)
+
+ # Test different behaviour for IP-ADC(0/1)
+ matrix = adcc.AdcMatrix("ip-adc0", ground_state)
+ assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True)
+ assert 5 == estimate_n_guesses(matrix, n_states=5, singles_only=True)
+ assert 10 == estimate_n_guesses(matrix, n_states=10, singles_only=True)
+
+ def test_estimate_n_guesses_ea(self):
+ from adcc.guess import estimate_n_guesses
+
+ refstate = testdata_cache.refstate("h2o_sto3g", case="gen")
+ ground_state = adcc.LazyMp(refstate)
+ matrix = adcc.AdcMatrix("ea-adc2", ground_state)
+
+ # Check minimal number of guesses is 4 and at some point
+ # we get more than four guesses
+ assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True)
+ assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True)
+ for i in range(3, 20):
+ assert i <= estimate_n_guesses(matrix, n_states=i,
+ singles_only=True)
+
+ # Test different behaviour for EA-ADC(0/1)
+ matrix = adcc.AdcMatrix("ea-adc0", ground_state)
+ assert 2 == estimate_n_guesses(matrix, n_states=1, singles_only=True)
+ assert 2 == estimate_n_guesses(matrix, n_states=2, singles_only=True)
+ assert 3 == estimate_n_guesses(matrix, n_states=3, singles_only=True)
+
def assert_symmetry_no_spin_change(self, matrix, guess, block,
spin_block_symmetrisation):
"""
@@ -183,6 +291,152 @@ def assert_symmetry_spin_flip(self, matrix, guess, block):
has_babb = np.max(np.abs(gtd[noa:, :nCa, nva:, nva:])) > 0
assert has_aaab or has_aaba or has_abbb or has_babb
+ def assert_symmetry_ip(self, matrix, guess, block, is_alpha):
+ """
+ Assert a guess vector has the correct symmetry for alpha/beta detachment
+ """
+ # Extract useful quantities
+ mospaces = matrix.mospaces
+ nCa = noa = mospaces.n_orbs_alpha("o1")
+ nCb = nob = mospaces.n_orbs_beta("o1")
+ nva = mospaces.n_orbs_alpha("v1")
+ nvb = mospaces.n_orbs_beta("v1")
+ if mospaces.has_core_occupied_space:
+ nCa = mospaces.n_orbs_alpha("o2")
+ nCb = mospaces.n_orbs_beta("o2")
+
+ # Singles
+ gts = guess.h.to_ndarray()
+ assert gts.shape == (nCa + nCb,)
+ if is_alpha:
+ assert np.max(np.abs(gts[nCa:])) == 0
+ else:
+ assert np.max(np.abs(gts[:nCa])) == 0
+
+ # Doubles
+ if "phh" not in matrix.axis_blocks:
+ return
+
+ gtd = guess.phh.to_ndarray()
+ assert gtd.shape == (noa + nob, nCa + nCb, nva + nvb)
+
+ if is_alpha:
+ assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a
+ assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a
+ assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b
+ assert np.max(np.abs(gtd[noa:, nCa:, :nva])) == 0 # bb->a
+ else:
+ assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b
+ assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b
+ assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a
+ assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b
+
+ if matrix.reference_state.restricted:
+ # Restricted automatically means alpha ionization
+ # Thus forbid spin-flip blocks with right spin
+ assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b
+
+ if not matrix.is_core_valence_separated:
+ assert_array_equal(gtd.transpose((1, 0, 2)), -gtd)
+
+ if block == "h":
+ if is_alpha:
+ assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b
+ assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b
+ assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a
+
+ assert np.max(np.abs(gts[:nCa])) > 0 # has_alpha
+ else:
+ assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a
+ assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a
+ assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b
+
+ assert np.max(np.abs(gts[nCa:])) > 0 # has_beta
+ elif block == "phh":
+ if is_alpha:
+ assert np.max(np.abs(gts[:nCa])) == 0
+ has_aaa = np.max(np.abs(gtd[:noa, :nCa, :nva])) > 0
+ has_abb = np.max(np.abs(gtd[:noa, nCa:, nva:])) > 0
+ has_bab = np.max(np.abs(gtd[noa:, :nCa, nva:])) > 0
+ assert has_aaa or has_abb or has_bab
+ else:
+ assert np.max(np.abs(gts[nCa:])) == 0
+ has_aba = np.max(np.abs(gtd[:noa, nCa:, :nva])) > 0
+ has_baa = np.max(np.abs(gtd[noa:, :nCa, :nva])) > 0
+ has_bbb = np.max(np.abs(gtd[noa:, nCa:, nva:])) > 0
+ assert has_aba or has_baa or has_bbb
+
+ def assert_symmetry_ea(self, matrix, guess, block, is_alpha):
+ """
+ Assert a guess vector has the correct symmetry for alpha/beta attachment
+ """
+ # Extract useful quantities
+ mospaces = matrix.mospaces
+ noa = mospaces.n_orbs_alpha("o1")
+ nob = mospaces.n_orbs_beta("o1")
+ nva = mospaces.n_orbs_alpha("v1")
+ nvb = mospaces.n_orbs_beta("v1")
+
+ # Singles
+ gts = guess.p.to_ndarray()
+ assert gts.shape == (nva + nvb,)
+ if is_alpha:
+ assert np.max(np.abs(gts[nva:])) == 0
+ else:
+ assert np.max(np.abs(gts[:nva])) == 0
+
+ # Doubles
+ if "pph" not in matrix.axis_blocks:
+ return
+
+ gtd = guess.pph.to_ndarray()
+ assert gtd.shape == (noa + nob, nva + nvb, nva + nvb)
+
+ if is_alpha:
+ assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab
+ assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba
+ assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb
+ assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb
+ else:
+ assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab
+ assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba
+ assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa
+ assert np.max(np.abs(gtd[noa:, :nva, :nva])) == 0 # b->aa
+
+ if matrix.reference_state.restricted:
+ # Restricted automatically means alpha attachment
+ # Thus forbid spin-flip blocks with right spin
+ assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb
+
+ assert_array_equal(gtd.transpose((0, 2, 1)), -gtd)
+
+ if block == "p":
+ if is_alpha:
+ assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab
+ assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba
+ assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa
+
+ assert np.max(np.abs(gts[:nva])) > 0 # has_alpha
+ else:
+ assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab
+ assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba
+ assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb
+
+ assert np.max(np.abs(gts[nva:])) > 0 # has_beta
+ elif block == "pph":
+ if is_alpha:
+ assert np.max(np.abs(gts[:nva])) == 0
+ has_aaa = np.max(np.abs(gtd[:noa, :nva, :nva])) > 0
+ has_bab = np.max(np.abs(gtd[noa:, :nva, nva:])) > 0
+ has_bba = np.max(np.abs(gtd[noa:, nva:, :nva])) > 0
+ assert has_aaa or has_bab or has_bba
+ else:
+ assert np.max(np.abs(gts[nva:])) == 0
+ has_aab = np.max(np.abs(gtd[:noa, :nva, nva:])) > 0
+ has_aba = np.max(np.abs(gtd[:noa, nva:, :nva])) > 0
+ has_bbb = np.max(np.abs(gtd[noa:, nva:, nva:])) > 0
+ assert has_aab or has_aba or has_bbb
+
def assert_orthonormal(self, guesses):
for (i, gi) in enumerate(guesses):
for (j, gj) in enumerate(guesses):
@@ -190,10 +444,10 @@ def assert_orthonormal(self, guesses):
assert adcc.dot(gi, gj) == approx(ref)
def assert_guess_values(self, matrix, block, guesses, spin_flip=False,
- triplet=False):
+ triplet=False, is_alpha: bool = None):
"""
- Assert that the guesses correspond to the smallest
- diagonal values.
+ Assert that the provided guesses correspond to the smallest
+ allowed diagonal elements for the requested block.
"""
# Extract useful quantities
mospaces = matrix.mospaces
@@ -204,40 +458,42 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False,
# Make a list of diagonal indices, ordered by the corresponding
# diagonal values
- sidcs = None
- if block == "ph":
- diagonal = matrix.diagonal().ph.to_ndarray()
+ diagonal = matrix.diagonal().get(block).to_ndarray()
+
+ # Doubles guesses are constructed from the 0th order diagonal
+ if matrix.method.level > 1 and not matrix.method.name.endswith("adc2"):
+ if block == "pphh":
+ diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0(
+ matrix.reference_state
+ ).pphh.to_ndarray()
+ elif block == "phh":
+ diagonal = adcc.adc_ip.matrix.diagonal_phh_phh_0(
+ matrix.reference_state
+ ).phh.to_ndarray()
+ elif block == "pph":
+ diagonal = adcc.adc_ea.matrix.diagonal_pph_pph_0(
+ matrix.reference_state
+ ).pph.to_ndarray()
+
+ # Build list of indices, which would sort the diagonal
+ order = np.argsort(diagonal.ravel())
+ sidcs = list(zip(*np.unravel_index(order, diagonal.shape)))
+ assert sidcs
- # Build list of indices, which would sort the diagonal
- sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()),
- diagonal.shape))
- assert sidcs.shape[0] == 1
+ if block == "ph":
if spin_flip:
- sidcs = [idx for idx in sidcs[0]
- if idx[0] < nCa and idx[1] >= nva]
+ sidcs = [idx for idx in sidcs
+ if idx[0] < nCa and idx[1] >= nva]
else:
sidcs = [
- idx for idx in sidcs[0]
+ idx for idx in sidcs
if any((idx[0] >= nCa and idx[1] >= nva,
idx[0] < nCa and idx[1] < nva)) # noqa: E221
]
elif block == "pphh":
- # the doubles guesses are constructed from the 0th order diagonal
- if matrix.method.name.endswith("adc2"):
- diagonal = matrix.diagonal().pphh.to_ndarray()
- else:
- diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0(
- matrix.reference_state
- ).pphh.to_ndarray()
-
- # Build list of indices, which would sort the diagonal
- sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()),
- diagonal.shape))
-
- assert sidcs.shape[0] == 1
if spin_flip:
sidcs = [
- idx for idx in sidcs[0]
+ idx for idx in sidcs
if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] >= nva, # noqa: E221,E501
idx[0] < noa and idx[1] < nCa and idx[2] >= nva and idx[3] < nva, # noqa: E221,E501
idx[0] < noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501
@@ -245,7 +501,7 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False,
]
else:
sidcs = [
- idx for idx in sidcs[0]
+ idx for idx in sidcs
# aaaa / bbbb / abab / baba / abba / baab
if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] < nva, # noqa: E221,E501
idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501
@@ -261,17 +517,69 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False,
# cover the ccvv block in CVS-ADC.
if triplet and not mospaces.has_core_occupied_space:
sidcs = [idx for idx in sidcs
- if abs(idx[0] - idx[1]) != noa
- or abs(idx[2] - idx[3]) != nva]
+ if abs(idx[0] - idx[1]) != noa
+ or abs(idx[2] - idx[3]) != nva]
sidcs = [idx for idx in sidcs if idx[2] != idx[3]]
if not matrix.is_core_valence_separated:
sidcs = [idx for idx in sidcs if idx[0] != idx[1]]
+ elif block == "h":
+ # IP-ADC singles
+ if is_alpha:
+ sidcs = [idx for idx in sidcs if idx[0] < noa]
+ else:
+ sidcs = [idx for idx in sidcs if idx[0] >= noa]
+ elif block == "phh":
+ # IP-ADC doubles
+ if is_alpha:
+ sidcs = [
+ idx for idx in sidcs
+ # aaa / abb / bab
+ if any((
+ idx[0] < noa and idx[1] < nCa and idx[2] < nva,
+ idx[0] < noa and idx[1] >= nCa and idx[2] >= nva,
+ idx[0] >= noa and idx[1] < nCa and idx[2] >= nva))
+ ]
+ else:
+ sidcs = [
+ idx for idx in sidcs
+ # aba / baa / bbb
+ if any((
+ idx[0] < noa and idx[1] >= nCa and idx[2] < nva,
+ idx[0] >= noa and idx[1] < nCa and idx[2] < nva,
+ idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva))
+ ]
+ sidcs = [idx for idx in sidcs if idx[0] != idx[1]]
+ elif block == "p":
+ # EA-ADC singles
+ if is_alpha:
+ sidcs = [idx for idx in sidcs if idx[0] < nva]
+ else:
+ sidcs = [idx for idx in sidcs if idx[0] >= nva]
+ elif block == "pph":
+ # EA-ADC doubles
+ if is_alpha:
+ sidcs = [
+ idx for idx in sidcs
+ if any((
+ idx[0] < noa and idx[1] < nva and idx[2] < nva,
+ idx[0] >= noa and idx[1] < nva and idx[2] >= nva,
+ idx[0] >= noa and idx[1] >= nva and idx[2] < nva))
+ ]
+ else:
+ sidcs = [
+ idx for idx in sidcs
+ if any((
+ idx[0] < noa and idx[1] < nva and idx[2] >= nva,
+ idx[0] < noa and idx[1] >= nva and idx[2] < nva,
+ idx[0] >= noa and idx[1] >= nva and idx[2] >= nva))
+ ]
+ sidcs = [idx for idx in sidcs if idx[1] != idx[2]]
# Group the indices by corresponding diagonal value
def grouping(x):
return np.round(diagonal[tuple(x)], decimals=12)
gidcs = [[tuple(gitem) for gitem in group]
- for _, group in itertools.groupby(sidcs, grouping)]
+ for key, group in itertools.groupby(sidcs, grouping)]
igroup = 0 # The current diagonal value group we are in
for (i, guess) in enumerate(guesses):
# Extract indices of non-zero elements
@@ -328,9 +636,47 @@ def base_test_spin_flip(self, system: str, case: str, method: str, block: str,
self.assert_orthonormal(guesses)
self.assert_guess_values(matrix, block, guesses, spin_flip=True)
- @pytest.mark.parametrize("method", singles_methods)
+ def base_test_ip(self, system: str, case: str, method: str, block: str,
+ is_alpha: bool, max_guesses: int = 10):
+ """
+ Test IP-ADC guess construction for alpha/beta detachment
+ """
+ hf = testdata_cache.refstate(system, case=case)
+ matrix = adcc.AdcMatrix(method, hf)
+ spin_change = -0.5 if is_alpha else +0.5
+ for n_guesses in range(3, max_guesses + 1):
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_change=spin_change,
+ is_alpha=is_alpha
+ )
+ assert len(guesses) == n_guesses
+ for gs in guesses:
+ self.assert_symmetry_ip(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+ self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha)
+
+ def base_test_ea(self, system: str, case: str, method: str, block: str,
+ is_alpha: bool, max_guesses: int = 10):
+ """
+ Test EA-ADC guess construction for alpha/beta attachment
+ """
+ hf = testdata_cache.refstate(system, case=case)
+ matrix = adcc.AdcMatrix(method, hf)
+ spin_change = +0.5 if is_alpha else -0.5
+ for n_guesses in range(1, max_guesses + 1):
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, n_guesses, block=block, spin_change=spin_change,
+ is_alpha=is_alpha
+ )
+ assert len(guesses) == n_guesses
+ for gs in guesses:
+ self.assert_symmetry_ea(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+ self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha)
+
+ @pytest.mark.parametrize("method", singles_methods_pp)
@pytest.mark.parametrize("case", h2o_sto3g.cases)
- def test_singles_h2o(self, method: str, case: str):
+ def test_singles_h2o_pp(self, method: str, case: str):
guesses = { # fewer guesses available
"fv-cvs": 1, "cvs": 2, "fc": 8, "fv": 5, "fc-fv": 4, "fc-cvs": 2,
"fc-fv-cvs": 1
@@ -340,9 +686,9 @@ def test_singles_h2o(self, method: str, case: str):
max_guesses=guesses.get(case, 10)
)
- @pytest.mark.parametrize("method", doubles_methods)
+ @pytest.mark.parametrize("method", doubles_methods_pp)
@pytest.mark.parametrize("case", h2o_sto3g.cases)
- def test_doubles_h2o(self, method: str, case: str):
+ def test_doubles_h2o_pp(self, method: str, case: str):
guesses = { # fewer ocvv guesses available
"fv-cvs": 4, "fc-fv-cvs": 3
}
@@ -351,9 +697,41 @@ def test_doubles_h2o(self, method: str, case: str):
max_guesses=guesses.get(case, 5)
)
- @pytest.mark.parametrize("method", singles_methods)
+ @pytest.mark.parametrize("method", singles_methods_ip)
+ @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ip"))
+ def test_singles_h2o_ip(self, method: str, case: str):
+ self.base_test_ip(
+ "h2o_sto3g", case, method, block="h", is_alpha=True,
+ max_guesses=3
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ip)
+ @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ip"))
+ def test_doubles_h2o_ip(self, method: str, case: str):
+ self.base_test_ip(
+ "h2o_sto3g", case, method, block="phh", is_alpha=True,
+ max_guesses=5
+ )
+
+ @pytest.mark.parametrize("method", singles_methods_ea)
+ @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ea"))
+ def test_singles_h2o_ea(self, method: str, case: str):
+ self.base_test_ea(
+ "h2o_sto3g", case, method, block="p", is_alpha=True,
+ max_guesses=1
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ea)
+ @pytest.mark.parametrize("case", h2o_sto3g.filter_cases("ea"))
+ def test_doubles_h2o_ea(self, method: str, case: str):
+ self.base_test_ea(
+ "h2o_sto3g", case, method, block="pph", is_alpha=True,
+ max_guesses=4
+ )
+
+ @pytest.mark.parametrize("method", singles_methods_pp)
@pytest.mark.parametrize("case", cn_sto3g.cases)
- def test_singles_cn(self, method: str, case: str):
+ def test_singles_cn_pp(self, method: str, case: str):
guesses = { # fewer guesses available
"cvs": 7, "fc-cvs": 7, "fv-cvs": 5, "fc-fv-cvs": 5
}
@@ -362,34 +740,108 @@ def test_singles_cn(self, method: str, case: str):
max_guesses=guesses.get(case, 10)
)
- @pytest.mark.parametrize("method", doubles_methods)
+ @pytest.mark.parametrize("method", doubles_methods_pp)
@pytest.mark.parametrize("case", cn_sto3g.cases)
- def test_doubles_cn(self, method: str, case: str):
+ def test_doubles_cn_pp(self, method: str, case: str):
self.base_test_no_spin_change(
system="cn_sto3g", case=case, method=method, block="pphh",
max_guesses=5
)
- @pytest.mark.parametrize("method", singles_methods)
+ @pytest.mark.parametrize("method", singles_methods_ip)
+ @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ip"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_singles_cn_ip(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ip(
+ "cn_sto3g", case, method, block="h", is_alpha=is_alpha,
+ max_guesses=3
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ip)
+ @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ip"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_doubles_cn_ip(self, method: str, case: str, is_alpha: bool):
+ case="gen"
+ self.base_test_ip(
+ "cn_sto3g", case, method, block="phh", is_alpha=is_alpha,
+ max_guesses=5
+ )
+
+ @pytest.mark.parametrize("method", singles_methods_ea)
+ @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ea"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_singles_cn_ea(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ea(
+ "cn_sto3g", case, method, block="p", is_alpha=is_alpha,
+ max_guesses=1
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ea)
+ @pytest.mark.parametrize("case", cn_sto3g.filter_cases("ea"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_doubles_cn_ea(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ea(
+ "cn_sto3g", case, method, block="pph", is_alpha=is_alpha,
+ max_guesses=5
+ )
+
+ @pytest.mark.parametrize("method", singles_methods_pp)
@pytest.mark.parametrize("case", hf_631g.cases)
- def test_singles_hf(self, method: str, case: str):
+ def test_singles_hf_pp(self, method: str, case: str):
self.base_test_spin_flip(
system="hf_631g", case=case, method=method, block="ph",
max_guesses=10
)
- @pytest.mark.parametrize("method", doubles_methods)
+ @pytest.mark.parametrize("method", doubles_methods_pp)
@pytest.mark.parametrize("case", hf_631g.cases)
- def test_doubles_hf(self, method: str, case: str):
+ def test_doubles_hf_pp(self, method: str, case: str):
self.base_test_spin_flip(
system="hf_631g", case=case, method=method, block="pphh",
max_guesses=5
)
+ @pytest.mark.parametrize("method", singles_methods_ip)
+ @pytest.mark.parametrize("case", hf_631g.filter_cases("ip"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_singles_hf_ip(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ip(
+ "hf_631g", case, method, block="h", is_alpha=is_alpha,
+ max_guesses=3
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ip)
+ @pytest.mark.parametrize("case", hf_631g.filter_cases("ip"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_doubles_hf_ip(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ip(
+ "hf_631g", case, method, block="phh", is_alpha=is_alpha,
+ max_guesses=5
+ )
+
+ @pytest.mark.parametrize("method", singles_methods_ea)
+ @pytest.mark.parametrize("case", hf_631g.filter_cases("ea"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_singles_hf_ea(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ea(
+ "hf_631g", case, method, block="p", is_alpha=is_alpha,
+ max_guesses=1
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_ea)
+ @pytest.mark.parametrize("case", hf_631g.filter_cases("ea"))
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_doubles_hf_ea(self, method: str, case: str, is_alpha: bool):
+ self.base_test_ea(
+ "hf_631g", case, method, block="pph", is_alpha=is_alpha,
+ max_guesses=5
+ )
+
+
#
# Tests against reference values
#
- def base_reference(self, matrix, ref):
+ def base_reference_pp(self, matrix, ref):
symmetrisations = ["none"]
if matrix.reference_state.restricted:
symmetrisations = ["symmetric", "antisymmetric"]
@@ -411,15 +863,234 @@ def base_reference(self, matrix, ref):
nonzeros = np.dstack(np.where(guess_b != 0))
assert nonzeros.shape[0] == 1
nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]]
- values = guess_b[guess_b != 0]
- assert nonzeros == ref_sb[i][0]
- assert_array_equal(values, np.array(ref_sb[i][1]))
+ indices_sorted = tuple(sorted(nonzeros))
+ indices_ref_sorted = tuple(sorted(ref_sb[i][0]))
+ assert indices_sorted == indices_ref_sorted
+
+ def base_reference_degenerate_pp(self, matrix, ref):
+ """
+ Validate PP guesses in presence of orbital degeneracies.
+
+ Ensures that:
+ - The number of generated guesses matches the reference manifold size.
+ - Each guess belongs to the correct diagonal energy group.
+ - Ordering within degenerate subspaces is not enforced.
+ """
+ symmetrisations = ["none"]
+ if matrix.reference_state.restricted:
+ symmetrisations = ["symmetric", "antisymmetric"]
+
+ for block in ["ph", "pphh"]:
+ for symm in symmetrisations:
+ ref_sb = ref[(block, symm)]
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, len(ref_sb), block, spin_change=0,
+ spin_block_symmetrisation=symm
+ )
+ assert len(guesses) == len(ref_sb)
+ for gs in guesses:
+ self.assert_symmetry_no_spin_change(matrix, gs, block, symm)
+ self.assert_orthonormal(guesses)
+
+ # Collect diagonal energies of actual guesses
+ diag_block = matrix.diagonal()[block].to_ndarray()
+ actual_energies = []
+ for guess in guesses:
+ arr = guess[block].to_ndarray()
+ nonzeros = np.dstack(np.where(arr != 0))
+ assert nonzeros.shape[0] == 1
+ idx = tuple(nonzeros[0][0])
+ actual_energies.append(diag_block[idx])
+
+ # Collect diagonal energies of reference guesses
+ ref_energies = []
+ for ref_entry in ref_sb:
+ ref_indices = ref_entry[0]
+ values = [diag_block[idx] for idx in ref_indices]
+
+ # enforce internal degeneracy consistency
+ np.testing.assert_allclose(
+ values, [values[0]] * len(values),
+ rtol=1e-12, atol=1e-14
+ )
+
+ ref_energies.append(values[0])
+
+ # Compare as multisets
+ np.testing.assert_allclose(
+ sorted(actual_energies),
+ sorted(ref_energies),
+ rtol=1e-12,
+ atol=1e-14
+ )
+
+ def base_reference_ip(self, matrix, ref, is_alpha=True):
+ spin_change = -0.5 if is_alpha else +0.5
+ for block in ["h", "phh"]:
+ ref_sb = ref[(block, is_alpha)]
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, len(ref_sb), block=block, spin_change=spin_change
+ )
+ assert len(guesses) == len(ref_sb)
+
+ for gs in guesses:
+ self.assert_symmetry_ip(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+
+ for (i, guess) in enumerate(guesses):
+ guess_b = guess[block].to_ndarray()
+ nonzeros = np.dstack(np.where(guess_b != 0))
+ assert nonzeros.shape[0] == 1
+ nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]]
+ values = guess_b[guess_b != 0]
+ assert nonzeros == ref_sb[i][0]
+ assert_array_equal(values, np.array(ref_sb[i][1]))
+
+ def base_reference_degenerate_ip(self, matrix, ref, is_alpha=True):
+ """
+ Validate IP guesses in presence of orbital degeneracies.
+
+ Ensures that:
+ - The number of generated guesses matches the reference manifold size.
+ - Each guess belongs to the correct diagonal energy group.
+ - Ordering within degenerate subspaces is not enforced.
+ """
+ spin_change = -0.5 if is_alpha else +0.5
+ for block in ["h", "phh"]:
+ ref_sb = ref[(block, is_alpha)]
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, len(ref_sb), block=block, spin_change=spin_change
+ )
+ assert len(guesses) == len(ref_sb)
+
+ for gs in guesses:
+ self.assert_symmetry_ip(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+
+ # Collect diagonal energies of actual guesses
+ diag_block = matrix.diagonal()[block].to_ndarray()
+ actual_energies = []
+ for guess in guesses:
+ arr = guess[block].to_ndarray()
+ nonzeros = np.dstack(np.where(arr != 0))
+ assert nonzeros.shape[0] == 1
+ idx = tuple(nonzeros[0][0])
+ actual_energies.append(diag_block[idx])
+
+ # Collect diagonal energies of reference guesses
+ ref_energies = []
+ for ref_entry in ref_sb:
+ ref_indices = ref_entry[0]
+ values = [diag_block[idx] for idx in ref_indices]
+
+ # enforce internal degeneracy consistency
+ np.testing.assert_allclose(
+ values, [values[0]] * len(values),
+ rtol=1e-12, atol=1e-14
+ )
+
+ ref_energies.append(values[0])
- @pytest.mark.parametrize("method", doubles_methods)
- def test_reference_h2o(self, method: str):
+ # Compare as multisets
+ np.testing.assert_allclose(
+ sorted(actual_energies),
+ sorted(ref_energies),
+ rtol=1e-12,
+ atol=1e-14
+ )
+
+ def base_reference_ea(self, matrix, ref, is_alpha=True):
+ spin_change = +0.5 if is_alpha else -0.5
+ for block in ["p", "pph"]:
+ ref_sb = ref[(block, is_alpha)]
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, len(ref_sb), block=block, spin_change=spin_change
+ )
+ assert len(guesses) == len(ref_sb)
+
+ for gs in guesses:
+ self.assert_symmetry_ea(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+
+ for (i, guess) in enumerate(guesses):
+ guess_b = guess[block].to_ndarray()
+ nonzeros = np.dstack(np.where(guess_b != 0))
+ assert nonzeros.shape[0] == 1
+ nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]]
+ values = guess_b[guess_b != 0]
+ assert nonzeros == ref_sb[i][0]
+ assert_array_equal(values, np.array(ref_sb[i][1]))
+
+ def base_reference_degenerate_ea(self, matrix, ref, is_alpha=True):
+ """
+ Validate EA guesses in presence of orbital degeneracies.
+
+ Ensures that:
+ - The number of generated guesses matches the reference manifold size.
+ - Each guess belongs to the correct diagonal energy group.
+ - Ordering within degenerate subspaces is not enforced.
+ """
+ spin_change = +0.5 if is_alpha else -0.5
+ for block in ["p", "pph"]:
+ ref_sb = ref[(block, is_alpha)]
+ guesses = adcc.guess.guesses_from_diagonal(
+ matrix, len(ref_sb), block=block, spin_change=spin_change
+ )
+ assert len(guesses) == len(ref_sb)
+
+ for gs in guesses:
+ self.assert_symmetry_ea(matrix, gs, block, is_alpha)
+ self.assert_orthonormal(guesses)
+
+ # Collect diagonal energies of actual guesses
+ diag_block = matrix.diagonal()[block].to_ndarray()
+ actual_energies = []
+ for guess in guesses:
+ arr = guess[block].to_ndarray()
+ nonzeros = np.dstack(np.where(arr != 0))
+ assert nonzeros.shape[0] == 1
+ idx = tuple(nonzeros[0][0])
+ actual_energies.append(diag_block[idx])
+
+ # Collect diagonal energies of reference guesses
+ ref_energies = []
+ for ref_entry in ref_sb:
+ ref_indices = ref_entry[0]
+ values = [diag_block[idx] for idx in ref_indices]
+
+ # enforce internal degeneracy consistency
+ np.testing.assert_allclose(
+ values, [values[0]] * len(values),
+ rtol=1e-12, atol=1e-14
+ )
+
+ ref_energies.append(values[0])
+
+ # Compare as multisets
+ np.testing.assert_allclose(
+ sorted(actual_energies),
+ sorted(ref_energies),
+ rtol=1e-12,
+ atol=1e-14
+ )
+
+ @pytest.mark.parametrize("method", doubles_methods_pp)
+ def test_reference_h2o_pp(self, method: str):
hf = testdata_cache.refstate("h2o_sto3g", "gen")
matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
- self.base_reference(matrix=matrix, ref=self.get_ref_h2o())
+ self.base_reference_pp(matrix=matrix, ref=self.get_ref_h2o_pp())
+
+ @pytest.mark.parametrize("method", doubles_methods_ip)
+ def test_reference_h2o_ip(self, method: str):
+ hf = testdata_cache.refstate("h2o_sto3g", "gen")
+ matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
+ self.base_reference_ip(matrix=matrix, ref=self.get_ref_h2o_ip())
+
+ @pytest.mark.parametrize("method", doubles_methods_ea)
+ def test_reference_h2o_ea(self, method: str):
+ hf = testdata_cache.refstate("h2o_sto3g", "gen")
+ matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
+ self.base_reference_ea(matrix=matrix, ref=self.get_ref_h2o_ea())
# NOTE: This test is a bit weird: the order of the guesses is
# ill defined, because some orbitals are degenerate for cn sto3g:
@@ -431,13 +1102,44 @@ def test_reference_h2o(self, method: str):
# against hard coded reference data. The test against numpy above should be
# sufficient.
- # @pytest.mark.parametrize("method", doubles_methods)
- # def test_reference_cn(self, method: str):
- # hf = testdata_cache.refstate("cn_sto3g", case="gen")
- # matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
- # self.base_reference(matrix=matrix, ref=self.get_ref_cn())
+ # Current workaround: Compare guess energies rather than exact ordering for
+ # these cases by calling 'base_reference_degenerate_{adc_type}()'
+
+ @pytest.mark.parametrize("method", doubles_methods_pp)
+ def test_reference_cn_pp(self, method: str):
+ hf = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
+ if not method.endswith("adc2"):
+ # NOTE: doubles guesses for higher ADC levels are constructed
+ # from the ADC(2) zeroth-order diagonal.
+ # We enforce this here explicitly to avoid method-dependent
+ # degeneracy reordering.
+ matrix._diagonal = adcc.AdcMatrix(method="adc2", hf_or_mp=hf).diagonal()
+ self.base_reference_degenerate_pp(matrix=matrix, ref=self.get_ref_cn_pp())
+
+ @pytest.mark.parametrize("method", doubles_methods_ip)
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_reference_cn_ip(self, method: str, is_alpha: bool):
+ hf = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
+ if not method.endswith("adc2"):
+ matrix._diagonal = adcc.AdcMatrix(method="ip-adc2", hf_or_mp=hf).diagonal()
+ self.base_reference_degenerate_ip(
+ matrix=matrix, ref=self.get_ref_cn_ip(), is_alpha=is_alpha
+ )
- def get_ref_h2o(self):
+ @pytest.mark.parametrize("method", doubles_methods_ea)
+ @pytest.mark.parametrize("is_alpha", [True, False])
+ def test_reference_cn_ea(self, method: str, is_alpha: bool):
+ hf = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf)
+ if not method.endswith("adc2"):
+ matrix._diagonal = adcc.AdcMatrix(method="ea-adc2", hf_or_mp=hf).diagonal()
+ self.base_reference_degenerate_ea(
+ matrix=matrix, ref=self.get_ref_cn_ea(), is_alpha=is_alpha
+ )
+
+ def get_ref_h2o_pp(self):
sq8 = 1 / np.sqrt(8)
sq12 = 1 / np.sqrt(12)
sq48 = 1 / np.sqrt(48)
@@ -523,7 +1225,7 @@ def get_ref_h2o(self):
],
}
- def get_ref_cn(self):
+ def get_ref_cn_pp(self):
sq8 = 1 / np.sqrt(8)
return {
("ph", "none"): [
@@ -553,3 +1255,113 @@ def get_ref_cn(self):
[0.5, -0.5, -0.5, 0.5]),
],
}
+
+ def get_ref_h2o_ip(self):
+ sq6 = 1 / np.sqrt(6)
+ asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)]
+ return {
+ ("h", True): [
+ ([(4, )], [1]),
+ ([(3, )], [1]),
+ ([(2, )], [1]),
+ ([(1, )], [1]),
+ ([(0, )], [1])
+ ],
+ ("phh", True): [
+ ([(4, 9, 2), (9, 4, 2)], asymm),
+ ([(3, 4, 0), (3, 9, 2), (4, 3, 0),
+ (4, 8, 2), (8, 4, 2), (9, 3, 2)],
+ [-sq6, -sq6, sq6, -sq6, sq6, sq6]),
+ ([(3, 8, 2), (8, 3, 2)], asymm),
+ ([(4, 9, 3), (9, 4, 3)], asymm),
+ ([(3, 4, 1), (3, 9, 3), (4, 3, 1),
+ (4, 8, 3), (8, 4, 3), (9, 3, 3)],
+ [-sq6, -sq6, sq6, -sq6, sq6, sq6])
+ ],
+ }
+
+ def get_ref_h2o_ea(self):
+ sq6 = 1 / np.sqrt(6)
+ asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)]
+ return {
+ ("p", True): [
+ ([(0, )], [1]),
+ ([(1, )], [1])
+ ],
+ ("pph", True): [
+ ([(9, 0, 2), (9, 2, 0)], asymm),
+ ([(8, 0, 2), (8, 2, 0)], asymm),
+ ([(4, 0, 1), (4, 1, 0), (9, 0, 3),
+ (9, 1, 2), (9, 2, 1), (9, 3, 0)],
+ [-sq6, sq6, -sq6, -sq6, sq6, sq6]),
+ ([(3, 0, 1), (3, 1, 0), (8, 0, 3),
+ (8, 1, 2), (8, 2, 1), (8, 3, 0)],
+ [-sq6, sq6, -sq6, -sq6, sq6, sq6]),
+ ([(7, 0, 2), (7, 2, 0)], asymm)
+ ],
+ }
+
+ def get_ref_cn_ip(self):
+ asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)]
+ asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)]
+ return {
+ ("h", True): [
+ ([(6, )], [1]),
+ ([(4, )], [1]), # occ. 4 and 5 are degenerate
+ ([(5, )], [1]), # occ. 4 and 5 are degenerate
+ ([(3, )], [1]),
+ ([(2, )], [1])
+ ],
+ ("h", False): [
+ ([(11, )], [1]),
+ ([(12, )], [1]),
+ ([(10, )], [1]),
+ ([(9, )], [1]),
+ ([(8, )], [1])
+ ],
+ ("phh", True): [
+ ([(6, 11, 3), (11, 6, 3)], asymm1),
+ ([(6, 12, 3), (12, 6, 3)], asymm1),
+ ([(4, 11, 3), (11, 4, 3)], asymm),
+ ([(4, 12, 3), (12, 4, 3)], asymm),
+ ([(5, 11, 3), (11, 5, 3)], asymm1)
+ ],
+ ("phh", False): [
+ ([(11, 12, 3), (12, 11, 3)], asymm),
+ ([(10, 11, 3), (11, 10, 3)], asymm),
+ ([(10, 12, 3), (12, 10, 3)], asymm),
+ ([(6, 11, 0), (11, 6, 0)], asymm1),
+ ([(6, 12, 0), (12, 6, 0)], asymm1)
+ ],
+ }
+
+ def get_ref_cn_ea(self):
+ asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)]
+ asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)]
+ return {
+ ("p", True): [
+ ([(0, )], [1]),
+ ([(1, )], [1]),
+ ([(2, )], [1])
+ ],
+ ("p", False): [
+ ([(3, )], [1]),
+ ([(4, )], [1]),
+ ([(5, )], [1]),
+ ([(6, )], [1])
+ ],
+ ("pph", True): [
+ ([(11, 0, 3), (11, 3, 0)], asymm),
+ ([(12, 0, 3), (12, 3, 0)], asymm),
+ ([(11, 1, 3), (11, 3, 1)], asymm1),
+ ([(12, 1, 3), (12, 3, 1)], asymm1),
+ ([(10, 0, 3), (10, 3, 0)], asymm)
+ ],
+ ("pph", False): [
+ ([(6, 0, 3), (6, 3, 0)], asymm),
+ ([(6, 1, 3), (6, 3, 1)], asymm1),
+ ([(4, 0, 3), (4, 3, 0)], asymm), # occ. 4 and 5 are degenerate
+ ([(5, 0, 3), (5, 3, 0)], asymm), # occ. 4 and 5 are degenerate
+ ([(4, 1, 3), (4, 3, 1)], asymm1)
+ ],
+ }
\ No newline at end of file
diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py
index 14ffd2c84..5eca29a14 100644
--- a/adcc/tests/properties_test.py
+++ b/adcc/tests/properties_test.py
@@ -39,29 +39,48 @@
# -> independent of method, case (gen/cvs/fc/fv) and kind (singlet/triplet)
# Actually, the tests should also be independent of the systems, because
# we only load some already tested density and contract it with some operator.
-methods = ["adc2"]
+pp_methods = ["adc2"]
+ip_ea_methods = ["ip-adc2", "ea-adc2"]
generators = ["adcman", "adcc"]
test_cases = testcases.get_by_filename(
"h2o_sto3g", "h2o_def2tzvp", "cn_sto3g", "cn_ccpvdz", "hf_631g"
)
-cases = [(case.file_name, "gen", kind)
+cases_pp = [(case.file_name, m, "gen", kind)
for case in test_cases
- for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp]
-unrestricted_cases = [
- (case.file_name, "gen", kind)
+ for m in pp_methods
+ for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp
+]
+unrestricted_cases_pp = [
+ (case.file_name, m, "gen", kind, None)
for case in test_cases if not case.restricted
+ for m in pp_methods
for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp
]
+cases_ip_ea = [(case.file_name, m, "gen", kind, is_alpha)
+ for case in test_cases
+ for m in ip_ea_methods
+ for kind in ["doublet", "any"] if kind in getattr(
+ case.kinds, AdcMethod(m).adc_type)
+ for is_alpha in ([True] if case.restricted else [True, False])
+]
+unrestricted_cases_ip_ea = [
+ (case.file_name, m, "gen", kind, is_alpha)
+ for case in test_cases if not case.restricted
+ for m in ip_ea_methods
+ for kind in ["doublet", "any"] if kind in getattr(
+ case.kinds, AdcMethod(m).adc_type)
+ for is_alpha in [True, False]
+]
+
gauge_origins = ["origin", "mass_center", "charge_center"]
-@pytest.mark.parametrize("method", methods)
class TestProperties:
@pytest.mark.parametrize("generator", generators)
- @pytest.mark.parametrize("system,case,kind", cases)
- def test_transition_dipole_moments(self, system: str, case: str, kind: str,
- method: str, generator: str):
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
+ def test_transition_dipole_moments(self, system: str, method: str,
+ case: str, kind: str, generator: str):
if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
@@ -92,9 +111,9 @@ def test_transition_dipole_moments(self, system: str, case: str, kind: str,
assert_allclose_signfix(res_tdm, ref_tdm, atol=1e-5)
@pytest.mark.parametrize("generator", generators)
- @pytest.mark.parametrize("system,case,kind", cases)
- def test_oscillator_strengths(self, system: str, case: str, kind: str,
- method: str, generator: str):
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
+ def test_oscillator_strengths(self, system: str, method: str, case: str,
+ kind: str, generator: str):
if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
@@ -121,11 +140,36 @@ def test_oscillator_strengths(self, system: str, case: str, kind: str,
assert (
res_oscs[i] == pytest.approx(2. / 3. * ref_tdm_norm * refevals[i])
)
+
+ @pytest.mark.parametrize("generator", generators)
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea)
+ def test_pole_strengths(self, system: str, method: str, case: str,
+ kind: str, is_alpha: bool, generator: str):
+ if generator == "adcman":
+ if method.endswith("adc2x"):
+ pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x")
+ refdata = testdata_cache._load_data(
+ system=system, method=method, case=case, source=generator,
+ is_alpha=is_alpha
+ )[kind]
+ state = testdata_cache._make_mock_adc_state(
+ system=system, method=method, case=case, kind=kind,
+ source=generator, is_alpha=is_alpha
+ )
+
+ res_pols = state.pole_strength
+ refevals = refdata["eigenvalues"]
+ ref_pols = refdata["pole_strengths"]
+
+ n_ref = len(state.excitation_vector)
+ for i in range(n_ref):
+ assert state.excitation_energy[i] == refevals[i]
+ assert res_pols[i] == pytest.approx(ref_pols[i])
@pytest.mark.parametrize("generator", generators)
- @pytest.mark.parametrize("system,case,kind", cases)
- def test_state_dipole_moments(self, system: str, case: str, kind: str,
- method: str, generator: str):
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
+ def test_state_dipole_moments_pp(self, system: str, method: str, case: str,
+ kind: str, generator: str):
if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
@@ -139,16 +183,41 @@ def test_state_dipole_moments(self, system: str, case: str, kind: str,
res_dms = state.state_dipole_moment
n_ref = len(state.excitation_vector)
assert_allclose(res_dms, refdata["state_dipole_moments"][:n_ref], atol=1e-4)
+
+ @pytest.mark.parametrize("generator", generators)
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea)
+ def test_state_dipole_moments_ip_ea(self, system: str, method: str,
+ case: str, kind: str, is_alpha: bool,
+ generator: str):
+ if generator == "adcman":
+ if method.endswith("adc2x"):
+ pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x")
+ refdata = testdata_cache._load_data(
+ system=system, method=method, case=case, source=generator,
+ is_alpha=is_alpha
+ )[kind]
+ state = testdata_cache._make_mock_adc_state(
+ system=system, method=method, case=case, kind=kind,
+ source=generator, is_alpha=is_alpha
+ )
+
+ res_dms = state.state_dipole_moment
+ n_ref = len(state.excitation_vector)
+ assert_allclose(res_dms, refdata["state_dipole_moments"][:n_ref], atol=1e-4)
- @pytest.mark.parametrize("system,case,kind",
- [c for c in unrestricted_cases if "cvs" not in c[1]])
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha",
+ [c for c in (unrestricted_cases_pp +
+ unrestricted_cases_ip_ea)
+ if "cvs" not in c[1]])
def test_state_ssq(self, system: str, case: str, kind: str,
- method: str):
+ method: str, is_alpha: bool | None):
refdata = testdata_cache._load_data(
- system=system, method=method, case=case, source="adcc"
+ system=system, method=method, case=case, source="adcc",
+ is_alpha=is_alpha
)[kind]
state = testdata_cache._make_mock_adc_state(
- system=system, method=method, case=case, kind=kind, source="adcc"
+ system=system, method=method, case=case, kind=kind, source="adcc",
+ is_alpha=is_alpha
)
res_dms = state.state_ssq
@@ -157,11 +226,11 @@ def test_state_ssq(self, system: str, case: str, kind: str,
# CVS-ADC state2state tdm not implemented
@pytest.mark.parametrize("generator", generators)
- @pytest.mark.parametrize("system,case,kind", [c for c in cases
- if "cvs" not in c[1]])
- def test_state2state_transition_dipole_moments(self, system: str, case: str,
- kind: str, method: str,
- generator: str):
+ @pytest.mark.parametrize("system,method,case,kind", [c for c in cases_pp
+ if "cvs" not in c[2]])
+ def test_state2state_transition_dipole_moments_pp(
+ self, system: str, case: str, kind: str, method: str, generator: str):
+
refdata = testdata_cache._load_data(
system=system, method=method, case=case, source=generator
)[kind]
@@ -184,6 +253,39 @@ def test_state2state_transition_dipole_moments(self, system: str, case: str,
assert_allclose_signfix(state2state.transition_dipole_moment[ii],
fromi_ref[ii], atol=1e-4)
+ @pytest.mark.parametrize("generator", generators)
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea)
+ def test_state2state_transition_dipole_moments_ip_ea(
+ self, system: str, method: str, case: str, kind: str, is_alpha: bool,
+ generator: str):
+ if generator == "adcman":
+ if method.endswith("adc2x"):
+ pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x")
+ refdata = testdata_cache._load_data(
+ system=system, method=method, case=case, source=generator,
+ is_alpha=is_alpha
+ )[kind]
+ state = testdata_cache._make_mock_adc_state(
+ system=system, method=method, case=case, kind=kind,
+ source=generator, is_alpha=is_alpha
+ )
+
+ refevals = refdata["eigenvalues"]
+ if len(refevals) < 2:
+ pytest.skip("Less than two states available.")
+
+ state_to_state = refdata["state_to_state"]
+ for i in range(len(state.excitation_vector) - 1):
+ assert state.excitation_energy[i] == refevals[i]
+ fromi_ref = state_to_state[f"from_{i}"]["transition_dipole_moments"]
+
+ state2state = State2States(state, initial=i)
+ for ii, j in enumerate(range(i + 1, state.size)):
+ assert state.excitation_energy[j] == refevals[j]
+ assert_allclose_signfix(state2state.transition_dipole_moment[ii],
+ fromi_ref[ii], atol=1e-4)
+
+ @pytest.mark.parametrize("method", pp_methods)
@pytest.mark.parametrize("case", ["gen", "cvs"])
def test_magnetic_transition_dipole_moments_z_component(self, method: str,
case: str):
@@ -208,7 +310,7 @@ def test_magnetic_transition_dipole_moments_z_component(self, method: str,
assert tdm[2] < 1e-10
# Only adcc reference data available.
- @pytest.mark.parametrize("system,case,kind", cases)
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
def test_magnetic_transition_dipole_moments(self, system: str, case: str,
kind: str, method: str):
refdata = testdata_cache._load_data(
@@ -229,7 +331,7 @@ def test_magnetic_transition_dipole_moments(self, system: str, case: str,
)
# Only adcc reference data available.
- @pytest.mark.parametrize("system,case,kind", cases)
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
def test_transition_dipole_moments_velocity(self, system: str, case: str,
kind: str, method: str):
refdata = testdata_cache._load_data(
@@ -248,7 +350,7 @@ def test_transition_dipole_moments_velocity(self, system: str, case: str,
)
# Only adcc reference data available.
- @pytest.mark.parametrize("system,case,kind", cases)
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
def test_transition_quadrupole_moments(self, system: str, case: str,
kind: str, method: str):
refdata = testdata_cache._load_data(
@@ -269,7 +371,7 @@ def test_transition_quadrupole_moments(self, system: str, case: str,
)
# Only adcc reference data available.
- @pytest.mark.parametrize("system,case,kind", cases)
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
def test_rotatory_strengths(self, system: str, case: str, kind: str,
method: str):
refdata = testdata_cache._load_data(
diff --git a/adcc/tests/state_densities_test.py b/adcc/tests/state_densities_test.py
index b2e8954d5..ca7eead42 100644
--- a/adcc/tests/state_densities_test.py
+++ b/adcc/tests/state_densities_test.py
@@ -25,13 +25,17 @@
from pytest import approx
from adcc import ExcitedStates, AdcMethod
+from adcc.ChargedExcitations import DetachedStates, AttachedStates
from adcc.State2States import State2States
from .testdata_cache import testdata_cache
from . import testcases
-methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
+pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
+# No need to test IP/EA-ADC(1) since it is equivalent to IP/EA-ADC(0)
+ip_ea_methods = [
+ t + m for t in ["ip-", "ea-"] for m in ["adc0", "adc2", "adc2x", "adc3"]]
generators = ["adcman", "adcc"]
@@ -42,28 +46,77 @@
test_cases = testcases.get_by_filename(
"h2o_sto3g", "h2o_def2tzvp", "cn_sto3g", "cn_ccpvdz", "hf_631g"
)
-cases = [(case.file_name, c, kind)
- for case in test_cases
- for c in ["gen", "cvs"] if c in case.cases
- for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp]
+cases_pp = [(case.file_name, m, c, kind)
+ for case in test_cases
+ for m in pp_methods
+ for c in ["gen", "cvs"] if c in case.cases
+ for kind in ["singlet", "any", "spin_flip"] if kind in case.kinds.pp
+]
+
+cases_ip_ea = [(case.file_name, m, c, kind, is_alpha)
+ for case in test_cases
+ for m in ip_ea_methods
+ for c in ["gen"] if c in case.cases
+ for kind in getattr(case.kinds, AdcMethod(m).adc_type)
+ for is_alpha in ([True] if case.restricted else [True, False])
+]
-@pytest.mark.parametrize("method", methods)
@pytest.mark.parametrize("generator", generators)
class TestStateDensities:
- @pytest.mark.parametrize("system,case,kind", cases)
- def test_state_diffdm(self, system: str, case: str, kind: str, method: str,
- generator: str):
+ @pytest.mark.parametrize("system,method,case,kind", cases_pp)
+ def test_state_diffdm_pp(self, system: str, method: str, case: str,
+ kind: str, generator: str):
if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
refdata = testdata_cache._load_data(
system=system, method=method, case=case, source=generator
)[kind]
- # construct a ExcitedStates instance using the eigenvalues and eigenstates
- # from the reference data.
- state: ExcitedStates = getattr(testdata_cache, f"{generator}_states")(
+ # construct an ExcitedStates instance
+ # using the eigenvalues and eigenstates from the reference data.
+ state: ExcitedStates = getattr(
+ testdata_cache, f"{generator}_states")(
system=system, method=method, case=case, kind=kind
- )
+ )
+
+ # since refdata was used to build state we have to have the same amount
+ # of states
+ for i in range(len(state.excitation_vector)):
+ # Check that we are talking about the same state when
+ # comparing reference and computed
+ assert state.excitation_energy[i] == refdata["eigenvalues"][i]
+
+ dm_ao_a, dm_ao_b = state.state_diffdm[i].to_ao_basis()
+ assert dm_ao_a.to_ndarray() == approx(refdata["state_diffdm_bb_a"][i])
+ assert dm_ao_b.to_ndarray() == approx(refdata["state_diffdm_bb_b"][i])
+
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea)
+ def test_state_diffdm_ip_ea(self, system: str, method: str, case: str,
+ kind: str, is_alpha: bool, generator: str):
+ if generator == "adcman":
+ if method.endswith("adc2x"):
+ pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x")
+ refdata = testdata_cache._load_data(
+ system=system, method=method, case=case, source=generator,
+ is_alpha=is_alpha
+ )[kind]
+ # construct an DetachedStates/AttachedStates instance
+ # using the eigenvalues and eigenstates from the reference data.
+ if AdcMethod(method).adc_type == "ip":
+ state: DetachedStates = getattr(
+ testdata_cache, f"{generator}_states")(
+ system=system, method=method, case=case, kind=kind,
+ is_alpha=is_alpha
+ )
+ elif AdcMethod(method).adc_type == "ea":
+ state: AttachedStates = getattr(
+ testdata_cache, f"{generator}_states")(
+ system=system, method=method, case=case, kind=kind,
+ is_alpha=is_alpha
+ )
+ else:
+ raise ValueError(f"Unknown ADC method: {method.name}")
+
# since refdata was used to build state we have to have the same amount
# of states
for i in range(len(state.excitation_vector)):
@@ -78,10 +131,10 @@ def test_state_diffdm(self, system: str, case: str, kind: str, method: str,
# adcman does not compute the tdm for singlet -> triplet transitions,
# because the transition dipole moment should be zero anyway
# -> remove triplet tests
- @pytest.mark.parametrize("system,case,kind",
- [c for c in cases if c[2] != "triplet"])
- def test_ground_to_excited_tdm(self, system: str, case: str, kind: str,
- method: str, generator: str):
+ @pytest.mark.parametrize("system,method,case,kind",
+ [c for c in cases_pp if c[3] != "triplet"])
+ def test_ground_to_excited_tdm(self, system: str, method: str, case: str,
+ kind: str, generator: str):
if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman":
pytest.skip("No CVS-ADC(0) adcman reference data available.")
refdata = testdata_cache._load_data(
@@ -107,11 +160,10 @@ def test_ground_to_excited_tdm(self, system: str, case: str, kind: str,
assert (dm_ao_b == approx(ref_dm_b))
# CVS state-to-state TDM is not implemented in adcc
- @pytest.mark.parametrize("system,case,kind",
- [c for c in cases if "cvs" not in c[1]])
- def test_state_to_state_tdm(self, system: str, case: str, kind: str,
- method: str, generator: str):
-
+ @pytest.mark.parametrize("system,method,case,kind",
+ [c for c in cases_pp if "cvs" not in c[2]])
+ def test_state_to_state_tdm_pp(self, system: str, method: str, case: str,
+ kind: str, generator: str):
refdata = testdata_cache._load_data(
system=system, method=method, case=case, source=generator
)[kind]
@@ -142,3 +194,54 @@ def test_state_to_state_tdm(self, system: str, case: str, kind: str,
dm_ao_a, dm_ao_b = state_to_state.transition_dm[j].to_ao_basis()
np.testing.assert_allclose(dm_ao_a.to_ndarray(), ref_a, atol=1e-4)
np.testing.assert_allclose(dm_ao_b.to_ndarray(), ref_b, atol=1e-4)
+
+ @pytest.mark.parametrize("system,method,case,kind,is_alpha", cases_ip_ea)
+ def test_state_to_state_tdm_ip_ea(self, system: str, method: str, case: str,
+ kind: str, is_alpha: bool, generator: str):
+ if generator == "adcman":
+ if method.endswith("adc2x"):
+ pytest.skip("No adcman reference data yet for IP/EA-ADC(2)-x")
+
+ refdata = testdata_cache._load_data(
+ system=system, method=method, case=case, source=generator,
+ is_alpha=is_alpha
+ )[kind]
+ if len(refdata["eigenvalues"]) < 2:
+ pytest.skip("Less than two states available.")
+ s2s_data = refdata["state_to_state"]
+
+ # construct a ExcitedStates instance using the eigenvalues and eigenstates
+ # from the reference data.
+ if AdcMethod(method).adc_type == "ip":
+ state: DetachedStates = getattr(
+ testdata_cache, f"{generator}_states")(
+ system=system, method=method, case=case, kind=kind,
+ is_alpha=is_alpha
+ )
+ elif AdcMethod(method).adc_type == "ea":
+ state: AttachedStates = getattr(
+ testdata_cache, f"{generator}_states")(
+ system=system, method=method, case=case, kind=kind,
+ is_alpha=is_alpha
+ )
+ else:
+ raise ValueError(f"Unknown ADC method: {method.name}")
+
+ # since refdata was used to build state we have to have the same amount
+ # of states
+ for i in range(len(state.excitation_vector) - 1):
+ # Check that we are talking about the same state when
+ # comparing reference and computed
+ assert state.excitation_energy[i] == refdata["eigenvalues"][i]
+ fromi_ref_a = s2s_data[f"from_{i}"]["state_to_excited_tdm_bb_a"]
+ fromi_ref_b = s2s_data[f"from_{i}"]["state_to_excited_tdm_bb_b"]
+
+ state_to_state = State2States(state, initial=i)
+ for j, (ref_a, ref_b) in enumerate(zip(fromi_ref_a, fromi_ref_b)):
+ ito = i + j + 1
+ assert state.excitation_energy[ito] == refdata["eigenvalues"][ito]
+ ref_energy = refdata["eigenvalues"][ito] - refdata["eigenvalues"][i]
+ assert state_to_state.excitation_energy[j] == ref_energy
+ dm_ao_a, dm_ao_b = state_to_state.transition_dm[j].to_ao_basis()
+ np.testing.assert_allclose(dm_ao_a.to_ndarray(), ref_a, atol=1e-4)
+ np.testing.assert_allclose(dm_ao_b.to_ndarray(), ref_b, atol=1e-4)
diff --git a/adcc/tests/testcases.py b/adcc/tests/testcases.py
index 4daeb8b1f..e834f2ad6 100644
--- a/adcc/tests/testcases.py
+++ b/adcc/tests/testcases.py
@@ -92,6 +92,8 @@ def filter_cases(self, adc_type: str) -> tuple[str, ...]:
# since cvs is not (yet) implemented for IP.
if adc_type == "pp":
return self.cases
+ elif adc_type in ("ip", "ea"):
+ return tuple(case for case in self.cases if "cvs" not in case)
raise NotImplementedError(f"Filtering for adc type {adc_type} not "
"implemented.")
@@ -110,14 +112,26 @@ def validate(self):
continue
assert component in requirements
assert getattr(self, requirements[component], None) is not None
- # validate the PP-ADC kinds
+ # validate the IP/EA/PP-ADC kinds
assert len(fields(self.kinds)) == 3
- assert not self.kinds.ip
- assert not self.kinds.ea
- if self.restricted:
- assert all(kind in ["singlet", "triplet"] for kind in self.kinds.pp)
- else:
- assert all(kind in ["any", "spin_flip"] for kind in self.kinds.pp)
+
+ if self.kinds.pp:
+ if self.restricted:
+ assert all(kind in ["singlet", "triplet"]
+ for kind in self.kinds.pp)
+ else:
+ assert all(kind in ["any", "spin_flip"]
+ for kind in self.kinds.pp)
+ if self.kinds.ip:
+ if self.restricted:
+ assert all(kind in ["doublet"] for kind in self.kinds.ip)
+ else:
+ assert all(kind in ["any"] for kind in self.kinds.ip)
+ if self.kinds.ea:
+ if self.restricted:
+ assert all(kind in ["doublet"] for kind in self.kinds.ea)
+ else:
+ assert all(kind in ["any"] for kind in self.kinds.ea)
def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]:
@@ -125,9 +139,9 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]:
Transforms the given kinds to a list of keywords to request states of the
corresponding kind in an adc calculation.
"""
- # singlet, triplet -> n_singlets, n_triplets
- # any -> n_states
- # spin_flip -> n_spin_flip
+ # singlet, doublet, triplet -> n_singlets, n_triplets
+ # any -> n_states
+ # spin_flip -> n_spin_flip
ret = []
for kind in kinds:
if kind == "any":
@@ -197,8 +211,10 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]:
def _init_test_cases() -> tuple[TestCase, ...]:
test_cases: list[TestCase] = []
# some shared data
- restricted_kinds = Kinds(pp=("singlet", "triplet"))
- unrestricted_kinds = Kinds(pp=("any",))
+ restricted_kinds = Kinds(pp=("singlet", "triplet"),
+ ip=("doublet",),
+ ea=("doublet",))
+ unrestricted_kinds = Kinds(pp=("any",), ip=("any",), ea=("any",))
spin_flip_kinds = Kinds(pp=("spin_flip",))
# CH2NH2
ref_cases = ("gen", "cvs")
diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py
index a1effde56..a4679b2a4 100644
--- a/adcc/tests/testdata_cache.py
+++ b/adcc/tests/testdata_cache.py
@@ -1,12 +1,15 @@
from . import testcases
from adcc.AdcMatrix import AdcMatrix
+from adcc.AdcMethod import AdcMethod
from adcc.ExcitedStates import ExcitedStates
+from adcc.ChargedExcitations import AttachedStates, DetachedStates
from adcc.LazyMp import LazyMp
from adcc.misc import cached_member_function
from adcc.ReferenceState import ReferenceState
from adcc.solver import EigenSolverStateBase
-from adcc import hdf5io, guess_zero
+from adcc import hdf5io
+from adcc.guess import guess_zero, determine_spin_change
from pathlib import Path
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,73 @@ 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 ("ip", "ea"):
+ assert isinstance(is_alpha, bool)
+ spin = "alpha" if is_alpha else "beta"
+ key = f"{case}/{gs_density_order}/{spin}"
if not datafile.exists():
raise FileNotFoundError(f"Missing reference data file {datafile}.")
with h5py.File(datafile, "r") as hdf5_file:
if key not in hdf5_file:
- raise ValueError(
- f"No data available for case {case} and gs_density_order "
- f"{gs_density_order} in file {datafile}."
- )
+ if is_alpha is None:
+ raise ValueError(
+ f"No data available for case {case} and "
+ f"gs_density_order {gs_density_order} in file {datafile}."
+ )
+ 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:
+ 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,
+ 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 +226,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 +234,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 +262,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)
@@ -257,34 +283,49 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase],
evec[blocks[1]].set_from_ndarray(
adc_data["eigenvectors_doubles"][i], 1e-14
)
- return ExcitedStates(states)
+
+ if matrix.method.adc_type == "pp":
+ return ExcitedStates(states)
+ elif matrix.method.adc_type == "ip":
+ return DetachedStates(states, is_alpha)
+ elif matrix.method.adc_type == "ea":
+ return AttachedStates(states, is_alpha)
+ else:
+ raise ValueError(f"Unknown ADC method: {method.name}")
def adcc_states(self, system: str, method: str, kind: str,
- case: str, gs_density_order: 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
)
@@ -299,7 +340,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 6dbc8e6d2..169a5abef 100644
--- a/adcc/tests/workflow_test.py
+++ b/adcc/tests/workflow_test.py
@@ -29,18 +29,26 @@
class TestWorkflow:
- def test_validate_state_parameters_rhf(self):
+ def test_validate_state_parameters_rhf_pp(self):
from adcc.workflow import validate_state_parameters
-
- refstate = testdata_cache.refstate("h2o_sto3g", case="gen")
-
- assert 3, "any" == validate_state_parameters(refstate, n_states=3)
- assert 4, "singlet" == validate_state_parameters(refstate, n_states=4,
- kind="singlet")
- assert 2, "triplet" == validate_state_parameters(refstate, n_states=2,
- kind="triplet")
- assert 2, "triplet" == validate_state_parameters(refstate, n_triplets=2)
- assert 6, "singlet" == validate_state_parameters(refstate, n_singlets=6)
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
+
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen")
+ matrix.method = AdcMethod("adc2")
+
+ assert (3, "any", None) == validate_state_parameters(
+ matrix, n_states=3)
+ assert (4, "singlet", None) == validate_state_parameters(
+ matrix, n_states=4, kind="singlet")
+ assert (2, "triplet", None) == validate_state_parameters(
+ matrix, n_states=2, kind="triplet")
+ assert (2, "triplet", None) == validate_state_parameters(
+ matrix, n_triplets=2)
+ assert (6, "singlet", None) == validate_state_parameters(
+ matrix, n_singlets=6)
invalid_cases = [
dict(), # No states requested
@@ -52,21 +60,30 @@ def test_validate_state_parameters_rhf(self):
dict(n_states=2, n_spin_flip=2), # States of two sorts
dict(n_triplets=2, kind="singlet"), # kind and n_ do not agree
dict(n_states=2, kind="bla"), # Kind invaled
+ dict(n_states=2, kind="doublet"), # Kind invalid for PP-ADC
+ dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC
+ dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC
]
for case in invalid_cases:
with pytest.raises(InputError):
- validate_state_parameters(refstate, **case)
+ validate_state_parameters(matrix, **case)
- def test_validate_state_parameters_uhf(self):
+ def test_validate_state_parameters_uhf_pp(self):
from adcc.workflow import validate_state_parameters
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
- refstate = testdata_cache.refstate("cn_sto3g", case="gen")
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix.method = AdcMethod("adc2")
- assert 3, "any" == validate_state_parameters(refstate, n_states=3,
- kind="any")
- assert 3, "any" == validate_state_parameters(refstate, n_states=3)
- assert 2, "spin_flip" == validate_state_parameters(refstate,
- n_spin_flip=2)
+ assert (3, "any", None) == validate_state_parameters(
+ matrix, n_states=3, kind="any")
+ assert (3, "any", None) == validate_state_parameters(
+ matrix, n_states=3)
+ assert (2, "spin_flip", None) == validate_state_parameters(
+ matrix, n_spin_flip=2)
invalid_cases = [
dict(), # No states requested
@@ -77,15 +94,170 @@ def test_validate_state_parameters_uhf(self):
dict(n_triplets=2, n_singlets=2), # States of two sorts
dict(n_states=2, n_spin_flip=2), # States of two sorts
dict(n_spin_flip=2, kind="singlet"), # kind and n_ do not agree
- dict(n_states=2, kind="bla"), # Kind invaled
+ dict(n_states=2, kind="bla"), # Kind invalid
dict(n_states=4, kind="singlet"), # UHF with singlets
dict(n_states=2, kind="triplet"), # UHF with triplets
dict(n_triplets=2), # UHF with triplets
dict(n_singlets=6), # UHF with singlets
+ dict(n_doublets=3), # UHF with doublets (only restricted IP/EA)
+ dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC
+ dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC
+ ]
+ for case in invalid_cases:
+ with pytest.raises(InputError):
+ validate_state_parameters(matrix, **case)
+
+ def test_validate_state_parameters_rhf_ip(self):
+ from adcc.workflow import validate_state_parameters
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
+
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen")
+ matrix.method = AdcMethod("ip-adc2")
+
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3))
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3, is_alpha=False))
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3, is_alpha=True)) # restricted always beta
+ assert (2, "doublet", True) == (validate_state_parameters(
+ matrix, n_states=2, kind="doublet"))
+ assert (2, "doublet", True) == (validate_state_parameters(
+ matrix, n_doublets=2))
+ assert (6, "doublet", True) == (validate_state_parameters(
+ matrix, n_doublets=6, is_alpha=True))
+
+ invalid_cases = [
+ dict(), # No states requested
+ dict(n_states=0), # No states requested
+ dict(n_doublets=-2), # Negative number of states requested
+ dict(n_states=2, kind="bla"), # Kind invalid
+ dict(n_singlets=2), # Kind invalid for IP/EA-ADC
+ dict(n_triplets=2), # Kind invalid for IP/EA-ADC
+ dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC
+ dict(n_states=2, is_alpha="yes"), # is_alpha not boolean
+ dict(n_states=2, is_alpha=1), # is_alpha not boolean
+ dict(n_states=2, n_spin_flip=2), # States of two sorts
+ dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree
+ ]
+
+ for case in invalid_cases:
+ with pytest.raises(InputError):
+ validate_state_parameters(matrix, **case)
+
+ def test_validate_state_parameters_uhf_ip(self):
+ from adcc.workflow import validate_state_parameters
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
+
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix.method = AdcMethod("ip-adc2")
+
+ assert (3, "any", True) == validate_state_parameters(
+ matrix, n_states=3, kind="any")
+ assert (3, "any", False) == validate_state_parameters(
+ matrix, n_states=3, is_alpha=False)
+ assert (3, "any", True) == validate_state_parameters(
+ matrix, n_states=3, is_alpha=True)
+
+ invalid_cases = [
+ dict(), # No states requested
+ dict(n_states=0), # No states requested
+ dict(n_states=-2), # Negative number of states requested
+ dict(n_states=2, kind="bla"), # Kind invalid
+ dict(n_doublets=2), # UHF with doublets
+ dict(n_states=2, kind="doublet"), # UHF with doublets
+ dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_states=2, is_alpha="yes"), # is_alpha not boolean
+ dict(n_states=2, is_alpha=1), # is_alpha not boolean
+ ]
+ for case in invalid_cases:
+ with pytest.raises(InputError):
+ validate_state_parameters(matrix, **case)
+
+ def test_validate_state_parameters_rhf_ea(self):
+ from adcc.workflow import validate_state_parameters
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
+
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen")
+
+ # IP
+ matrix.method = AdcMethod("ea-adc2")
+
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3))
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3, is_alpha=False))
+ assert (3, "any", True) == (validate_state_parameters(
+ matrix, n_states=3, is_alpha=True)) # restricted always beta
+ assert (2, "doublet", True) == (validate_state_parameters(
+ matrix, n_states=2, kind="doublet"))
+ assert (2, "doublet", True) == (validate_state_parameters(
+ matrix, n_doublets=2))
+ assert (6, "doublet", True) == (validate_state_parameters(
+ matrix, n_doublets=6, is_alpha=True))
+
+ invalid_cases = [
+ dict(), # No states requested
+ dict(n_states=0), # No states requested
+ dict(n_doublets=-2), # Negative number of states requested
+ dict(n_states=2, kind="bla"), # Kind invalid
+ dict(n_singlets=2), # Kind invalid for IP/EA-ADC
+ dict(n_triplets=2), # Kind invalid for IP/EA-ADC
+ dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC
+ dict(n_states=2, is_alpha="yes"), # is_alpha not boolean
+ dict(n_states=2, is_alpha=1), # is_alpha not boolean
+ dict(n_states=2, n_spin_flip=2), # States of two sorts
+ dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree
+ ]
+
+ for case in invalid_cases:
+ with pytest.raises(InputError):
+ validate_state_parameters(matrix, **case)
+
+ def test_validate_state_parameters_uhf_ea(self):
+ from adcc.workflow import validate_state_parameters
+ from adcc.AdcMatrix import AdcMatrixlike
+ from adcc.AdcMethod import AdcMethod
+
+ # Build empty AdcMatrixlike object and assign ref_state and method
+ matrix = AdcMatrixlike()
+ matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen")
+ matrix.method = AdcMethod("ea-adc2")
+
+ assert (3, "any", True) == validate_state_parameters(
+ matrix, n_states=3, kind="any")
+ assert (3, "any", False) == validate_state_parameters(
+ matrix, n_states=3, is_alpha=False)
+ assert (3, "any", True) == validate_state_parameters(
+ matrix, n_states=3, is_alpha=True)
+
+ invalid_cases = [
+ dict(), # No states requested
+ dict(n_states=0), # No states requested
+ dict(n_states=-2), # Negative number of states requested
+ dict(n_states=2, kind="bla"), # Kind invalid
+ dict(n_doublets=2), # UHF with doublets
+ dict(n_states=2, kind="doublet"), # UHF with doublets
+ dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF
+ dict(n_states=2, is_alpha="yes"), # is_alpha not boolean
+ dict(n_states=2, is_alpha=1), # is_alpha not boolean
]
for case in invalid_cases:
with pytest.raises(InputError):
- validate_state_parameters(refstate, **case)
+ validate_state_parameters(matrix, **case)
def test_construct_adcmatrix(self):
from adcc.workflow import construct_adcmatrix
@@ -98,6 +270,8 @@ def test_construct_adcmatrix(self):
res = construct_adcmatrix(hfdata, method="adc3")
assert isinstance(res, adcc.AdcMatrix)
assert res.method == adcc.AdcMethod("adc3")
+ assert res.method.adc_type == "pp"
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == []
assert res.mospaces.frozen_core == []
assert res.mospaces.frozen_virtual == []
@@ -105,26 +279,52 @@ def test_construct_adcmatrix(self):
res = construct_adcmatrix(hfdata, method="cvs-adc3", core_orbitals=1)
assert isinstance(res, adcc.AdcMatrix)
assert res.method == adcc.AdcMethod("cvs-adc3")
+ assert res.method.adc_type == "pp"
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == [0, 7]
assert res.mospaces.frozen_core == []
assert res.mospaces.frozen_virtual == []
res = construct_adcmatrix(hfdata, method="adc2", frozen_core=1)
+ assert res.method.adc_type == "pp"
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == []
assert res.mospaces.frozen_core == [0, 7]
assert res.mospaces.frozen_virtual == []
res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1)
+ assert res.method.adc_type == "pp"
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == []
assert res.mospaces.frozen_core == []
assert res.mospaces.frozen_virtual == [6, 13]
res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1,
frozen_core=1)
+ assert res.method.adc_type == "pp"
+ assert res.axis_blocks == ["ph", "pphh"]
assert res.mospaces.core_orbitals == []
assert res.mospaces.frozen_core == [0, 7]
assert res.mospaces.frozen_virtual == [6, 13]
+ res = construct_adcmatrix(hfdata, method="ip-adc3")
+ assert isinstance(res, adcc.AdcMatrix)
+ assert res.method == adcc.AdcMethod("ip-adc3")
+ assert res.method.adc_type == "ip"
+ assert res.axis_blocks == ["h", "phh"]
+ assert res.mospaces.core_orbitals == []
+ assert res.mospaces.frozen_core == []
+ assert res.mospaces.frozen_virtual == []
+
+ res = construct_adcmatrix(hfdata, method="ea-adc2")
+ assert isinstance(res, adcc.AdcMatrix)
+ assert res.method == adcc.AdcMethod("ea-adc2")
+ assert res.method.adc_type == "ea"
+ assert res.axis_blocks == ["p", "pph"]
+ assert res.mospaces.core_orbitals == []
+ assert res.mospaces.frozen_core == []
+ assert res.mospaces.frozen_virtual == []
+
invalid_cases = [
dict(), # Missing method
dict(method="dadadad"), # Unknown method
@@ -195,7 +395,7 @@ def test_construct_adcmatrix(self):
match=r"^Ignored frozen_virtual parameter"):
construct_adcmatrix(mtx_cvs, frozen_virtual=1)
- def test_diagonalise_adcmatrix(self):
+ def test_diagonalise_adcmatrix_pp(self):
from adcc.workflow import diagonalise_adcmatrix
system = "h2o_sto3g"
@@ -209,11 +409,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 +417,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))
- def test_obtain_guesses_by_inspection(self):
+ 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)
+
+ 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 +521,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
\ No newline at end of file
diff --git a/adcc/workflow.py b/adcc/workflow.py
index a9d27bf63..76aab684b 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 .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,81 @@ 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("Less guesses provided via guesses (== {}) "
+ "than states to be computed (== {})"
+ "".format(len(guesses), n_states))
+ if n_guesses is not None:
+ warnings.warn("Ignoring n_guesses parameter, since guesses are "
+ "explicitly provided.")
+ if n_guesses_doubles is not None:
+ warnings.warn("Ignoring n_guesses_doubles parameter, since guesses"
+ " are explicitly provided.")
+ # Set spin_change to None since we don't know if guesses are provided
+ spin_change = None
+
+
diagres = diagonalise_adcmatrix(
- matrix, n_states, 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 == "pp":
+ exstates = ExcitedStates(diagres)
+ elif matrix.method.adc_type == "ip":
+ exstates = DetachedStates(diagres, is_alpha)
+ elif matrix.method.adc_type == "ea":
+ exstates = AttachedStates(diagres, is_alpha)
+ else:
+ raise ValueError(f"Unknown ADC method: {matrix.method.name}")
+
exstates.kind = kind
exstates.spin_change = spin_change
@@ -255,20 +310,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 +340,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 +367,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 +396,139 @@ def validate_state_parameters(reference_state, n_states=None, n_singlets=None,
kind = "spin_flip"
n_states = n_spin_flip
+ # Check for IP- and EA-ADC parameter is_alpha
+ if adc_type == "pp":
+ if is_alpha is not None:
+ raise InputError("is_alpha may only be set for IP- and EA-ADC "
+ "calculations")
+ else:
+ if not isinstance(is_alpha, bool) and is_alpha is not None:
+ raise InputError("is_alpha has to be a Boolean or None.")
+ if is_alpha is None or reference_state.restricted:
+ # Per default set to True and for restricted references, only alpha
+ # states will be computed (beta states are identical)
+ is_alpha = True
+
# Check if there are states to be computed
if n_states is None or n_states == 0:
raise InputError("No excited states to be computed. Specify at least "
- "one of n_states, n_singlets, n_triplets, "
- "or n_spin_flip")
+ "one of n_states, n_singlets, n_doublets, "
+ "n_triplets, or n_spin_flip.")
if n_states < 0:
raise InputError("n_states needs to be positive")
- if kind not in ["any", "spin_flip", "singlet", "triplet"]:
+ if kind not in ["any", "spin_flip", "singlet", "doublet", "triplet"]:
raise InputError("The kind parameter may only take the values 'any', "
- "'singlet', 'triplet' or 'spin_flip'")
- if kind in ["singlet", "triplet"] and not reference_state.restricted:
- raise InputError("kind==singlet and kind==triplet are only valid for "
- "ADC calculations in combination with a restricted "
- "ground state.")
+ "'singlet', 'doublet', 'triplet' or 'spin_flip'")
+ if (kind in ["singlet", "doublet", "triplet"]
+ and not reference_state.restricted):
+ raise InputError("kind==singlet, kind==doublet and kind==triplet are "
+ "only valid for ADC calculations in combination with "
+ "a restricted ground state.")
if kind in ["spin_flip"] and reference_state.restricted:
raise InputError("kind==spin_flip is only valid for "
- "ADC calculations in combination with an unrestricted "
+ "ADC calculations in combination with an unrestricted"
+ " ground state.")
+ if kind in ["spin_flip", "singlet", "triplet"] and adc_type != "pp":
+ raise InputError("kind==singlet, kind==triplet, and kind==spin_flip "
+ "are only valid for PP-ADC calculations.")
+ if kind == "doublet" and adc_type == "pp":
+ raise InputError("kind==doublet is only valid for IP/EA-ADC "
+ "calculations in combination with a restricted "
"ground state.")
- return n_states, kind
+ return n_states, kind, is_alpha
+
+
+def 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)
+
+ # 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 < 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)
-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):
+ if len(guesses) < n_guesses:
+ raise InputError("Less guesses found than requested: {} found, "
+ "{} requested".format(
+ len(guesses), n_guesses))
+ return guesses
+
+
+def construct_guesses(
+ matrix: AdcMatrix,
+ n_states: int,
+ kind: str,
+ spin_change: float,
+ n_guesses: int,
+ n_guesses_doubles: Optional[int] = None,
+ is_alpha: Optional[bool] = None,
+ eigensolver: Optional[str] = "davidson"
+ ) -> list[AmplitudeVector]:
+ """
+ This function constructs appropriate guesses if not given.
+ Returns a :class:`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 +544,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 +600,10 @@ def setup_environment(matrix, environment):
Setup environment matrix terms and/or energy corrections.
Internal function called from run_adc.
"""
+ if environment and matrix.method.adc_type != "pp":
+ raise NotImplementedError("Environment for IP- and EA-ADC calculations"
+ " not implemented.")
+
valid_envs = ["ptss", "ptlr", "linear_response"]
hf = matrix.reference_state
if hf.environment and environment is None:
@@ -583,8 +662,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 e5727efc4..fb4d6f04a 100644
--- a/libadcc_src/amplitude_vector_enforce_spin_kind.cc
+++ b/libadcc_src/amplitude_vector_enforce_spin_kind.cc
@@ -31,7 +31,8 @@ namespace libadcc {
namespace lt = libtensor;
void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor,
- std::string block, std::string spin_kind) {
+ std::string block,
+ std::string spin_kind, bool is_ip) {
// Nothing to do for singles block
if (block == "s") return;
@@ -64,10 +65,130 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor,
return;
}
+ if (spin_kind == "doublet") {
+ auto& u2 = asbt3(doubles_tensor);
+ lt::block_tensor_ctrl<3, scalar_type> ctrl(u2);
+ const lt::symmetry<3, scalar_type>& sym = ctrl.req_const_symmetry();
+
+ // Extract the number of blocks per dimension
+ const lt::block_index_space<3>& bis = sym.get_bis();
+ lt::dimensions<3> bidims(bis.get_block_index_dims());
+
+ // Setup i1 to point to 0,0,0 and i2 to the half of the
+ // full number of blocks, i.e. to the alpha blocks in each
+ // dimension only.
+ lt::index<3> i1, i2;
+ for (size_t i = 0; i < 3; i++) i2[i] = bidims[i] / 2 - 1;
+
+ // Index range over all alpha-alpha-alpha blocks
+ // in all point group symmetries
+ const lt::index_range<3> index_range_alpha(i1, i2);
+
+ // This dimensions object contains the number of alpha blocks per dimension
+ lt::dimensions<3> bidims_alpha(index_range_alpha);
+
+ // Iterate over all alpha-alpha-alpha blocks
+ lt::abs_index<3> ai(bidims_alpha);
+ do {
+ // This gives the block index tuple
+ const lt::index<3>& ii = ai.get_index();
+
+ // Construct the orbit corresponding to this index (i.e. the iterator
+ // running over all elements equivalent by symmetry
+ // Ignore spin-forbidden, i.e. zero orbits
+ lt::short_orbit<3, scalar_type> orbi(sym, ii, /* compute_if_allowed_orbit = */ true);
+ if (!orbi.is_allowed()) continue;
+
+ // get_acindex -> get absolute canonical index
+ // Continue if our current index is larger than the canonical index
+ if (orbi.get_acindex() < ai.get_abs_index()) continue;
+
+ // TODO This might be wrong ... think about it and talk to Adrian
+ // the point is that orbi might have other strides than bidims_alpha
+ // Continue if the canonical index is already past the
+ // alpha-alpha-alpha block
+ if (orbi.get_acindex() > bidims_alpha.get_size()) continue;
+
+ // Get the index tuple of the canonical block of (alpha, alpha, alpha)
+ const lt::index<3>& ci = orbi.get_cindex();
+
+ lt::index<3> i1(ci);
+ lt::index<3> i2(ci);
+
+ if (is_ip) { // IP-ADC calculation
+ // set i1 to (alpha, beta, beta) equivalent of the canonical index
+ // pinned by ai and orbi
+ i1[1] += bidims_alpha[1];
+ i1[2] += bidims_alpha[2];
+
+ // set i2 to (beta, alpha, beta)
+ i2[0] += bidims_alpha[0];
+ i2[2] += bidims_alpha[2];
+
+ } else { // EA-ADC calculation
+ // set i1 to (beta, alpha, beta) equivalent of the canonical index
+ // pinned by ai and orbi
+ i1[0] += bidims_alpha[0];
+ i1[2] += bidims_alpha[2];
+
+ // set i2 to (beta, beta, alpha)
+ i2[0] += bidims_alpha[0];
+ i2[1] += bidims_alpha[1];
+ }
+
+ //
+ // What the following code does is that it keeps the spin projection (S^2)
+ // properly, provided that the symmetry setup is done as in
+ // contrib/adc_pp/adc_guess_d.C. It assumes the coefficients as setup in
+ // contrib/adc_pp/adc_guess_d.C adc_guess_d::build_guesses in order to preserve
+ // S^2 value setup in the guess.
+ //
+
+ lt::orbit<3, scalar_type> orb1(sym, i1, false), orb2(sym, i2, false);
+ // Canonical block of (a, b, b) for IP-ADC or (b, a, b) for EA-ADC
+ const lt::index<3>& ci1 = orb1.get_cindex();
+ // Canonical block of (b, a, b) for IP-ADC or (b, b, a) for EA-ADC
+ const lt::index<3>& ci2 = orb2.get_cindex();
+ bool zero1 = ctrl.req_is_zero_block(ci1);
+ bool zero2 = ctrl.req_is_zero_block(ci2);
+ if (zero1 && zero2) {
+ // Set (alpha, alpha, alpha) to zero
+ // This effectively filters out the quartet components with zero blocks
+ // in (alpha, beta, beta) and (beta, alpha, beta) (IP-ADC) erroneously
+ // introduced due to numerical errors. (beta, alpha, beta) and
+ // (beta, beta, alpha) blocks for EA-ADC.
+ ctrl.req_zero_block(ci);
+ continue;
+ }
+
+ // Get block corresponding to canonical index of (alpha, alpha, alpha)
+ lt::dense_tensor_wr_i<3, scalar_type>& blk = ctrl.req_block(ci);
+
+ if (!zero1) {
+ // IP: (alpha, beta, beta) / EA: (beta, alpha, beta) is not zero
+ lt::dense_tensor_rd_i<3, scalar_type>& blk1 = ctrl.req_const_block(ci1);
+ lt::tod_copy<3>(blk1, orb1.get_transf(i1)).perform(/* assign= */ true, blk);
+ ctrl.ret_const_block(ci1);
+ }
+ if (!zero2) {
+ // IP: (beta, alpha, beta) / EA: (beta, beta, alpha) is not zero
+ lt::dense_tensor_rd_i<3, scalar_type>& blk2 = ctrl.req_const_block(ci2);
+
+ // Assign if (alpha, beta, beta) for IP- and (beta, alpha, beta) for
+ // EA-ADC is zero, else +=
+ lt::tod_copy<3>(blk2, orb2.get_transf(i2)).perform(zero1, blk);
+ ctrl.ret_const_block(ci2);
+ }
+ ctrl.ret_block(ci);
+
+ } while (ai.inc());
+ return;
+ }
+
if (spin_kind != "singlet") {
throw not_implemented_error(
- "Only implemented for spin_kind == 'singlet' and spin_kind == "
- "'triplet'.");
+ "Only implemented for spin_kind == 'singlet', spin_kind == "
+ "'triplet' or spin_kind == 'doublet'.");
}
auto& u2 = asbt4(doubles_tensor);
@@ -169,4 +290,4 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor,
} while (ai.inc());
}
-} // namespace libadcc
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.hh b/libadcc_src/amplitude_vector_enforce_spin_kind.hh
index 6a7a4dbd8..7e6eaddfb 100644
--- a/libadcc_src/amplitude_vector_enforce_spin_kind.hh
+++ b/libadcc_src/amplitude_vector_enforce_spin_kind.hh
@@ -36,7 +36,9 @@ namespace libadcc {
* @param block The block of an amplitude this tensor represents
* @param spin_kind The kind of spin to enforce
*/
-void amplitude_vector_enforce_spin_kind(std::shared_ptr tensor, std::string block,
- std::string spin_kind);
+void amplitude_vector_enforce_spin_kind(std::shared_ptr tensor,
+ std::string block,
+ std::string spin_kind,
+ bool is_ip);
///@}
} // namespace libadcc
diff --git a/libadcc_src/fill_ea_doubles_guesses.cc b/libadcc_src/fill_ea_doubles_guesses.cc
new file mode 100644
index 000000000..b3d5c2f32
--- /dev/null
+++ b/libadcc_src/fill_ea_doubles_guesses.cc
@@ -0,0 +1,78 @@
+//
+// Copyright (C) 2020 by the adcc authors
+//
+// This file is part of adcc.
+//
+// adcc is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// adcc is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with adcc. If not, see .
+//
+
+#include "fill_ea_doubles_guesses.hh"
+#include "TensorImpl.hh"
+#include "guess/ea_adc_guess_d.hh"
+
+namespace libadcc {
+
+size_t fill_ea_doubles_guesses(std::vector> guesses_d,
+ std::shared_ptr mospaces,
+ std::shared_ptr d_o,
+ std::shared_ptr d_v,
+ bool a_spin, bool restricted, bool doublet,
+ int spin_change_twice,
+ scalar_type degeneracy_tolerance) {
+
+ size_t n_guesses = guesses_d.size();
+ if (n_guesses == 0) return 0;
+
+ // Make a copy of the doubles symmetry
+ libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0]));
+ libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis());
+ libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s);
+
+ // Make ab pointers object
+ auto make_ab = [](const MoSpaces& mo, const std::string& space) {
+ const std::vector& block_spin = mo.map_block_spin.at(space);
+ std::vector ab;
+ for (size_t i = 0; i < block_spin.size(); ++i) {
+ ab.push_back(block_spin[i] == 'b');
+ }
+ return ab;
+ };
+
+ const std::vector spaces_d = guesses_d[0]->subspaces();
+ std::vector> abvectors;
+ for (size_t i = 0; i < 3; ++i) {
+ abvectors.push_back(make_ab(*mospaces, spaces_d[i]));
+ }
+ libtensor::sequence<3, std::vector*> ab_d;
+ for (size_t i = 0; i < 3; ++i) {
+ ab_d[i] = &abvectors[i];
+ }
+
+ // Make singles list data structure
+ std::list*, double>> guesspairs;
+ for (size_t i = 0; i < n_guesses; i++) {
+ guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0);
+ }
+
+ if (abs(spin_change_twice) != 1){
+ throw not_implemented_error("spin_change ==" +
+ std::to_string(spin_change_twice) + " has not been tested.");
+ }
+
+ return ea_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin,
+ restricted, doublet, ab_d, spin_change_twice,
+ degeneracy_tolerance);
+}
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/fill_ea_doubles_guesses.hh b/libadcc_src/fill_ea_doubles_guesses.hh
new file mode 100644
index 000000000..4d6c3c072
--- /dev/null
+++ b/libadcc_src/fill_ea_doubles_guesses.hh
@@ -0,0 +1,53 @@
+//
+// Copyright (C) 2020 by the adcc authors
+//
+// This file is part of adcc.
+//
+// adcc is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// adcc is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with adcc. If not, see .
+//
+
+#pragma once
+#include "Tensor.hh"
+
+namespace libadcc {
+
+
+/** Fill the passed vector of doubles blocks with doubles guesses using
+ * the O and V matrices.
+ *
+ *
+ * guesses_d Vectors of guesses, all elements are assumed to be initialised to zero
+ * and the symmetry is assumed to be properly set up.
+ * mospaces Mospaces object
+ * d_o Fock matrix to construct guesses from (occ.)
+ * d_v Fock matrix to construct guesses from (virt.)
+ * a_spin If alpha ionization (false: beta)
+ * restricted Is this a restricted calculation
+ * doublet Doublet or quartet states (only in case of restricted calculation)
+ * spin_change_twice Twice the value of the spin change to enforce in an excitation.
+ * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered
+ * degenerate, i.e. identical.
+ *
+ * \returns The number of guess vectors which have been properly initialised
+ * (the others are invalid and should be discarded).
+ */
+size_t fill_ea_doubles_guesses(std::vector> guesses_d,
+ std::shared_ptr mospaces,
+ std::shared_ptr d_o,
+ std::shared_ptr d_v,
+ bool a_spin, bool restricted, bool doublet,
+ int spin_change_twice,
+ scalar_type degeneracy_tolerance);
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/fill_ip_doubles_guesses.cc b/libadcc_src/fill_ip_doubles_guesses.cc
new file mode 100644
index 000000000..aa8885d02
--- /dev/null
+++ b/libadcc_src/fill_ip_doubles_guesses.cc
@@ -0,0 +1,78 @@
+//
+// Copyright (C) 2020 by the adcc authors
+//
+// This file is part of adcc.
+//
+// adcc is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// adcc is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with adcc. If not, see .
+//
+
+#include "fill_ip_doubles_guesses.hh"
+#include "TensorImpl.hh"
+#include "guess/ip_adc_guess_d.hh"
+
+namespace libadcc {
+
+size_t fill_ip_doubles_guesses(std::vector> guesses_d,
+ std::shared_ptr mospaces,
+ std::shared_ptr d_o,
+ std::shared_ptr d_v,
+ bool a_spin, bool restricted, bool doublet,
+ int spin_change_twice,
+ scalar_type degeneracy_tolerance) {
+
+ size_t n_guesses = guesses_d.size();
+ if (n_guesses == 0) return 0;
+
+ // Make a copy of the doubles symmetry
+ libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0]));
+ libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis());
+ libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s);
+
+ // Make ab pointers object
+ auto make_ab = [](const MoSpaces& mo, const std::string& space) {
+ const std::vector& block_spin = mo.map_block_spin.at(space);
+ std::vector ab;
+ for (size_t i = 0; i < block_spin.size(); ++i) {
+ ab.push_back(block_spin[i] == 'b');
+ }
+ return ab;
+ };
+
+ const std::vector spaces_d = guesses_d[0]->subspaces();
+ std::vector> abvectors;
+ for (size_t i = 0; i < 3; ++i) {
+ abvectors.push_back(make_ab(*mospaces, spaces_d[i]));
+ }
+ libtensor::sequence<3, std::vector*> ab_d;
+ for (size_t i = 0; i < 3; ++i) {
+ ab_d[i] = &abvectors[i];
+ }
+
+ // Make singles list data structure
+ std::list*, double>> guesspairs;
+ for (size_t i = 0; i < n_guesses; i++) {
+ guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0);
+ }
+
+ if (abs(spin_change_twice) != 1){
+ throw not_implemented_error("spin_change ==" +
+ std::to_string(spin_change_twice) + " has not been tested.");
+ }
+
+ return ip_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin,
+ restricted, doublet, ab_d, spin_change_twice,
+ degeneracy_tolerance);
+}
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/fill_ip_doubles_guesses.hh b/libadcc_src/fill_ip_doubles_guesses.hh
new file mode 100644
index 000000000..10bbf1d2d
--- /dev/null
+++ b/libadcc_src/fill_ip_doubles_guesses.hh
@@ -0,0 +1,53 @@
+//
+// Copyright (C) 2020 by the adcc authors
+//
+// This file is part of adcc.
+//
+// adcc is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// adcc is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with adcc. If not, see .
+//
+
+#pragma once
+#include "Tensor.hh"
+
+namespace libadcc {
+
+
+/** Fill the passed vector of doubles blocks with doubles guesses using
+ * the O and V matrices.
+ *
+ *
+ * guesses_d Vectors of guesses, all elements are assumed to be initialised to zero
+ * and the symmetry is assumed to be properly set up.
+ * mospaces Mospaces object
+ * d_o Fock matrix to construct guesses from (occ.)
+ * d_v Fock matrix to construct guesses from (virt.)
+ * a_spin If alpha ionization (false: beta)
+ * restricted Is this a restricted calculation
+ * doublet Doublet or quartet states (only in case of restricted calculation)
+ * spin_change_twice Twice the value of the spin change to enforce in an excitation.
+ * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered
+ * degenerate, i.e. identical.
+ *
+ * \returns The number of guess vectors which have been properly initialised
+ * (the others are invalid and should be discarded).
+ */
+size_t fill_ip_doubles_guesses(std::vector> guesses_d,
+ std::shared_ptr mospaces,
+ std::shared_ptr d_o,
+ std::shared_ptr d_v,
+ bool a_spin, bool restricted, bool doublet,
+ int spin_change_twice,
+ scalar_type degeneracy_tolerance);
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/guess/ea_adc_guess_d.cc b/libadcc_src/guess/ea_adc_guess_d.cc
new file mode 100644
index 000000000..9f432d69c
--- /dev/null
+++ b/libadcc_src/guess/ea_adc_guess_d.cc
@@ -0,0 +1,484 @@
+#include "ea_adc_guess_d.hh"
+#include "../exceptions.hh"
+
+// Change visibility of libtensor singletons to public
+#pragma GCC visibility push(default)
+#include
+#include
+#include
+#include
+#include
+#include
+#pragma GCC visibility pop
+
+namespace libadcc {
+
+// TODO This file definitely needs a cleanup.
+
+using namespace libtensor;
+using libtensor::index;
+
+/** \brief Element type for guess vectors
+ **/
+template
+struct guess_element {
+ libtensor::index bidx; //!< Block index
+ libtensor::index idx; //!< In block index
+ double coeff; //!< Coefficient;
+
+ guess_element(const libtensor::index& bidx_, const libtensor::index& idx_,
+ const double& coeff_)
+ : bidx(bidx_), idx(idx_), coeff(coeff_) {}
+};
+
+/** \brief Base class for guess formation **/
+template
+class index_handler {
+ public:
+ protected:
+ libtensor::sequence*> m_ab; //!< Alpha-beta block markers
+
+ private:
+ libtensor::sequence m_na; //!< Number of alpha spin blocks
+
+ public:
+ /** \brief Constructor
+ \param ab Alpha-beta block markers (for N orbital spaces)
+ \param sym Symmetry of guess vectors
+ \param ms Spin multiplicity
+ **/
+ index_handler(const libtensor::sequence*>& ab)
+ : m_ab(ab), m_na(0) {
+ for (size_t i = 0; i < N; i++) {
+ for (size_t j = 0; j < m_ab[i]->size(); j++) {
+ if (!m_ab[i]->at(j)) m_na[i]++;
+ }
+ }
+ }
+
+ /** \brief Calculates the spin projection \f$ m_s \f$ of the block.
+ \param bidx Block index
+ \param orb_type Orbital type per dim (true = occupied)
+ \return -1 or +1 for ionization of an alpha or beta electron
+ */
+ int get_spin_proj(const libtensor::mask& orb_type,
+ const libtensor::index& bidx) const {
+ for (size_t i = 0; i < N; i++) {
+ if (bidx[i] > m_ab[i]->size()) {
+ throw runtime_error("Block index exceeds dim");
+ }
+ }
+
+ int ms = 0;
+ for (size_t i = 0; i < N; i++) {
+ // Left side is true if orbital is occ.
+ // Right side is true if orbital has beta spin
+ // Hence it is true for occ. beta orbitals and virt. alpha orbitals
+ if (orb_type[i] == m_ab[i]->at(bidx[i]))
+ ms += 1;
+ else
+ ms -= 1;
+ }
+ return ms;
+ }
+
+ /** \brief Split block index into spatial part and spin part
+ \param bidx Input block index
+ \param sp Spin index (alpha = false, beta = true)
+ \param sbidx Spatial block index
+ **/
+ void split_block_index(const libtensor::index& bidx, libtensor::mask& sp,
+ libtensor::index& sbidx) const {
+ for (size_t i = 0; i < N; i++) {
+ sp[i] = m_ab[i]->at(bidx[i]);
+ sbidx[i] = (sp[i] ? bidx[i] - m_na[i] : bidx[i]);
+ }
+ }
+
+ /** \brief Merge spatial part and spin part of block index
+ \param sp Spin index (alpha = false, beta = true)
+ \param sbidx Spatial block index
+ \param bidx Input block index
+ **/
+ void merge_block_index(const libtensor::mask& sp, const libtensor::index& sbidx,
+ libtensor::index& bidx) const {
+ for (size_t i = 0; i < N; i++) {
+ bidx[i] = (sp[i] ? sbidx[i] + m_na[i] : sbidx[i]);
+ }
+ }
+};
+
+namespace {
+typedef libtensor::compare4min compare_t;
+typedef libtensor::btod_select<1, compare_t>::list_type list1d_t;
+typedef libtensor::btod_select<3, compare_t>::list_type list3d_t;
+typedef std::list*, double>> list_t;
+
+/** Determine if occupied indices should be symmetrized */
+void determine_sym(const symmetry<3, double>& sym, bool& sym_v) {
+
+ sym_v = false;
+ for (symmetry<3, double>::iterator it1 = sym.begin(); it1 != sym.end(); it1++) {
+
+ const symmetry_element_set<3, double>& set = sym.get_subset(it1);
+ const std::string& id = set.get_id();
+
+ if (id.compare(se_perm<3, double>::k_sym_type) != 0) continue;
+ if (set.is_empty()) return;
+
+ typedef symmetry_element_set_adapter<3, double, se_perm<3, double>> adapter_t;
+
+ adapter_t ad(set);
+ for (adapter_t::iterator it2 = ad.begin(); it2 != ad.end(); it2++) {
+
+ const se_perm<3, double>& el = ad.get_elem(it2);
+
+ const permutation<3>& p = el.get_perm();
+ sym_v |= (p[1] == 2 && p[2] == 1);
+ }
+ }
+}
+
+/** Determine the spin of the guess vectors */
+unsigned determine_spin(bool restricted, bool doublet) {
+
+ if (restricted) {
+ if (doublet) return 2;
+ else return 4;
+ } else {
+ return 0;
+ }
+}
+
+/** Transfers the elements of a 1D list to a 3D list */
+void transfer_elements(const list1d_t& o, const list1d_t& v,
+ index_group_map_h2p& to, const libtensor::symmetry<3,
+ double>& sym, const index_handler<3>& base, int dm_s) {
+
+ // Determine symmetry
+ bool sym_v; // Are the two virtual indices identical
+ determine_sym(sym, sym_v);
+
+ to.clear();
+
+ dimensions<3> bidims = sym.get_bis().get_block_index_dims();
+
+ for (list1d_t::const_iterator ita = o.begin(); ita != o.end(); ita++) {
+
+ for (list1d_t::const_iterator itb = v.begin(); itb != v.end(); itb++) {
+
+ for (list1d_t::const_iterator itc = v.begin(); itc != v.end(); itc++) {
+
+ // Discard element combinations which are not allowed due to the
+ // permutational symmetry!!!
+ const index<1>& bidxa = ita->get_block_index();
+ const index<1>& idxa = ita->get_in_block_index();
+ const index<1>& bidxb = itb->get_block_index();
+ const index<1>& idxb = itb->get_in_block_index();
+ const index<1>& bidxc = itc->get_block_index();
+ const index<1>& idxc = itc->get_in_block_index();
+
+ if (sym_v && bidxb[0] == bidxc[0] && idxb[0] == idxc[0]) continue;
+
+ double value = ita->get_value() + itb->get_value() + itc->get_value();
+
+ libtensor::index<3> bidx, idx;
+ bidx[0] = bidxa[0];
+ bidx[1] = bidxb[0];
+ bidx[2] = bidxc[0];
+ idx[0] = idxa[0];
+ idx[1] = idxb[0];
+ idx[2] = idxc[0];
+
+ if (sym_v && bidx[1] > bidx[2]) {
+ std::swap(bidx[1], bidx[2]);
+ std::swap(idx[1], idx[2]);
+ } else if (sym_v && bidx[1] == bidx[2] && idx[1] > idx[2]) {
+ std::swap(idx[1], idx[2]);
+ }
+
+ // Ignore blocks where the targeted spin_change is not achieved
+ mask<3> orb_type;
+ orb_type[0] = true;
+ if (base.get_spin_proj(orb_type, bidx) != dm_s) continue;
+
+ // Check if the block is allowed in the symmetry of the guess
+ orbit<3, double> orb(sym, bidx);
+ if (!orb.is_allowed()) continue;
+
+ // Find canonical index
+ abs_index<3> abi(orb.get_acindex(), bidims);
+ const tensor_transf<3, double>& tr = orb.get_transf(bidx);
+ bidx = abi.get_index();
+ permutation<3> pinv(tr.get_perm(), true);
+ idx.permute(pinv);
+
+ // Split block index into spin part and spatial part
+ mask<3> spm;
+ index<3> spi;
+ base.split_block_index(bidx, spm, spi);
+
+ to.add_index(value, spm, spi, idx);
+ } // for itc
+ } // for itb
+ } // for ita
+}
+
+size_t build_guesses(list_t::iterator& cur_guess, list_t::iterator end,
+ const index_group_h2p& ig, double value,
+ const symmetry<3, double>& sym, bool a_spin,
+ bool restricted, bool doublet, index_handler<3>& base) {
+ bool sym_v; // Are the two occupied indices identical
+ determine_sym(sym, sym_v);
+ const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry
+
+ if (cur_guess == end) return 0;
+
+ int ms = a_spin ? 1 : -1;
+
+ const index<3>& spidx = ig.get_spatial_bidx();
+ const index<3>& idx = ig.get_idx();
+
+ std::vector>> lv;
+
+ // No specific spin create as many guesses as there are available in the
+ // index group
+ if (spin == 0) {
+ lv.resize(ig.size());
+
+ // Reform full block indices
+ size_t i = 0;
+ std::vector> bidx(ig.size());
+ for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) {
+ base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]);
+ }
+
+ if (ig.size() == 1) {
+
+ static double coeff[1] = {1.0};
+ for (size_t i = 0; i < 1; i++) {
+ for (size_t j = 0; j < 1; j++)
+ lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j]));
+ }
+ } else if (ig.size() == 3) {
+ static const double coeff[3][3] = {
+ // in case of ms == 1 (alpha attachment)
+ // aaa bab bba
+ // and in case of ms == -1 (beta attachment)
+ // bbb aba aab
+ { 1.0, -1.0, -1.0}, // quartet
+ { 0.0, -1.0, 1.0}, // doublet 1
+ { -2.0, -1.0, -1.0}}; // doublet 2
+
+ if (ms == 1) { // alpha attachment
+ for (size_t i = 0; i < 3; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j]));
+ }
+ } else { // beta attachment
+ for (size_t i = 0; i < 3; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j]));
+ }
+ }
+ } else {
+ // Form spin elements
+ for (size_t i = 0; i < ig.size(); i++)
+ lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0));
+ }
+ }
+ else if (spin == 2) {
+ // Reform full block indices
+ size_t i = 0;
+ std::vector> bidx(ig.size());
+ for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) {
+ base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]);
+ }
+
+ if (ig.size() == 1) {
+ lv.resize(1);
+
+ static double coeff[1] = {1.0};
+ for (size_t i = 0; i < 1; i++) {
+ for (size_t j = 0; j < 1; j++)
+ lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j]));
+ }
+ } else if (ig.size() == 3) {
+ lv.resize(2);
+
+ static const double coeff[2][3] = {
+ // in case of ms == 1 (alpha attachment)
+ // aaa bab bba
+ // and in case of ms == -1 (beta attachment)
+ // bbb aba aab
+ { 0.0, -1.0, 1.0}, // doublet 1
+ { -2.0, -1.0, -1.0}}; // doublet 2
+
+ if (ms == 1) { // alpha attachment
+ for (size_t i = 0; i < 2; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j]));
+ }
+ } else { // beta attachment
+ for (size_t i = 0; i < 2; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j]));
+ }
+ }
+ } else {
+ // Form spin elements
+ lv.resize(ig.size());
+ for (size_t i = 0; i < ig.size(); i++) {
+ lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0));
+ }
+ }
+ } else if (spin == 4) {
+ // Reform full block indices
+ size_t i = 0;
+ std::vector> bidx(ig.size());
+ for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) {
+ base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]);
+ }
+
+ if (ig.size() == 3) {
+ lv.resize(1);
+
+ static const double coeff[1][3] = {
+ // in case of ms == 1 (alpha attachment)
+ // aaa bab bba
+ // and in case of ms == -1 (beta attachment)
+ // bbb aba aab
+ { 1.0, -1.0, -1.0}}; // quartet
+
+ if (ms == 1) { // alpha attachment
+ for (size_t i = 0; i < 1; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j]));
+ }
+ } else { // beta attachment
+ for (size_t i = 0; i < 1; i++) {
+ for (size_t j = 0; j < 3; j++)
+ lv[i].push_back(guess_element<3>(bidx[2-j], idx, coeff[i][j]));
+ }
+ }
+ } else {
+ // Form spin elements
+ lv.resize(ig.size());
+ for (size_t i = 0; i < ig.size(); i++) {
+ lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0));
+ }
+ }
+ }
+
+
+ size_t i = 0;
+ for (; i < lv.size() && cur_guess != end; i++, cur_guess++) {
+ libtensor::btensor<3, double>& bt = *(cur_guess->first);
+ { // Setup up the symmetry
+ libtensor::block_tensor_wr_ctrl<3, double> ctrl(bt);
+ ctrl.req_zero_all_blocks();
+ libtensor::symmetry<3, double>& sym_to = ctrl.req_symmetry();
+ libtensor::so_copy<3, double>(sym).perform(sym_to);
+ }
+
+ // Set the elements
+ libtensor::btod_set_elem<3> set_op;
+ for (auto it = lv[i].begin(); it != lv[i].end(); it++) {
+ set_op.perform(bt, it->bidx, it->idx, it->coeff);
+ }
+
+ // Normalise
+ double norm = libtensor::btod_dotprod<3>(bt, bt).calculate();
+ if (norm != 1.0) {
+ libtensor::btod_scale<3>(bt, 1.0 / sqrt(norm)).perform();
+ }
+ cur_guess->second = value;
+ }
+ return i;
+}
+
+} // namespace
+
+size_t ea_adc_guess_d(std::list*, double>>& va,
+ libtensor::btensor_i<1, double>& d_o,
+ libtensor::btensor_i<1, double>& d_v,
+ const libtensor::symmetry<3, double>& sym,
+ bool a_spin, bool restricted, bool doublet,
+ const libtensor::sequence<3, std::vector*>& ab,
+ int dm_s, double degeneracy_tolerance) {
+
+ size_t nguesses = va.size();
+ if (nguesses == 0) return 0;
+
+ // TODO sym_v should be stored in an adc_guess_base-like object
+ // Determine symmetry and spin
+ bool sym_v; // Are the two occupied indices identical
+ determine_sym(sym, sym_v);
+ const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry
+
+ size_t ns = nguesses;
+ index_group_map_h2p igm(degeneracy_tolerance, sym_v);
+
+ bool max_reached = false;
+ // Create empty 1d symmetry to use with btod_select
+ symmetry<1, double> sym1(d_o.get_bis()), sym2(d_v.get_bis());
+
+ // Search for smallest elements until we have found enough.
+ size_t size = 0;
+ while (size < nguesses && !max_reached) {
+
+ igm.clear();
+ size = 0;
+
+ ns *= 2;
+ list1d_t ilx_o, ilx_v;
+ btod_select<1, compare_t>(d_o, sym1).perform(ilx_o, ns);
+ btod_select<1, compare_t>(d_v, sym2).perform(ilx_v, ns);
+
+ max_reached = ilx_o.size() < ns;
+
+ index_handler<3> base(ab);
+ transfer_elements(ilx_o, ilx_v, igm, sym, base, dm_s);
+ ilx_o.clear();
+ ilx_v.clear();
+
+ //size++; // we want to have the real size of the index group guesses
+ // Count the number of elements
+ if (spin == 0) {
+ for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) {
+ size += igm.get_group(it).size();
+ }
+ } else if (spin == 2) {
+ for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) {
+ if (igm.get_group(it).size() == 3) {
+ size += 2; // two doublets, one quartet
+ } else if (igm.get_group(it).size() == 1) {
+ size += 1; // only a doublet in this case
+ }
+ }
+ } else if (spin == 4) {
+ for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) {
+ if (igm.get_group(it).size() == 3) {
+ size += 1; // one quartet
+ }
+ }
+ }
+ } // while
+
+ // Now form the guess vectors
+ nguesses = 0;
+
+ list_t::iterator guess = va.begin();
+ // Loop until list is empty or we have constructed all guesses
+ index_group_map_h2p::iterator it = igm.begin();
+ while (it != igm.end() && guess != va.end()) {
+ index_handler<3> base(ab);
+ nguesses +=
+ build_guesses(guess, va.end(), igm.get_group(it), igm.get_value(it),
+ sym, a_spin, restricted, doublet, base);
+ it++;
+ }
+
+ return nguesses;
+}
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/guess/ea_adc_guess_d.hh b/libadcc_src/guess/ea_adc_guess_d.hh
new file mode 100644
index 000000000..9e943696e
--- /dev/null
+++ b/libadcc_src/guess/ea_adc_guess_d.hh
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "index_group_h2p.hh"
+
+namespace libadcc {
+
+/** \brief Forms a list of doubles guess vectors.
+ Selects the smallest elements from the provided OV matrices (for Koopman's
+ guess this should be the delta Fock matrix) and combines two of these
+ elements to form the doubles guesses.
+ \param va List of doubles-value pairs to initialize.
+ \param d_o Fock matrix to construct guesses from (occ.)
+ \param d_v Fock matrix to construct guesses from. (virt.)
+ \param sym Symmetry of guess vectors.
+ \param a_spin If alpha ionization (false: beta)
+ \param restricted Is this a restricted calculation
+ \param doublet Doublet or quartet states (only in case of restricted
+ calculation)
+ \param ab Alpha/beta spin blocks of occupied orbitals.
+ \param dm_s Delta m_s, spin-change twice
+ \return Number of guess vectors created
+ **/
+size_t ea_adc_guess_d(std::list*, double>>& va,
+ libtensor::btensor_i<1, double>& d_o,
+ libtensor::btensor_i<1, double>& d_v,
+ const libtensor::symmetry<3, double>& sym,
+ bool a_spin, bool restricted, bool doublet,
+ const libtensor::sequence<3, std::vector*>& ab,
+ int dm_s, double degeneracy_tolerance);
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/guess/index_group_h2p.cc b/libadcc_src/guess/index_group_h2p.cc
new file mode 100644
index 000000000..1376aee6f
--- /dev/null
+++ b/libadcc_src/guess/index_group_h2p.cc
@@ -0,0 +1,84 @@
+#include "index_group_h2p.hh"
+#include "../exceptions.hh"
+#include
+
+namespace libadcc {
+
+using namespace libtensor;
+using libtensor::index;
+
+libtensor::mask<3> index_group_h2p::get_spin_mask(size_t sp) const {
+ if (m_s.count(sp) == 0) {
+ throw runtime_error("Could not find spin state sp ==" + std::to_string(sp) + ".");
+ }
+ return compute_spin_mask(sp);
+}
+
+size_t index_group_h2p::compute_spin(const mask<3>& spm) {
+
+ size_t s = 0;
+ for (size_t i = 0; i < 3; i++) s = s * 2 + (spm[i] ? 1 : 0);
+
+ return s;
+}
+
+mask<3> index_group_h2p::compute_spin_mask(size_t sp) {
+
+ mask<3> m;
+ size_t i = 0, curbit = 1 << 2;
+ while (sp != 0 && i < 3) {
+ m[i++] = (sp & curbit);
+ curbit >>= 1;
+ }
+ return m;
+}
+
+void index_group_map_h2p::add_index(double val, mask<3> spm, index<3> spidx, index<3> idx) {
+
+ find_canonical_index(spm, spidx, idx);
+
+ // Loop over group map and look for similar value
+ std::multimap::iterator it = m_idxmap.begin();
+ for (; it != m_idxmap.end(); it++) {
+ if (fabs(val - it->first) < m_thresh) break;
+ }
+
+ // Try to add element to index groups which belong to similar
+ // values
+ bool added = false;
+ while (it != m_idxmap.end() && fabs(val - it->first) < m_thresh && !added) {
+
+ index_group_h2p& grp = it->second;
+ if (spidx == grp.get_spatial_bidx() && idx == grp.get_idx()) {
+ grp.add(spm);
+ added = true;
+ }
+ it++;
+ }
+
+ // If no index group found start a new one.
+ if (!added) {
+ std::multimap::iterator ic = m_idxmap.insert(
+ std::pair(val, index_group_h2p(spidx, idx)));
+ ic->second.add(spm);
+ }
+}
+
+void index_group_map_h2p::find_canonical_index(mask<3>& m, index<3>& spidx,
+ index<3>& idx) const {
+
+ if (m_sym_v) {
+ if (spidx[1] == spidx[2]) {
+ if (idx[1] > idx[2]) {
+ std::swap(idx[1], idx[2]);
+ std::swap(m[1], m[2]);
+ }
+ } else if (spidx[1] > spidx[2]) {
+ std::swap(spidx[1], spidx[2]);
+ std::swap(idx[1], idx[2]);
+ std::swap(m[1], m[2]);
+ }
+ }
+}
+
+} // namespace libadcc
\ No newline at end of file
diff --git a/libadcc_src/guess/index_group_h2p.hh b/libadcc_src/guess/index_group_h2p.hh
new file mode 100644
index 000000000..47b353581
--- /dev/null
+++ b/libadcc_src/guess/index_group_h2p.hh
@@ -0,0 +1,164 @@
+#pragma once
+// Change visibility of libtensor singletons to public
+#pragma GCC visibility push(default)
+#include
+#pragma GCC visibility pop
+#include