diff --git a/adcc/AdcMatrix.py b/adcc/AdcMatrix.py index 7b6c5974..a0643881 100644 --- a/adcc/AdcMatrix.py +++ b/adcc/AdcMatrix.py @@ -27,6 +27,8 @@ from .LazyMp import LazyMp from .adc_pp import matrix as ppmatrix +from .adc_ip import matrix as ipmatrix +from .adc_ea import matrix as eamatrix from .timings import Timer, timed_member_call from .AdcMethod import AdcMethod, Method, AdcType from .functions import ones_like @@ -73,6 +75,8 @@ class AdcMatrixlike: _special_block_orders = { "adc2x": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 1}, + "ip-adc2x": {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 1}, + "ea-adc2x": {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 1}, "isr1s": {"ph_ph": 1, "ph_pphh": None, "pphh_ph": None, "pphh_pphh": None}, "isr2d": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 0}, } @@ -104,7 +108,9 @@ def _default_block_orders(cls, method: Method, # - determine which spaces are available in the ADC(n) matrix # starting from the given minimal space min_space = { - AdcType.PP: "ph" + AdcType.PP: "ph", + AdcType.IP: "h", + AdcType.EA: "p", }.get(method.adc_type, None) if min_space is None: raise ValueError(f"Unknown adc type {method.adc_type.to_str()} for " @@ -196,6 +202,10 @@ def _is_valid_space(cls, space: str, method: Method) -> bool: # be equal or differ e.g. by +-1 (IP/EA) if method.adc_type is AdcType.PP: return n_particle == n_hole + elif method.adc_type is AdcType.IP: + return n_particle == n_hole - 1 + elif method.adc_type is AdcType.EA: + return n_particle == n_hole + 1 raise ValueError(f"Unknown adc type {method.adc_type.to_str()} for method " f"{method.name}. Can not validate space.") @@ -268,11 +278,19 @@ def __init__(self, method, hf_or_mp, block_orders=None, intermediates=None, variant = None if self.is_core_valence_separated: variant = "cvs" + # Directly import block dispatch functions? + BLOCK_DISPATCH = { + AdcType.PP: ppmatrix.block, + AdcType.IP: ipmatrix.block, + AdcType.EA: eamatrix.block} + block_dispatch_fun = BLOCK_DISPATCH[self.method.adc_type] blocks = { - block: ppmatrix.block(self.ground_state, block.split("_"), - order=order, intermediates=self.intermediates, - variant=variant) - for block, order in self.block_orders.items() if order is not None + block: block_dispatch_fun(self.ground_state, block.split("_"), + order=order, + intermediates=self.intermediates, + variant=variant) + for block, order in self.block_orders.items() + if order is not None } self.blocks = {bl: blocks[bl].apply for bl in blocks} if diagonal_precomputed: @@ -442,33 +460,57 @@ def construct_symmetrisation_for_blocks(self): Returns a dictionary block identifier -> function """ ret = {} - if self.is_core_valence_separated: - # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a) - ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)]) - else: - def symmetrise_generic_adc_doubles(invec): - # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a) - # doubles part is antisymmetric wrt. (i,j,a,b) <-> (j,i,a,b) - scratch = invec.antisymmetrise([(0, 1)]).antisymmetrise([(2, 3)]) - # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a) - return scratch.symmetrise([(0, 1), (2, 3)]) - ret["pphh"] = symmetrise_generic_adc_doubles - - def symmetrise_generic_adc_triples(invec): - # triples part is antisymmetric wrt. permutations of (i,j,k) - # and wrt. permutations of (a,b,c) - scratch = ( - invec.antisymmetrise([(0, 1, 2)]).antisymmetrise([(3, 4, 5)]) - ) - # triples part is symmetric wrt. permutations of (i,j,k) and - # permutations of (a,b,c) - # NOTE: The doubles fix the (numerical) symmetry with a single - # symmetrise call. This is not possible for triples, since the - # following symmetrise call only covers 6 of the 18 - # even permutations generated by the 2 antisymmetrise calls above: - # ijkabc + ikjacb + jikbac + jkibca + kijcab + kjicba - return scratch.symmetrise([(0, 1, 2), (3, 4, 5)]) - ret["ppphhh"] = symmetrise_generic_adc_triples + if self.method.adc_type is AdcType.PP: + if self.is_core_valence_separated: + # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a) + ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)]) + else: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a) + # doubles part is antisymmetric wrt. (i,j,a,b) <-> (j,i,a,b) + scratch = invec.antisymmetrise([(0, 1)]).antisymmetrise( + [(2, 3)]) + # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a) + return scratch.symmetrise([(0, 1), (2, 3)]) + ret["pphh"] = symmetrise_generic_adc_doubles + + def symmetrise_generic_adc_triples(invec): + # triples part is antisymmetric wrt. permutations of (i,j,k) + # and wrt. permutations of (a,b,c) + scratch = ( + invec.antisymmetrise([(0, 1, 2)]).antisymmetrise( + [(3, 4, 5)]) + ) + # triples part is symmetric wrt. permutations of (i,j,k) and + # permutations of (a,b,c) + # NOTE: The doubles fix the (numerical) symmetry with a single + # symmetrise call. This is not possible for triples, since the + # following symmetrise call only covers 6 of the 18 + # even permutations generated by the 2 antisymmetrise calls + # above: + # ijkabc + ikjacb + jikbac + jkibca + kijcab + kjicba + return scratch.symmetrise([(0, 1, 2), (3, 4, 5)]) + ret["ppphhh"] = symmetrise_generic_adc_triples + elif self.method.adc_type is AdcType.IP: + if not self.is_core_valence_separated: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,j,a) <-> (j,i,a) + return invec.antisymmetrise([(0, 1)]) + ret["phh"] = symmetrise_generic_adc_doubles + + def symmetrise_generic_adc_triples(invec): + # TODO + pass + elif self.method.adc_type is AdcType.EA: + if not self.is_core_valence_separated: + def symmetrise_generic_adc_doubles(invec): + # doubles part is antisymmetric wrt. (i,a,b) <-> (i,a,b) + return invec.antisymmetrise([(1, 2)]) + ret["pph"] = symmetrise_generic_adc_doubles + + def symmetrise_generic_adc_triples(invec): + # TODO + pass return ret def dense_basis(self, axis_blocks=None, ordering="adcc"): diff --git a/adcc/AdcMethod.py b/adcc/AdcMethod.py index df14e6b5..c3257a3c 100644 --- a/adcc/AdcMethod.py +++ b/adcc/AdcMethod.py @@ -70,6 +70,8 @@ def to_int(self) -> int: class AdcType(Enum): PP = "pp" + IP = "ip" + EA = "ea" def to_str(self) -> str: return self.value @@ -253,7 +255,7 @@ def at_level(self: T, newlevel: Union[int, str]) -> T: def as_method(self, method_cls: type[T]) -> T: """ - Return a equivalent Method with the method base name replaced + Return an equivalent Method with the method base name replaced by the provided name. """ assert self._method_base_name is not None @@ -294,7 +296,23 @@ class AdcMethod(Method): ): LevelSpec( max_level=3, special_levels=(MethodLevel.TWO_X,) - ) + ), + LevelKey( + adc_type=AdcType.IP, + gs_type=GroundStateType.MP, + cvs=False + ): LevelSpec( + max_level=3, + special_levels=(MethodLevel.TWO_X,) + ), + LevelKey( + adc_type=AdcType.EA, + gs_type=GroundStateType.MP, + cvs=False + ): LevelSpec( + max_level=3, + special_levels=(MethodLevel.TWO_X,) + ), } @@ -316,5 +334,21 @@ class IsrMethod(Method): ): LevelSpec( max_level=2, special_levels=(MethodLevel.ONE_S, MethodLevel.TWO_D) + ), + LevelKey( + adc_type=AdcType.IP, + gs_type=GroundStateType.MP, + cvs=False + ): LevelSpec( + max_level=2, + special_levels=None + ), + LevelKey( + adc_type=AdcType.EA, + gs_type=GroundStateType.MP, + cvs=False + ): LevelSpec( + max_level=2, + special_levels=None ) } diff --git a/adcc/AmplitudeVector.py b/adcc/AmplitudeVector.py index aff573b6..b08adad2 100644 --- a/adcc/AmplitudeVector.py +++ b/adcc/AmplitudeVector.py @@ -26,7 +26,9 @@ class AmplitudeVector(dict): def __init__(self, **kwargs): """ Construct an AmplitudeVector. Typical use cases are - ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``. + ``AmplitudeVector(ph=tensor_singles, pphh=tensor_doubles)``. For IP-ADC + ``AmplitudeVector(h=tensor_singles, phh=tensor_doubles)``, and for + EA-ADC ``AmplitudeVector(p=tensor_singles, pph=tensor_doubles)`` """ super().__init__(**kwargs) diff --git a/adcc/ChargedExcitations.py b/adcc/ChargedExcitations.py new file mode 100644 index 00000000..1178cbf7 --- /dev/null +++ b/adcc/ChargedExcitations.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +import numpy as np +from scipy import constants + +from .import adc_ip, adc_ea +from .ElectronicStates import TableColumn, ElectronicStates, _timer_name +from .functions import dot +from .misc import cached_member_function +from .AdcMethod import AdcType + + +class ChargedExcitation(ElectronicStates): + + @property + def pole_strength(self) -> np.ndarray: + """Array of pole strengths of all computed states""" + pass + + @cached_member_function(timer=_timer_name, separate_timings_by_args=False) + def _pole_strength(self, state_n: int) -> np.ndarray: + """Computes the pole strength for a single state""" + pass + + def _describe_helper(self, block_norms=True, excitation_type_name="energy"): + """ + Creates and returns the to be printed columns + + Parameters + ---------- + block_norms : bool, optional + Show the norms of the n particle (n-1) hole blocks of the charged + excited states, by default ``True``. + + excitation_type_name : str, optional + Defines the name of the energy property. + 'ionization potential'/'electron affinity' for IP/EA + """ + # Collect the columns to print + columns: list[TableColumn] = [] + values: list[str] = [] + # count the number of states + values.extend(str(i) for i in range(self.size)) + columns.append(TableColumn(header="#", values=values.copy(), unit="")) + values.clear() + # excitation energy in a.u. and eV + eV = constants.value("Hartree energy in eV") + values.extend(f"{e:^13.7g} {e * eV:^13.7g}" for e in self.excitation_energy) + columns.append(TableColumn( + header=excitation_type_name, values=values.copy(), + unit="(au) (eV)" + )) + values.clear() + # vector norm + blocks = self.matrix.axis_blocks + if block_norms and len(blocks) > 0: + values.extend(f"{dot(vec.get(blocks[0]), vec.get(blocks[0])):^9.4f}" + for vec in self.excitation_vector) + columns.append(TableColumn( + header="|v1|^2", values=values.copy(), unit="" + )) + values.clear() + if block_norms and len(blocks) > 1: + values.extend(f"{dot(vec.get(blocks[1]), vec.get(blocks[1])):^9.4f}" + for vec in self.excitation_vector) + columns.append(TableColumn( + header="|v2|^2", values=values.copy(), unit="" + )) + values.clear() + + return columns + + +class DetachedStates(ChargedExcitation): + _module = adc_ip + + def __init__(self, data, is_alpha: bool, method: str = None, + property_method: str = None): + self.is_alpha = is_alpha + super().__init__(data, method, property_method) + + if self.method.adc_type is not AdcType.IP: + raise ValueError("DetachedStates computes excited state properties" + " for IP-ADC. Got the non-IP-ADC method " + f"{self.method.name}") + + def describe(self, block_norms=True): + """ + Return a string providing a human-readable description of the class + + Parameters + ---------- + block_norms : bool, optional + Show the norms of the n particle (n+1) hole blocks of the charged + excited states, by default ``True``. + """ + assert (self.matrix.axis_blocks == ["h"] + or self.matrix.axis_blocks == ["h", "phh"]) + columns = self._describe_helper( + block_norms=block_norms, + excitation_type_name="ionization potential") + + # Format the state information: kind, spin_change, + # alpha/beta detachment, and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + spin_type = "alpha" if self.is_alpha else "beta" + state_info.append( + f"(ΔMS={self.spin_change:+.1f}), {spin_type} detachment" + ) + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + return self._describe(columns, state_info) + + +class AttachedStates(ChargedExcitation): + _module = adc_ea + + def __init__(self, data, is_alpha: bool, method: str = None, + property_method: str = None): + self.is_alpha = is_alpha + super().__init__(data, method, property_method) + + if self.method.adc_type is not AdcType.EA: + raise ValueError("DetachedStates computes excited state properties" + " for EA-ADC. Got the non-EA-ADC method " + f"{self.method.name}") + + def describe(self, block_norms=True): + """ + Return a string providing a human-readable description of the class + + Parameters + ---------- + pole_strengths : bool optional + Show oscillator strengths, by default ``True``. + + state_dipole_moments : bool, optional + Show state dipole moments, by default ``False``. + + block_norms : bool, optional + Show the norms of the n particle (n+1) hole blocks of the charged + excited states, by default ``True``. + """ + assert (self.matrix.axis_blocks == ["p"] + or self.matrix.axis_blocks == ["p", "pph"]) + columns = self._describe_helper( + block_norms=block_norms, + excitation_type_name="electron affinity") + + # Format the state information: kind, spin_change, + # alpha/beta detachment, and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + spin_type = "alpha" if self.is_alpha else "beta" + state_info.append( + f"(ΔMS={self.spin_change:+.1f}), {spin_type} attachment" + ) + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + return self._describe(columns, state_info) diff --git a/adcc/ElectronicStates.py b/adcc/ElectronicStates.py index 417daf88..fd5d4b31 100644 --- a/adcc/ElectronicStates.py +++ b/adcc/ElectronicStates.py @@ -72,7 +72,8 @@ def __init__(self, data, method: str = None, self._timed_objects.append((datakey, data)) # Copy some optional attributes - for optattr in ["converged", "spin_change", "kind", "n_iter"]: + for optattr in ["converged", "spin_change", "kind", "n_iter", + "is_alpha"]: if hasattr(data, optattr): setattr(self, optattr, getattr(data, optattr)) @@ -112,7 +113,7 @@ def __init__(self, data, method: str = None, self._excitation_energy_uncorrected = \ data.excitation_energy.copy() if hasattr(data, "excitation_energy_uncorrected"): - self._excitation_energy_uncorrected =\ + self._excitation_energy_uncorrected = \ data.excitation_energy_uncorrected.copy() if hasattr(data, "excitation_vector"): self._excitation_vector = data.excitation_vector @@ -460,7 +461,7 @@ def convert_x_units(spectrum: Spectrum): plots = spectrum.plot(style="discrete", **kwargs) return plots - def _describe(self, columns: list["TableColumn"]): + def _describe(self, columns: list["TableColumn"], state_info: list[str]): """ Return a string providing a human-readable description of the class @@ -486,25 +487,11 @@ def _describe(self, columns: list["TableColumn"]): columns[-1] = columns[-1].with_width(new_width) table_width = corr_width - # - Format the header # Format the method method = self.method.name if self.property_method != self.method: method += f" ({self.property_method.name})" - # Format the state information: kind, spin_change and convergence - state_info = [] - if hasattr(self, "kind") and self.kind: - state_info.append(self.kind) - if hasattr(self, "spin_change") and self.spin_change is not None and \ - self.spin_change != 0: - state_info.append(f"(ΔMS={self.spin_change:+2d})") - if hasattr(self, "converged"): - conv = "converged" if self.converged else "NOT CONVERGED" - if state_info: # add separator to previous entry - state_info[-1] += "," - state_info.append(conv) - state_info = " ".join(state_info) - # actually format the header + # Format the header if table_width > len(method) + 4: header = ( # -4 for the spaces f"{method:s} {state_info:>{str(table_width - len(method) - 4)}s}" @@ -767,6 +754,38 @@ def format(self, amplitude: AmplitudeVector) -> list[str]: + spin_coeff_gap + self.value_format ) } + elif self.matrix.axis_blocks == ["h"]: + formats = {"o": ( + "{} -> " + idx_spin_gap + "{}->" + + spin_coeff_gap + self.value_format + )} + elif self.matrix.axis_blocks == ["h", "phh"]: + formats = { + "o": ( + empty_idx + " {} -> " + empty_idx + idx_spin_gap + + " {}-> " + spin_coeff_gap + self.value_format + ), + "oov": ( + "{} {} -> {}" + idx_spin_gap + "{}{}->{}" + + spin_coeff_gap + self.value_format + ) + } + elif self.matrix.axis_blocks == ["p"]: + formats = {"v": ( + " -> {}" + idx_spin_gap + "->{}" + + spin_coeff_gap + self.value_format + )} + elif self.matrix.axis_blocks == ["p", "pph"]: + formats = { + "v": ( + empty_idx + " -> {} " + empty_idx + idx_spin_gap + + " ->{} " + spin_coeff_gap + self.value_format + ), + "ovv": ( + "{} -> {} {}" + idx_spin_gap + + "{}->{}{}" + spin_coeff_gap + self.value_format + ) + } else: raise NotImplementedError("Unknown ADC matrix structure") diff --git a/adcc/ExcitedStates.py b/adcc/ExcitedStates.py index fae6c727..757d019b 100644 --- a/adcc/ExcitedStates.py +++ b/adcc/ExcitedStates.py @@ -155,7 +155,7 @@ def describe(self, oscillator_strengths=True, rotatory_strengths=False, unit="x(au) y(au) z(au) abs(au)" )) values.clear() - values.clear() + # values if ssq and not self.reference_state.restricted: values.extend(f"{ssq:^9.4f}" for ssq in self.state_ssq) @@ -163,7 +163,24 @@ def describe(self, oscillator_strengths=True, rotatory_strengths=False, header="", values=values.copy(), unit="(au)" )) values.clear() - return self._describe(columns) + + # Format the state information: kind, spin_change and convergence + state_info = [] + if hasattr(self, "kind") and self.kind: + state_info.append(self.kind) + if hasattr(self, "spin_change") and self.spin_change is not None and \ + self.spin_change != 0: + # For PP, spin_change can only be integer values + spin_change = int(self.spin_change) + state_info.append(f"(ΔMS={spin_change:+2d})") + if hasattr(self, "converged"): + conv = "converged" if self.converged else "NOT CONVERGED" + if state_info: # add separator to previous entry + state_info[-1] += "," + state_info.append(conv) + state_info = " ".join(state_info) + + return self._describe(columns, state_info) def to_qcvars(self, properties=False, recurse=False): """ diff --git a/adcc/__init__.py b/adcc/__init__.py index 67208921..8283e053 100644 --- a/adcc/__init__.py +++ b/adcc/__init__.py @@ -36,6 +36,8 @@ from .memory_pool import memory_pool from .State2States import State2States from .ExcitedStates import ExcitedStates +from .ChargedExcitations import (ChargedExcitation, DetachedStates, + AttachedStates) from .Excitation import Excitation from .ElectronicTransition import ElectronicTransition from .DataHfProvider import DataHfProvider, DictHfProvider @@ -49,8 +51,9 @@ from .opt_einsum_integration import register_with_opt_einsum # This has to be the last set of import -from .guess import (guess_symmetries, guess_zero, guesses_any, guesses_singlet, - guesses_spin_flip, guesses_triplet) +from .guess import (guess_symmetries, guess_zero, guesses_any, + guesses_singlet, guesses_spin_flip, guesses_triplet, + guesses_doublet) from .workflow import run_adc from .exceptions import InputError @@ -61,13 +64,17 @@ "linear_combination", "zeros_like", "direct_sum", "memory_pool", "set_n_threads", "get_n_threads", "AmplitudeVector", "HartreeFockProvider", "ExcitedStates", "State2States", + "ChargedExcitation", "DetachedStates", "AttachedStates", "Excitation", "ElectronicTransition", "Tensor", "DictHfProvider", "DataHfProvider", "OneParticleOperator", "OneParticleDensity", "TwoParticleOperator", "TwoParticleDensity", "OperatorSymmetry", "guesses_singlet", "guesses_triplet", "guesses_any", - "guess_symmetries", "guesses_spin_flip", "guess_zero", "LazyMp", + "guesses_doublet", "guess_symmetries", "guesses_spin_flip", + "guess_zero", "LazyMp", "adc0", "cis", "adc1", "adc2", "adc2x", "adc3", "cvs_adc0", "cvs_adc1", "cvs_adc2", "cvs_adc2x", "cvs_adc3", + "ip_adc0", "ip_adc1", "ip_adc2", "ip_adc2x", "ip_adc3", + "ea_adc0", "ea_adc1", "ea_adc2", "ea_adc2x", "ea_adc3", "banner"] __version__ = "0.18.0" @@ -146,6 +153,56 @@ def cvs_adc3(*args, **kwargs): return run_adc(*args, **kwargs, method="cvs-adc3") +@with_runadc_doc +def ip_adc0(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc0") + + +@with_runadc_doc +def ip_adc1(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc1") + + +@with_runadc_doc +def ip_adc2(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc2") + + +@with_runadc_doc +def ip_adc2x(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc2x") + + +@with_runadc_doc +def ip_adc3(*args, **kwargs): + return run_adc(*args, **kwargs, method="ip-adc3") + + +@with_runadc_doc +def ea_adc0(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc0") + + +@with_runadc_doc +def ea_adc1(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc1") + + +@with_runadc_doc +def ea_adc2(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc2") + + +@with_runadc_doc +def ea_adc2x(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc2x") + + +@with_runadc_doc +def ea_adc3(*args, **kwargs): + return run_adc(*args, **kwargs, method="ea-adc3") + + def banner(colour=sys.stdout.isatty()): """Return a nice banner describing adcc and its components diff --git a/adcc/adc_ea/__init__.py b/adcc/adc_ea/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/adcc/adc_ea/matrix.py b/adcc/adc_ea/matrix.py new file mode 100644 index 00000000..ac2fcd93 --- /dev/null +++ b/adcc/adc_ea/matrix.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt +from collections import namedtuple + +from adcc import block as b +from adcc.functions import direct_sum, einsum +from adcc.Intermediates import Intermediates, register_as_intermediate +from adcc.AmplitudeVector import AmplitudeVector + +# +# An explanation of the dispatch structure can be found in `adc_pp/matrix.py` +# +__all__ = ["block"] + +AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"]) + + +def block(ground_state, spaces, order, variant=None, intermediates=None): + """ + Gets ground state, potentially intermediates, spaces (p, pph and so on) + and the perturbation theory order for the block, + variant is "cvs" or sth like that. + + It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space, + while one would probably want in the long run that one can have an "o2" space, + but not do CVS. + """ + reference_state = ground_state.reference_state + if intermediates is None: + intermediates = Intermediates(ground_state) + + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) + + if fn not in globals(): + raise ValueError("Could not dispatch: " + f"spaces={spaces} order={order} variant={variant}. " + "Probably the secular matrix is not implemented for " + "the requested method.") + return globals()[fn](reference_state, ground_state, intermediates) + + +# +# 0th order main +# +def block_p_p_0(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", hf.fvv, ampl.p)) + diagonal = AmplitudeVector(p=hf.fvv.diagonal()) + return AdcBlock(apply, diagonal) + + +def diagonal_pph_pph_0(hf): + res = direct_sum("-i+a+b->iab", + hf.foo.diagonal(), hf.fvv.diagonal(), hf.fvv.diagonal()) + return AmplitudeVector(pph=res.symmetrise(1, 2)) + + +def block_pph_pph_0(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(pph=( + - einsum("jab,ij->iab", ampl.pph, hf.foo) + + 2 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2) + )) + return AdcBlock(apply, diagonal_pph_pph_0(hf)) + + +# +# 1st order main +# +def block_p_p_1(hf, mp, intermediates): + # Same as ADC(0) + return block_p_p_0(hf, mp, intermediates) + + +def diagonal_pph_pph_1(hf): + return AmplitudeVector(pph=( + + 2.0 * direct_sum( + "-ia+b->iab", einsum("iaia->ia", hf.ovov), hf.fvv.diagonal()) + + 1.0 * direct_sum( + "-i+ab->iab", hf.foo.diagonal(), einsum("abab->ab", hf.vvvv)) + ).symmetrise(1, 2)) + + +def block_pph_pph_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(pph=( + - einsum("jab,ij->iab", ampl.pph, hf.foo) + + 2.0 * einsum("ac,icb->iab", hf.fvv, ampl.pph).antisymmetrise(1, 2) + + 0.5 * einsum("abcd,icd->iab", hf.vvvv, ampl.pph) + - 2.0 * einsum("icka,kcb->iab", hf.ovov, ampl.pph).antisymmetrise(1, 2) + )) + return AdcBlock(apply, diagonal_pph_pph_1(hf)) + + +# +# 1st order coupling +# +def block_p_pph_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(p=( + - 1 / sqrt(2) * einsum("jabc,jbc->a", hf.ovvv, ampl.pph))) + return AdcBlock(apply, 0) + + +def block_pph_p_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(pph=( + - 1 / sqrt(2) * einsum("icab,c->iab", hf.ovvv, ampl.p))) + return AdcBlock(apply, 0) + + +# +# 2nd order main +# +def block_p_p_2(hf, mp, intermediates): + # Intermediate can be found in 'adc_pp/matrix.py' + i1 = intermediates.adc2_i1 + diagonal = AmplitudeVector(p=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p)) + return AdcBlock(apply, diagonal) + + +# +# 2nd order coupling +# +def block_p_pph_2(hf, mp, intermediates): + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = - intermediates.adc3_pib + + def apply(ampl): + return AmplitudeVector(p=( + + 1 / sqrt(2) * einsum("jabc,jbc->a", i2, ampl.pph))) + return AdcBlock(apply, 0) + + +def block_pph_p_2(hf, mp, intermediates): + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = - intermediates.adc3_pib + + def apply(ampl): + return AmplitudeVector(pph=( + + 1 / sqrt(2) * einsum("icab,c->iab", i2, ampl.p))) + return AdcBlock(apply, 0) + + +# +# 3rd order main +# +def block_p_p_3(hf, mp, intermediates): + i1 = intermediates.adc3_ea_i1 + diagonal = AmplitudeVector(p=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(p=einsum("ab,b->a", i1, ampl.p)) + return AdcBlock(apply, diagonal) + + +# +# Intermediates +# + +@register_as_intermediate +def adc3_ea_i1(hf, mp, intermediates): + return ( + hf.fvv + ( + + 0.5 * einsum("ijac,ijbc->ab", mp.t2oo, hf.oovv) + - 0.25 * einsum("ijbc,ijac->ab", mp.t2oo, mp.t2eri(b.oovv, b.oo)) + + 0.5 * einsum("ijbc,ijca->ab", mp.t2oo, mp.t2eri(b.oovv, b.vv)) + + einsum("ijbc,jiac->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + - 2 * einsum("ijbc,jica->ab", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + ).symmetrise() + + intermediates.sigma_inf_vv + ) + + +@register_as_intermediate +def sigma_inf_vv(hf, mp, intermediates): + # Static self-energy, oo part \Sigma_{ij}(\infty) + p0 = mp.mp2_diffdm + return (einsum("iajb,ij->ab", hf.ovov, p0.oo) + + 2 * einsum("iacb,ic->ab", hf.ovvv, p0.ov) + + einsum("acbd,cd->ab", hf.vvvv, p0.vv)).symmetrise() diff --git a/adcc/adc_ea/util.py b/adcc/adc_ea/util.py new file mode 100644 index 00000000..b1409749 --- /dev/null +++ b/adcc/adc_ea/util.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + + +def check_singles_amplitudes(spaces, *amplitudes): + check_have_singles_block(*amplitudes) + check_singles_subspaces(spaces, *amplitudes) + + +def check_doubles_amplitudes(spaces, *amplitudes): + check_have_doubles_block(*amplitudes) + check_doubles_subspaces(spaces, *amplitudes) + + +def check_have_singles_block(*amplitudes): + if any("p" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(0) level and " + "beyond expects an excitation amplitude with a " + "singles part.") + + +def check_have_doubles_block(*amplitudes): + if any("pph" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(2) level and " + "beyond expects an excitation amplitude with a " + "singles and a doubles part.") + + +def check_singles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u1 = amplitude.p + if u1.subspaces != spaces: + raise ValueError("Mismatch in subspaces singles part " + f"(== {u1.subspaces}), where {spaces} " + "was expected.") + + +def check_doubles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u2 = amplitude.pph + if u2.subspaces != spaces: + raise ValueError("Mismatch in subspaces doubles part " + f"(== {u2.subspaces}), where " + f"{spaces} was expected.") diff --git a/adcc/adc_ip/__init__.py b/adcc/adc_ip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/adcc/adc_ip/matrix.py b/adcc/adc_ip/matrix.py new file mode 100644 index 00000000..74ca2814 --- /dev/null +++ b/adcc/adc_ip/matrix.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- +from math import sqrt +from collections import namedtuple + +from adcc import block as b +from adcc.functions import direct_sum, einsum +from adcc.Intermediates import Intermediates, register_as_intermediate +from adcc.AmplitudeVector import AmplitudeVector + +# +# An explanation of the dispatch structure can be found in `adc_pp/matrix.py` +# +__all__ = ["block"] + +AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"]) + + +def block(ground_state, spaces, order, variant=None, intermediates=None): + """ + Gets ground state, potentially intermediates, spaces (h, phh and so on) + and the perturbation theory order for the block, + variant is "cvs" or sth like that. + + It is assumed largely, that CVS is equivalent to mp.has_core_occupied_space, + while one would probably want in the long run that one can have an "o2" space, + but not do CVS. + """ + reference_state = ground_state.reference_state + if intermediates is None: + intermediates = Intermediates(ground_state) + + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) + + if fn not in globals(): + raise ValueError("Could not dispatch: " + f"spaces={spaces} order={order} variant={variant}. " + "Probably the secular matrix is not implemented for " + "the requested method.") + return globals()[fn](reference_state, ground_state, intermediates) + + +# +# 0th order main +# +def block_h_h_0(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(h=-einsum("ij,j->i", hf.foo, ampl.h)) + diagonal = AmplitudeVector(h=-hf.foo.diagonal()) + return AdcBlock(apply, diagonal) + + +def diagonal_phh_phh_0(hf): + fCC = hf.fcc if hf.has_core_occupied_space else hf.foo + res = direct_sum("-i-J+a->iJa", + hf.foo.diagonal(), fCC.diagonal(), hf.fvv.diagonal()) + return AmplitudeVector(phh=res) + + +def block_phh_phh_0(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(phh=( + + einsum("ab,ijb->ija", hf.fvv, ampl.phh) + - 2 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1) + )) + return AdcBlock(apply, diagonal_phh_phh_0(hf)) + + +# +# 1st order main +# +def block_h_h_1(hf, mp, intermediates): + # Same as ADC(0) + return block_h_h_0(hf, mp, intermediates) + + +def diagonal_phh_phh_1(hf): + return AmplitudeVector(phh=( + - 2.0 * direct_sum( + "i+ja->ija", hf.foo.diagonal(), einsum("iaia->ia", hf.ovov)) + + 1.0 * direct_sum( + "ij+a->ija", einsum("ijij->ij", hf.oooo), hf.fvv.diagonal()) + ).symmetrise(0, 1)) + + +def block_phh_phh_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(phh=( + + einsum("ac,ijc->ija", hf.fvv, ampl.phh) + - 2.0 * einsum("ik,kja->ija", hf.foo, ampl.phh).antisymmetrise(0, 1) + + 0.5 * einsum("ijkl,kla->ija", hf.oooo, ampl.phh) + - 2.0 * einsum("kaic,kjc->ija", hf.ovov, ampl.phh).antisymmetrise(0, 1) + )) + return AdcBlock(apply, diagonal_phh_phh_1(hf)) + + +# +# 1st order coupling +# +def block_h_phh_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(h=( + + 1 / sqrt(2) * einsum("jkib,jkb->i", hf.ooov, ampl.phh))) + return AdcBlock(apply, 0) + + +def block_phh_h_1(hf, mp, intermediates): + def apply(ampl): + return AmplitudeVector(phh=( + + 1 / sqrt(2) * einsum("ijka,k->ija", hf.ooov, ampl.h))) + return AdcBlock(apply, 0) + + +# +# 2nd order main +# +def block_h_h_2(hf, mp, intermediates): + # Intermediate can be found in 'adc_pp/matrix.py' + i1 = - intermediates.adc2_i2 + diagonal = AmplitudeVector(h=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h)) + return AdcBlock(apply, diagonal) + + +# +# 2nd order coupling +# +def block_h_phh_2(hf, mp, intermediates): + # M_{12} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = intermediates.adc3_pia + + def apply(ampl): + return AmplitudeVector(h=( + + 1 / sqrt(2) * einsum("jkib,jkb->i", i2, ampl.phh))) + return AdcBlock(apply, 0) + + +def block_phh_h_2(hf, mp, intermediates): + # M_{21} + # Intermediate can be found in 'adc_pp/matrix.py' + i2 = intermediates.adc3_pia + + def apply(ampl): + return AmplitudeVector(phh=( + + 1 / sqrt(2) * einsum("ijka,k->ija", i2, ampl.h))) + return AdcBlock(apply, 0) + + +# +# 3rd order main +# +def block_h_h_3(hf, mp, intermediates): + # M_{11} + i1 = intermediates.adc3_ip_i1 + diagonal = AmplitudeVector(h=i1.diagonal()) + + def apply(ampl): + return AmplitudeVector(h=einsum("ij,j->i", i1, ampl.h)) + return AdcBlock(apply, diagonal) + + +# +# Intermediates +# + +@register_as_intermediate +def adc3_ip_i1(hf, mp, intermediates): + return ( + - hf.foo + ( + + 0.5 * einsum("ikab,jkab->ij", mp.t2oo, hf.oovv) + - 0.25 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.vv)) + + 0.5 * einsum("jkab,ikba->ij", mp.t2oo, mp.t2eri(b.oovv, b.oo)) + + einsum("jkab,kiab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + - 2 * einsum("jkab,ikab->ij", mp.t2oo, mp.t2eri(b.oovv, b.ov)) + ).symmetrise() + - intermediates.sigma_inf_oo + ) + + +@register_as_intermediate +def sigma_inf_oo(hf, mp, intermediates): + # Static self-energy, oo part \Sigma_{ij}(\infty) + p0 = mp.mp2_diffdm + return (einsum("ikjl,kl->ij", hf.oooo, p0.oo) + + 2 * einsum("ikja,ka->ij", hf.ooov, p0.ov) + + einsum("iajb,ab->ij", hf.ovov, p0.vv)).symmetrise() diff --git a/adcc/adc_ip/util.py b/adcc/adc_ip/util.py new file mode 100644 index 00000000..3ef04258 --- /dev/null +++ b/adcc/adc_ip/util.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + + +def check_singles_amplitudes(spaces, *amplitudes): + check_have_singles_block(*amplitudes) + check_singles_subspaces(spaces, *amplitudes) + + +def check_doubles_amplitudes(spaces, *amplitudes): + check_have_doubles_block(*amplitudes) + check_doubles_subspaces(spaces, *amplitudes) + + +def check_have_singles_block(*amplitudes): + if any("h" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(0) level and " + "beyond expects an excitation amplitude with a " + "singles part.") + + +def check_have_doubles_block(*amplitudes): + if any("phh" not in amplitude.keys() for amplitude in amplitudes): + raise ValueError("ADC(2) level and " + "beyond expects an excitation amplitude with a " + "singles and a doubles part.") + + +def check_singles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u1 = amplitude.h + if u1.subspaces != spaces: + raise ValueError("Mismatch in subspaces singles part " + f"(== {u1.subspaces}), where {spaces} " + "was expected.") + + +def check_doubles_subspaces(spaces, *amplitudes): + for amplitude in amplitudes: + u2 = amplitude.phh + if u2.subspaces != spaces: + raise ValueError("Mismatch in subspaces doubles part " + f"(== {u2.subspaces}), where " + f"{spaces} was expected.") diff --git a/adcc/adc_pp/matrix.py b/adcc/adc_pp/matrix.py index f516d705..56085fe2 100644 --- a/adcc/adc_pp/matrix.py +++ b/adcc/adc_pp/matrix.py @@ -60,24 +60,12 @@ def block(ground_state, spaces, order, variant=None, intermediates=None): while one would probably want in the long run that one can have an "o2" space, but not do CVS. """ - if isinstance(variant, str): - variant = [variant] - elif variant is None: - variant = [] reference_state = ground_state.reference_state if intermediates is None: intermediates = Intermediates(ground_state) - if ground_state.has_core_occupied_space and "cvs" not in variant: - raise ValueError("Cannot run a general (non-core-valence approximated) " - "ADC method on top of a ground state with a " - "core-valence separation.") - if not ground_state.has_core_occupied_space and "cvs" in variant: - raise ValueError("Cannot run a core-valence approximated ADC method on " - "top of a ground state without a " - "core-valence separation.") - - fn = "_".join(["block"] + variant + spaces + [str(order)]) + fn = b.get_block_name(spaces, order, variant, + ground_state.has_core_occupied_space) if fn not in globals(): raise ValueError("Could not dispatch: " diff --git a/adcc/block.py b/adcc/block.py index f5f0ae96..b5a9d52f 100644 --- a/adcc/block.py +++ b/adcc/block.py @@ -176,3 +176,24 @@ def invert_transpose_tuple(p: tuple[int, ...]) -> tuple[int, ...]: factor *= -1 transpose = invert_transpose_tuple(transpose) return (canonical_block, factor, transpose) + + +def get_block_name(spaces, order, variant, has_core_occupied_space): + """ + Assembles name of block function with variant, spaces, and order. + """ + if isinstance(variant, str): + variant = [variant] + elif variant is None: + variant = [] + + if has_core_occupied_space and "cvs" not in variant: + raise ValueError("Cannot run a general (non-core-valence approximated) " + "ADC method on top of a ground state with a " + "core-valence separation.") + if not has_core_occupied_space and "cvs" in variant: + raise ValueError("Cannot run a core-valence approximated ADC method on " + "top of a ground state without a " + "core-valence separation.") + + return "_".join(["block"] + variant + spaces + [str(order)]) diff --git a/adcc/guess/__init__.py b/adcc/guess/__init__.py index b2690876..05c5e937 100644 --- a/adcc/guess/__init__.py +++ b/adcc/guess/__init__.py @@ -22,25 +22,30 @@ ## --------------------------------------------------------------------- from .guess_zero import guess_symmetries, guess_zero from .guesses_from_diagonal import guesses_from_diagonal +from .util import estimate_n_guesses, determine_spin_change +from ..AdcMethod import AdcType __all__ = ["guess_zero", "guesses_from_diagonal", + "get_spin_block_symmetrisation", "guesses_doublet", "guesses_singlet", "guesses_triplet", "guesses_any", - "guesses_spin_flip", "guess_symmetries"] + "guesses_spin_flip", "guess_symmetries", + "estimate_n_guesses", "determine_spin_change"] -def guess_kwargs_kind(kind): +def get_spin_block_symmetrisation(kind: str) -> str: """ Return the kwargs required to be passed to `guesses_from_diagonal` to computed states of the passed excitation `kind`. """ - kwargsmap = dict( - singlet=dict(spin_block_symmetrisation="symmetric", spin_change=0), - triplet=dict(spin_block_symmetrisation="antisymmetric", spin_change=0), - spin_flip=dict(spin_block_symmetrisation="none", spin_change=-1), - any=dict(spin_block_symmetrisation="none", spin_change=0), - ) + symmetrisation = { + "singlet": "symmetric", + "doublet": "none", + "triplet": "antisymmetric", + "spin_flip": "none", + "any": "none" + } try: - return kwargsmap[kind] + return symmetrisation[kind] except KeyError: raise ValueError(f"Kind not known: {kind}") @@ -50,15 +55,46 @@ def guesses_singlet(matrix, n_guesses, block="ph", **kwargs): Obtain guesses for computing singlet states by inspecting the passed ADC matrix. - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("singlet"), **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=0, + spin_block_symmetrisation=get_spin_block_symmetrisation("singlet"), + **kwargs + ) + + +def guesses_doublet(matrix, n_guesses, block="h", is_alpha=True, **kwargs): + """ + Obtain guesses for computing doublet states by inspecting the passed + ADC matrix. + + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + if matrix.method.adc_type is AdcType.IP: + spin_change = -0.5 + elif matrix.method.adc_type is AdcType.EA: + spin_change = 0.5 + else: + raise ValueError(f"Invalid doublet ADC type: {matrix.method.adc_type}") + return guesses_from_diagonal( + matrix, n_guesses, block=block, + is_alpha=is_alpha, spin_change=spin_change, + spin_block_symmetrisation=get_spin_block_symmetrisation("doublet"), + **kwargs + ) def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): @@ -66,18 +102,21 @@ def guesses_triplet(matrix, n_guesses, block="ph", **kwargs): Obtain guesses for computing triplet states by inspecting the passed ADC matrix. - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("triplet"), **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=0, + spin_block_symmetrisation=get_spin_block_symmetrisation("triplet"), + **kwargs + ) -# guesses for computing any state (singlet or triplet) +# guesses for computing any state (excluding spin-flip states) guesses_any = guesses_from_diagonal @@ -86,12 +125,15 @@ def guesses_spin_flip(matrix, n_guesses, block="ph", **kwargs): Obtain guesses for computing spin-flip states by inspecting the passed ADC matrix. - matrix The matrix for which guesses are to be constructed - n_guesses The number of guesses to be searched for. Less number of - vectors are returned if this many could not be found. - block Diagonal block to use for obtaining the guesses - (typically "ph" or "pphh"). - kwargs Any other argument understood by guesses_from_diagonal. + matrix The matrix for which guesses are to be constructed + n_guesses The number of guesses to be searched for. Less number of + vectors are returned if this many could not be found. + block Diagonal block to use for obtaining the guesses + (typically "ph" or "pphh"). + kwargs Any other argument understood by guesses_from_diagonal. """ - return guesses_from_diagonal(matrix, n_guesses, block=block, - **guess_kwargs_kind("spin_flip"), **kwargs) + return guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=-1, + spin_block_symmetrisation=get_spin_block_symmetrisation("spin_flip"), + **kwargs + ) diff --git a/adcc/guess/guess_zero.py b/adcc/guess/guess_zero.py index 6378c7fe..653e5eab 100644 --- a/adcc/guess/guess_zero.py +++ b/adcc/guess/guess_zero.py @@ -23,6 +23,7 @@ from adcc import AmplitudeVector, Symmetry, Tensor from ..AdcMatrix import AdcMatrixlike +from ..AdcMethod import AdcType def guess_zero(matrix, spin_change=0, spin_block_symmetrisation="none"): @@ -83,28 +84,25 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"): raise ValueError("Only integer or half-integer spin_change is allowed. " "You passed {}".format(spin_change)) - max_spin_change = 0 - if "ph" in matrix.axis_blocks: - max_spin_change = 1 - if "pphh" in matrix.axis_blocks: - max_spin_change = 2 - if "ppphhh" in matrix.axis_blocks: - max_spin_change = 3 - if spin_change > max_spin_change: - raise ValueError("spin_change for singles guesses may only be in the " - f"range [{-max_spin_change}, {max_spin_change}] and " - f"not {spin_change}.") + max_spin_change = 0.5 * len(matrix.axis_blocks[-1]) + valid_spin_changes = [ + max_spin_change - i for i in range(int(2 * max_spin_change + 1))] + + if spin_change not in valid_spin_changes: + raise ValueError("spin_change may only be one of " + f"{valid_spin_changes}, and not {spin_change}.") symmetries = {} - if "ph" in matrix.axis_blocks: - symmetries["ph"] = guess_symmetry_singles( - matrix, spin_change=spin_change, - spin_block_symmetrisation=spin_block_symmetrisation - ) - if "pphh" in matrix.axis_blocks: - symmetries["pphh"] = guess_symmetry_doubles( + singles = matrix.axis_blocks[0] + symmetries[singles] = guess_symmetry_singles( + matrix, spin_change=spin_change, + spin_block_symmetrisation=spin_block_symmetrisation, block=singles + ) + if len(matrix.axis_blocks) >= 2: + doubles = matrix.axis_blocks[1] + symmetries[doubles] = guess_symmetry_doubles( matrix, spin_change=spin_change, - spin_block_symmetrisation=spin_block_symmetrisation + spin_block_symmetrisation=spin_block_symmetrisation, block=doubles ) if "ppphhh" in matrix.axis_blocks: symmetries["ppphhh"] = guess_symmetry_triples( @@ -115,24 +113,36 @@ def guess_symmetries(matrix, spin_change=0, spin_block_symmetrisation="none"): def guess_symmetry_singles(matrix, spin_change=0, - spin_block_symmetrisation="none"): - symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces["ph"])) + spin_block_symmetrisation="none", block="ph"): + symmetry = Symmetry(matrix.mospaces, "".join(matrix.axis_spaces[block])) symmetry.irreps_allowed = ["A"] + if spin_change != 0 and spin_block_symmetrisation != "none": raise NotImplementedError("spin_symmetrisation != 'none' only " "implemented for spin_change == 0") - elif spin_block_symmetrisation == "symmetric": - symmetry.spin_block_maps = [("aa", "bb", 1)] - symmetry.spin_blocks_forbidden = ["ab", "ba"] - elif spin_block_symmetrisation == "antisymmetric": - symmetry.spin_block_maps = [("aa", "bb", -1)] + + if spin_block_symmetrisation in ("symmetric", "antisymmetric"): + # PP-ADC + fac = 1 if spin_block_symmetrisation == "symmetric" else -1 + symmetry.spin_block_maps = [("aa", "bb", fac)] symmetry.spin_blocks_forbidden = ["ab", "ba"] + + elif ( + matrix.method.adc_type is not AdcType.PP + and matrix.reference_state.restricted + ): + # IP- and EA-ADC + # attach/detach alpha electron + # forbidden beta blocks (["a"]) not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["b"] + return symmetry def guess_symmetry_doubles(matrix, spin_change=0, - spin_block_symmetrisation="none"): - spaces_d = matrix.axis_spaces["pphh"] + spin_block_symmetrisation="none", block="pphh"): + spaces_d = matrix.axis_spaces[block] symmetry = Symmetry(matrix.mospaces, "".join(spaces_d)) symmetry.irreps_allowed = ["A"] @@ -140,34 +150,82 @@ def guess_symmetry_doubles(matrix, spin_change=0, raise NotImplementedError("spin_symmetrisation != 'none' only " "implemented for spin_change == 0") - if spin_change == 0 \ - and spin_block_symmetrisation in ("symmetric", "antisymmetric"): - fac = 1 if spin_block_symmetrisation == "symmetric" else -1 - # Spin mapping between blocks where alpha and beta are just mirrored - symmetry.spin_block_maps = [("aaaa", "bbbb", fac), - ("abab", "baba", fac), - ("abba", "baab", fac)] + if matrix.method.adc_type is AdcType.PP: + # PP-ADC + if spin_block_symmetrisation in ("symmetric", "antisymmetric"): + fac = 1 if spin_block_symmetrisation == "symmetric" else -1 + # Spin mapping between blocks where alpha and beta are mirrored + symmetry.spin_block_maps = [("aaaa", "bbbb", fac), + ("abab", "baba", fac), + ("abba", "baab", fac)] - # Mark blocks which change spin as forbidden - symmetry.spin_blocks_forbidden = ["aabb", # spin_change +2 - "bbaa", # spin_change -2 - "aaab", # spin_change +1 - "aaba", # spin_change +1 - "abaa", # spin_change -1 - "baaa", # spin_change -1 - "abbb", # spin_change +1 - "babb", # spin_change +1 - "bbab", # spin_change -1 - "bbba"] # spin_change -1 + # Mark blocks which change spin as forbidden + symmetry.spin_blocks_forbidden = ["aabb", # spin_change -2 + "bbaa", # spin_change +2 + "aaab", # spin_change -1 + "aaba", # spin_change -1 + "abaa", # spin_change +1 + "baaa", # spin_change +1 + "abbb", # spin_change -1 + "babb", # spin_change -1 + "bbab", # spin_change +1 + "bbba"] # spin_change +1 + + # Add index permutation symmetry: + permutations = ["ijab"] + if spaces_d[0] == spaces_d[1]: + permutations.append("-jiab") + if spaces_d[2] == spaces_d[3]: + permutations.append("-ijba") + if len(permutations) > 1: + symmetry.permutations = permutations + + elif matrix.method.adc_type is AdcType.IP: + # IP-ADC + # No spin mapping between blocks + symmetry.spin_block_maps = [] + + # Mark blocks which change spin incorrectly as forbidden + if matrix.reference_state.restricted: # kind=doublet + # detach alpha electron + # forbidden beta ionization blocks not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["aab", # spin_change -3/2 + "bba", # spin_change +3/2 + "aba", # spin_change +1/2 + "baa", # spin_change +1/2 + "bbb"] # spin_change +1/2 + + # Add index permutation symmetry: + permutations = ["ija"] + if spaces_d[0] == spaces_d[1]: + permutations.append("-jia") + if len(permutations) > 1: + symmetry.permutations = permutations + + elif matrix.method.adc_type is AdcType.EA: + # EA-ADC + # No spin mapping between blocks + symmetry.spin_block_maps = [] + + # Mark blocks which change spin incorrectly as forbidden + if matrix.reference_state.restricted: # kind=doublet + # attach alpha electron + # forbidden beta attachment blocks not needed because for a + # restricted reference only alpha states will be computed + symmetry.spin_blocks_forbidden = ["baa", # spin_change +3/2 + "abb", # spin_change -3/2 + "aab", # spin_change -1/2 + "aba", # spin_change -1/2 + "bbb"] # spin_change -1/2 + + # Add index permutation symmetry: + permutations = ["iab"] + if spaces_d[1] == spaces_d[2]: + permutations.append("-iba") + if len(permutations) > 1: + symmetry.permutations = permutations - # Add index permutation symmetry: - permutations = ["ijab"] - if spaces_d[0] == spaces_d[1]: - permutations.append("-jiab") - if spaces_d[2] == spaces_d[3]: - permutations.append("-ijba") - if len(permutations) > 1: - symmetry.permutations = permutations return symmetry diff --git a/adcc/guess/guesses_from_diagonal.py b/adcc/guess/guesses_from_diagonal.py index 2d94f972..83aa78ca 100644 --- a/adcc/guess/guesses_from_diagonal.py +++ b/adcc/guess/guesses_from_diagonal.py @@ -28,10 +28,12 @@ from itertools import groupby from ..AdcMatrix import AdcMatrixlike +from ..AdcMethod import AdcType from .guess_zero import guess_zero -def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, +def guesses_from_diagonal(matrix, n_guesses, block="ph", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): """ @@ -45,23 +47,26 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, vectors are returned if this many could not be found. block Diagonal block to use for obtaining the guesses (typically "ph" or "pphh"). + is_alpha Is the detached/attached electron alpha spin for the + respective IP-/EA-ADC calculation. spin_change The spin change to enforce in an excitation. - Typical values are 0 (singlet/triplet/any) and -1 (spin-flip). + Typical values are 0 (singlet/triplet/any), +/- 0.5 (doublet) + and -1 (spin-flip). spin_block_symmetrisation - Symmetrisation to enforce between equivalent spin blocks, which - all yield the desired spin_change. E.g. if spin_change == 0, - then both the alpha->alpha and beta->beta blocks of the singles - part of the excitation vector achieve a spin change of 0. - The symmetry specified with this parameter will then be imposed - between the a-a and b-b blocks. Valid values are "none", - "symmetric" and "antisymmetric", where "none" enforces - no particular symmetry. + Symmetrisation to enforce between equivalent spin blocks, + which all yield the desired spin_change. E.g. if + spin_change == 0, then both the alpha->alpha and beta->beta + blocks of the singles part of the excitation vector achieve a + spin change of 0. The symmetry specified with this parameter + will then be imposed between the a-a and b-b blocks. Valid + values are "none", "symmetric" and "antisymmetric", where + "none" enforces no particular symmetry. degeneracy_tolerance Tolerance for two entries of the diagonal to be considered degenerate, i.e. identical. max_diagonal_value - Maximal diagonal value, which is considered as a valid candidate - to form a guess. + Maximal diagonal value, which is considered as a valid + candidate to form a guess. """ if not isinstance(matrix, AdcMatrixlike): raise TypeError("matrix needs to be of type AdcMatrixlike") @@ -74,8 +79,8 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, "ADC calculations on top of restricted reference " "states.") if int(spin_change * 2) / 2 != spin_change: - raise ValueError("Only integer or half-integer spin_change is allowed. " - "You passed {}".format(spin_change)) + raise ValueError("Only integer or half-integer spin_change is allowed." + " You passed {}".format(spin_change)) if block not in matrix.axis_blocks: raise ValueError("The passed ADC matrix does not have the block '{}.'" @@ -83,14 +88,15 @@ def guesses_from_diagonal(matrix, n_guesses, block="ph", spin_change=0, if n_guesses == 0: return [] - if block == "ph": + if block in ["h", "p", "ph"]: guessfunction = guesses_from_diagonal_singles - elif block == "pphh": + elif block in ["phh", "pph", "pphh"]: guessfunction = guesses_from_diagonal_doubles else: - raise ValueError(f"Don't know how to generate guesses for block {block}") + raise ValueError("Don't know how to generate guesses for block " + f"{block}") - return guessfunction(matrix, n_guesses, spin_change, + return guessfunction(matrix, n_guesses, block, kind, is_alpha, spin_change, spin_block_symmetrisation, degeneracy_tolerance, max_diagonal_value) @@ -127,8 +133,8 @@ def spin_change(self): ("v", "a"): +0.5, # add alpha ("v", "b"): -0.5, # add beta } - return int(sum(mapping_spin_change[(space[0], spin)] - for space, spin in zip(self.subspaces, self.spin_block))) + return sum(mapping_spin_change[(space[0], spin)] + for space, spin in zip(self.subspaces, self.spin_block)) def __repr__(self): return f"({self.index} {self.value})" @@ -197,11 +203,12 @@ def telem_nospin(telem): return res -def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0, +def guesses_from_diagonal_singles(matrix, n_guesses, block="ph", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): - motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces["ph"]) + motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces[block]) if n_guesses == 0: return [] @@ -216,12 +223,12 @@ def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0, # Also it filters out too large diagonal entries (which are essentially # hopeless to give useful excitations) def pred_singles(telem): - return (ret[0].ph.is_allowed(telem.index) + return (ret[0].get(block).is_allowed(telem.index) and telem.spin_change == spin_change and abs(telem.value) <= max_diagonal_value) elements = find_smallest_matching_elements( - pred_singles, matrix.diagonal().ph, motrans, n_guesses, + pred_singles, matrix.diagonal().get(block), motrans, n_guesses, degeneracy_tolerance=degeneracy_tolerance ) if len(elements) == 0: @@ -245,25 +252,25 @@ def telem_nospin(telem): group = list(group) if len(group) == 1: # Just add the single vector - ret[ivec].ph[group[0].index] = 1.0 + ret[ivec].get(block)[group[0].index] = 1.0 ivec += 1 elif len(group) == 2: # Since these two are grouped together, their # spatial parts must be identical. # Add the positive linear combination ... - ret[ivec].ph[group[0].index] = 1 - ret[ivec].ph[group[1].index] = 1 + ret[ivec].get(block)[group[0].index] = 1 + ret[ivec].get(block)[group[1].index] = 1 ivec += 1 # ... and the negative linear combination if ivec < n_guesses: - ret[ivec].ph[group[0].index] = +1 - ret[ivec].ph[group[1].index] = -1 + ret[ivec].get(block)[group[0].index] = +1 + ret[ivec].get(block)[group[1].index] = -1 ivec += 1 else: raise AssertionError("group size > 3 should not occur " - "when setting up single guesse.") + "when setting up single guesses.") assert ivec <= n_guesses # Resize in case less guesses found than requested @@ -271,7 +278,8 @@ def telem_nospin(telem): return [evaluate(v / np.sqrt(v @ v)) for v in ret[:ivec]] -def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0, +def guesses_from_diagonal_doubles(matrix, n_guesses, block="pphh", kind=None, + is_alpha=None, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14, max_diagonal_value=1000): @@ -283,24 +291,41 @@ def guesses_from_diagonal_doubles(matrix, n_guesses, spin_change=0, spin_block_symmetrisation=spin_block_symmetrisation) for _ in range(n_guesses)] - # Build delta-Fock matrices - spaces_d = matrix.axis_spaces["pphh"] - df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2]) - df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3]) - - guesses_d = [gv.pphh for gv in ret] # Extract doubles parts spin_change_twice = int(spin_change * 2) assert spin_change_twice / 2 == spin_change - n_found = libadcc.fill_pp_doubles_guesses( - guesses_d, matrix.mospaces, df02, df13, - spin_change_twice, degeneracy_tolerance - ) + # Extract doubles parts + guesses_d = [gv.get(block) for gv in ret] + + if matrix.method.adc_type is AdcType.PP: + # PP-ADC + spaces_d = matrix.axis_spaces[block] + # Build delta-Fock matrices + df02 = matrix.ground_state.df(spaces_d[0] + spaces_d[2]) + df13 = matrix.ground_state.df(spaces_d[1] + spaces_d[3]) + n_found = libadcc.fill_pp_doubles_guesses( + guesses_d, matrix.mospaces, df02, df13, + spin_change_twice, degeneracy_tolerance + ) + else: + # IP- and EA-ADC + # Build Fock matrices and multiply occ. orbitals with -1 to invert the + # order for simplicity in the C++ code + d_o = matrix.reference_state.foo.diagonal() * (-1) + d_v = matrix.reference_state.fvv.diagonal() + doublet = (kind == "doublet") + is_restricted = matrix.reference_state.restricted + double_guess_func = {AdcType.IP: libadcc.fill_ip_doubles_guesses, + AdcType.EA: libadcc.fill_ea_doubles_guesses} + n_found = double_guess_func[matrix.method.adc_type]( + guesses_d, matrix.mospaces, d_o, d_v, is_alpha, is_restricted, + doublet, spin_change_twice, degeneracy_tolerance) + # Resize in case less guesses found than requested ret = ret[:n_found] # Filter out elements above the noted diagonal value - diagonal_elements = [ret_d.pphh.dot(matrix.diagonal().pphh * ret_d.pphh) - for ret_d in ret] + diagonal_elements = [ret_d.get(block).dot(matrix.diagonal().get(block) + * ret_d.get(block)) for ret_d in ret] return [ret[i] for (i, elem) in enumerate(diagonal_elements) if elem <= max_diagonal_value] diff --git a/adcc/guess/util.py b/adcc/guess/util.py new file mode 100644 index 00000000..9ddf59a7 --- /dev/null +++ b/adcc/guess/util.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +## --------------------------------------------------------------------- +## +## Copyright (C) 2026 by the adcc authors +## +## This file is part of adcc. +## +## adcc is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## adcc is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with adcc. If not, see . +## +## --------------------------------------------------------------------- + +from ..AdcMethod import AdcMethod, AdcType +from typing import Optional + + +def determine_spin_change(method: AdcMethod, kind: str, + is_alpha: Optional[bool] = None) -> float: + if method.adc_type is AdcType.PP: + if kind == "spin_flip": + return -1.0 + else: + return 0.0 + elif method.adc_type is AdcType.IP: + if is_alpha is None: + raise TypeError("'is_alpha' has to be True|False for IP-ADC") + return +0.5 - int(is_alpha) + elif method.adc_type is AdcType.EA: + if is_alpha is None: + raise TypeError("'is_alpha' has to be True|False for EA-ADC") + return -0.5 + int(is_alpha) + else: + raise ValueError(f"Unknown ADC type: {method.adc_type}") + + +def estimate_n_guesses(matrix, n_states, n_guesses_per_state=2, + singles_only=True) -> int: + """ + Implementation of a basic heuristic to find a good number of guess + vectors to be searched for using the find_guesses function. + Internal function called from run_adc. + + matrix ADC matrix + n_states Number of states to be computed + singles_only Try to stay withing the singles excitation space + with the number of guess vectors. + n_guesses_per_state Number of guesses to search for for each state + """ + # Try to use at least 4 or twice the number of states + # to be computed as guesses + n_guesses = n_guesses_per_state * max(2, n_states) + + if singles_only: + # Compute the maximal number of sensible singles block guesses. + # This is roughly the number of occupied alpha orbitals + # times the number of virtual alpha orbitals + # + # If the system is core valence separated, then only the + # core electrons count as "occupied". + mospaces = matrix.mospaces + sp_occ = "o2" if matrix.is_core_valence_separated else "o1" + n_virt_a = mospaces.n_orbs_alpha("v1") + n_occ_a = mospaces.n_orbs_alpha(sp_occ) + estimate = n_occ_a * n_virt_a + if ( + matrix.method.level.to_int() < 2 + and matrix.method.adc_type is not AdcType.PP + ): + # Adjustment for IP- and EA-ADC(0/1) calculations + estimate = (n_occ_a if matrix.method.adc_type is AdcType.IP + else n_virt_a) + n_guesses = min(n_guesses, estimate) + + # Adjust if we overshoot the maximal number of sensible singles block + # guesses, but make sure we get at least n_states guesses + return max(n_states, n_guesses) diff --git a/adcc/projection.py b/adcc/projection.py index d6eee258..a25fa5fc 100644 --- a/adcc/projection.py +++ b/adcc/projection.py @@ -26,7 +26,8 @@ import numpy as np -from .guess import guess_kwargs_kind, guess_symmetries +from .guess import (get_spin_block_symmetrisation, guess_symmetries, + determine_spin_change) from .Tensor import Tensor from .MoSpaces import expand_spaceargs from .Symmetry import Symmetry @@ -332,8 +333,10 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, raise ValueError("kind needs to be given if first argument is not an " "ExcitedStates object and spin symmetry setup is not " "explicitly given.") - return transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector, kind, - **guess_kwargs_kind(kind)) + return transfer_cvs_to_full( + state_matrix_cvs, matrix_full, vector, kind, + spin_change=determine_spin_change(matrix_full.method, kind), + spin_block_symmetrisation=get_spin_block_symmetrisation(kind)) if vector is None: if hasattr(state_matrix_cvs, "excitation_vector"): @@ -342,8 +345,11 @@ def transfer_cvs_to_full(state_matrix_cvs, matrix_full, vector=None, kind=None, raise ValueError("vector needs to be given if first argument is not an " "ExcitedStates object.") if isinstance(vector, list): - return [transfer_cvs_to_full(state_matrix_cvs, matrix_full, v, kind, - **guess_kwargs_kind(kind)) for v in vector] + return [transfer_cvs_to_full( + state_matrix_cvs, matrix_full, v, kind, + spin_change=determine_spin_change(matrix_full.method, kind), + spin_block_symmetrisation=get_spin_block_symmetrisation(kind) + ) for v in vector] if isinstance(state_matrix_cvs, AdcMatrixlike): mospaces_cvs = state_matrix_cvs.mospaces diff --git a/adcc/solver/explicit_symmetrisation.py b/adcc/solver/explicit_symmetrisation.py index d49e55bf..10ff0a31 100644 --- a/adcc/solver/explicit_symmetrisation.py +++ b/adcc/solver/explicit_symmetrisation.py @@ -24,6 +24,7 @@ from adcc import evaluate from adcc.AmplitudeVector import AmplitudeVector +from adcc.AdcMethod import AdcType # TODO # This interface is not that great and leads to duplicate information @@ -76,6 +77,8 @@ class IndexSpinSymmetrisation(IndexSymmetrisation): def __init__(self, matrix, enforce_spin_kind="singlet"): super().__init__(matrix) self.enforce_spin_kind = enforce_spin_kind + # Bool to distinguish IP/EA if 'spin_kind=='doublet' + self.is_ip_adc = matrix.method.adc_type is AdcType.IP def symmetrise(self, new_vectors): if isinstance(new_vectors, AmplitudeVector): @@ -87,12 +90,13 @@ def symmetrise(self, new_vectors): for vec in new_vectors: # Only work on the doubles part # the other blocks are not yet implemented - # or nothing needs to be done ("ph" block) - if "pphh" in vec.blocks: + # or nothing needs to be done ("ph"/"h"/"p" block) + if len(vec.blocks) > 1: # TODO: Note that the "d" is needed here because the C++ side # does not yet understand ph and pphh amplitude_vector_enforce_spin_kind( - vec.pphh, "d", self.enforce_spin_kind + vec.get(vec.blocks[1]), "d", self.enforce_spin_kind, + self.is_ip_adc ) return new_vectors diff --git a/adcc/tests/AdcMatrix_test.py b/adcc/tests/AdcMatrix_test.py index 6525529d..d12ba582 100644 --- a/adcc/tests/AdcMatrix_test.py +++ b/adcc/tests/AdcMatrix_test.py @@ -124,7 +124,7 @@ def test_default_block_orders(self): } assert block_orders == ref_isr2d - def test_validate_block_orders(self): + def test_validate_block_orders_pp(self): valid_block_orders = ( {"ph_ph": 0}, # adc0 {"ph_ph": 1, "ph_pphh": None, "ppphhh_ppphhh": None}, # adc1 @@ -149,7 +149,7 @@ def test_validate_block_orders(self): block_orders, AdcMethod("adc0") ) - def test_validate_space(self): + def test_validate_space_pp(self): # some valid PP-ADC methods assert AdcMatrixlike._is_valid_space( "ph", AdcMethod("adc0") @@ -171,15 +171,65 @@ def test_validate_space(self): "phh", AdcMethod("adc0") ) + def test_validate_block_orders_ip_ea(self): + # Valid IP block orders + valid_ip = ( + {"h_h": 0}, # ip-adc0 + {"h_h": 2, "h_phh": 1, "phh_h": 1, "phh_phh": 0}, # ip-adc2 + ) + for block_orders in valid_ip: + AdcMatrixlike._validate_block_orders( + block_orders, AdcMethod("ip-adc2") + ) + + # Valid EA block orders + valid_ea = ( + {"p_p": 0}, # ea-adc0 + {"p_p": 2, "p_pph": 1, "pph_p": 1, "pph_pph": 0}, # ea-adc2 + ) + for block_orders in valid_ea: + AdcMatrixlike._validate_block_orders( + block_orders, AdcMethod("ea-adc2") + ) + + # Invalid mixed blocks + with pytest.raises(ValueError): + AdcMatrixlike._validate_block_orders( + {"ph_ph": 0}, AdcMethod("ip-adc2") + ) + + def test_validate_space_ip(self): + ip = AdcMethod("ip-adc0") + assert AdcMatrixlike._is_valid_space("h", ip) + assert AdcMatrixlike._is_valid_space("phh", ip) + assert AdcMatrixlike._is_valid_space("ppphhhh", ip) + assert not AdcMatrixlike._is_valid_space("ph", ip) + assert not AdcMatrixlike._is_valid_space("p", ip) + + def test_validate_space_ea(self): + ea = AdcMethod("ea-adc0") + assert AdcMatrixlike._is_valid_space("p", ea) + assert AdcMatrixlike._is_valid_space("pph", ea) + assert AdcMatrixlike._is_valid_space("pppphhh", ea) + assert not AdcMatrixlike._is_valid_space("ph", ea) + assert not AdcMatrixlike._is_valid_space("h", ea) + h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop() -methods = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"] +pp_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"] +ip_methods = ["ip-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]] +ea_methods = ["ea-" + m for m in ["adc0", "adc2", "adc2x", "adc3"]] + +cases = [(m, c) for c in ["gen", "cvs"] for m in pp_methods] +# No CVS for IP-ADC (yet) +# Test only for is_alpha=True for simplicity +cases += [(m, c) for c in ["gen"] for m in ip_methods] +cases += [(m, c) for c in ["gen"] for m in ea_methods] # Distinct implementations of the matrix equations only exist for the cases # "gen" and "cvs". -@pytest.mark.parametrize("method", methods) -@pytest.mark.parametrize("case", ["gen", "cvs"]) +@pytest.mark.parametrize("method,case", cases) @pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"]) class TestAdcMatrix: def load_matrix_data(self, system: str, case: str, method: str) -> dict: @@ -241,16 +291,20 @@ def test_matvec(self, system: str, case: str, method: str): if matrix.reference_state.restricted: if matrix.method.adc_type is AdcType.PP: kind = "singlet" + elif matrix.method.adc_type in (AdcType.IP, AdcType.EA): + kind = "doublet" else: raise ValueError(f"Unknown adc type {matrix.method.adc_type}.") else: kind = "any" # we don't do the test for spin flip trial_vec = self.construct_trial_vec(system, case, method, kind) result = matrix @ trial_vec - assert_allclose(matdata["matvec_singles"], result.ph.to_ndarray(), + assert_allclose(matdata["matvec_singles"], + result.get(matrix.axis_blocks[0]).to_ndarray(), rtol=1e-10, atol=1e-12) if "matvec_doubles" in matdata: - assert_allclose(matdata["matvec_doubles"], result.pphh.to_ndarray(), + assert_allclose(matdata["matvec_doubles"], + result.get(matrix.axis_blocks[1]).to_ndarray(), rtol=1e-10, atol=1e-12) if "matvec_triples" in matdata: assert_allclose(matdata["matvec_triples"], result.ppphhh.to_ndarray(), @@ -266,6 +320,8 @@ def test_compute_block(self, system: str, case: str, method: str): if matrix.reference_state.restricted: if matrix.method.adc_type is AdcType.PP: kind = "singlet" + elif matrix.method.adc_type in (AdcType.IP, AdcType.EA): + kind = "doublet" else: raise ValueError(f"Unknwon adc type {matrix.method.adc_type}.") else: @@ -284,10 +340,91 @@ def test_compute_block(self, system: str, case: str, method: str): atol=1e-12 ) + def test_hermiticity(self, system, case, method): + if "cvs" in case and method == "adc4": + pytest.skip("CVS-ADC(4) not implemented") + matrix = self.construct_matrix(system, case, method) + + # Only test for Hermitian ADC variants + # (Projected matrix may not preserve symmetry fully) + spin_change = 0 + if matrix.method.adc_type is AdcType.IP: + spin_change = -0.5 + elif matrix.method.adc_type is AdcType.EA: + spin_change = 0.5 + + v = adcc.guess_zero(matrix, spin_change=spin_change) + w = adcc.guess_zero(matrix, spin_change=spin_change) + + v.set_random() + w.set_random() + + Av = matrix @ v + Aw = matrix @ w + + lhs = v.dot(Aw) + rhs = Av.dot(w) + + assert abs(lhs - rhs) < 1.5e-10 + class TestAdcMatrixInterface: - @pytest.mark.parametrize("method", methods) - @pytest.mark.parametrize("case", h2o_sto3g.cases) + @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods) + @pytest.mark.parametrize("system", ["h2o_sto3g"]) + @pytest.mark.parametrize("case", ["gen"]) # no CVS for IP/EA + def test_axis_structure_all_types(self, system, case, method): + reference_state = testdata_cache.refstate(system=system, case=case) + ground_state = adcc.LazyMp(reference_state) + + matrix = adcc.AdcMatrix(method, ground_state) + + assert matrix.ndim == 2 + assert matrix.shape[0] == matrix.shape[1] + assert len(matrix) == matrix.shape[0] + + blocks = matrix.axis_blocks + assert isinstance(blocks, list) + assert len(blocks) >= 1 + + # Block ordering must follow excitation rank + lengths = [len(b) for b in blocks] + assert lengths == sorted(lengths) + + # Axis dictionaries must match blocks + assert sorted(matrix.axis_spaces.keys(), key=len) == blocks + assert sorted(matrix.axis_lengths.keys(), key=len) == blocks + + # Validate block labels by ADC type + adc_type = matrix.method.adc_type + + if adc_type is AdcType.PP: + assert all(set(b).issubset({"p", "h"}) for b in blocks) + assert blocks[0].count("p") == 1 + assert blocks[0].count("h") == 1 + + elif adc_type is AdcType.IP: + # First block must remove one electron + assert blocks[0].count("h") == 1 + assert blocks[0].count("p") == 0 + + elif adc_type is AdcType.EA: + # First block must add one electron + assert blocks[0].count("p") == 1 + assert blocks[0].count("h") == 0 + + else: + raise AssertionError(f"Unknown ADC type {adc_type}") + + # Validate axis lengths consistency + for block in blocks: + assert matrix.axis_lengths[block] > 0 + + # Reference consistency + assert matrix.reference_state == reference_state + assert matrix.mospaces == reference_state.mospaces + + @pytest.mark.parametrize("method", pp_methods + ip_methods + ea_methods) + @pytest.mark.parametrize("case", ["gen"]) # No CVS for IP/EA @pytest.mark.parametrize("system", ["h2o_sto3g"]) def test_properties(self, system: str, case: str, method: str): if "cvs" in case and method == "adc4": @@ -302,10 +439,20 @@ def test_properties(self, system: str, case: str, method: str): assert matrix.is_core_valence_separated == ("cvs" in case) # check that the blocks are correct blocks = matrix.axis_blocks + assert isinstance(blocks, list) + assert len(blocks) >= 1 if matrix.method.adc_type is AdcType.PP: assert blocks == ( ["ph", "pphh", "ppphhh"][:matrix.method.level.to_int() // 2 + 1] ) + elif matrix.method.adc_type is AdcType.IP: + assert blocks == ( + ["h", "phh", "pphhh"][:matrix.method.level.to_int() // 2 + 1] + ) + elif matrix.method.adc_type is AdcType.EA: + assert blocks == ( + ["p", "pph", "ppphh"][:matrix.method.level.to_int() // 2 + 1] + ) else: raise NotImplementedError(f"Unknown adc type {matrix.method.adc_type}.") assert sorted(matrix.axis_spaces.keys(), key=len) == blocks @@ -347,19 +494,29 @@ def test_properties(self, system: str, case: str, method: str): assert matrix.mospaces == reference_state.mospaces assert isinstance(matrix.timer, adcc.timings.Timer) - def test_intermediates_adc2(self): + @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"]) + def test_intermediates_adc2(self, method: str): ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen")) - matrix = adcc.AdcMatrix("adc2", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) assert isinstance(matrix.intermediates, Intermediates) intermediates = Intermediates(ground_state) matrix.intermediates = intermediates assert matrix.intermediates == intermediates - def test_matvec_adc2(self): + @pytest.mark.parametrize("method", ["adc2", "ip-adc2", "ea-adc2"]) + def test_matvec_adc2(self, method: str): ground_state = adcc.LazyMp(testdata_cache.refstate("h2o_sto3g", case="gen")) - matrix = adcc.AdcMatrix("adc2", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) + blocks = matrix.axis_blocks - vectors = [adcc.guess_zero(matrix) for _ in range(3)] + spin_change = 0 + if matrix.method.adc_type is AdcType.IP: + spin_change = -0.5 + elif matrix.method.adc_type is AdcType.EA: + spin_change = 0.5 + + vectors = [ + adcc.guess_zero(matrix, spin_change=spin_change) for _ in range(3)] for vec in vectors: vec.set_random() v, w, x = vectors @@ -372,32 +529,34 @@ def test_matvec_adc2(self): # @ operator (1 vector) resv = matrix @ v diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 # @ operator (multiple vectors) resv, resw, resx = matrix @ [v, w, x] diffs = [refv - resv, refw - resw, refx - resx] for i in range(3): - assert diffs[i].ph.dot(diffs[i].ph) < 1e-12 - assert diffs[i].pphh.dot(diffs[i].pphh) < 1e-12 + assert diffs[i].get(blocks[0]).dot(diffs[i].get(blocks[0])) < 1e-12 + assert diffs[i].get(blocks[1]).dot(diffs[i].get(blocks[1])) < 1e-12 # compute matvec resv = matrix.matvec(v) diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 resv = matrix.rmatvec(v) diffv = refv - resv - assert diffv.ph.dot(diffv.ph) < 1e-12 - assert diffv.pphh.dot(diffv.pphh) < 1e-12 + assert diffv.get(blocks[0]).dot(diffv.get(blocks[0])) < 1e-12 + assert diffv.get(blocks[1]).dot(diffv.get(blocks[1])) < 1e-12 # Test apply - resv.ph = matrix.block_apply("ph_ph", v.ph) - resv.ph += matrix.block_apply("ph_pphh", v.pphh) + resv[blocks[0]] = matrix.block_apply(f"{blocks[0]}_{blocks[0]}", + v.get(blocks[0])) + resv[blocks[0]] += matrix.block_apply(f"{blocks[0]}_{blocks[1]}", + v.get(blocks[1])) refv = matrix.matvec(v) - diffv = resv.ph - refv.ph + diffv = resv.get(blocks[0]) - refv.get(blocks[0]) assert diffv.dot(diffv) < 1e-12 def test_extra_term(self): @@ -468,40 +627,47 @@ def apply(invec): @pytest.mark.parametrize("system", ["h2o_sto3g", "cn_sto3g"]) +@pytest.mark.parametrize("method", ["adc3", "ip-adc3", "ea-adc3"]) class TestAdcMatrixShifted: - def construct_matrices(self, system, shift): + def construct_matrices(self, system: str, method: str, shift: float): reference_state = testdata_cache.refstate(system, case="gen") ground_state = adcc.LazyMp(reference_state) - matrix = adcc.AdcMatrix("adc3", ground_state) + matrix = adcc.AdcMatrix(method, ground_state) shifted = AdcMatrixShifted(matrix, shift) return matrix, shifted - def test_diagonal(self, system: str): + def test_diagonal(self, system: str, method: str): shift = -0.3 - matrix, shifted = self.construct_matrices(system, shift) + matrix, shifted = self.construct_matrices(system, method, shift) - for block in ("ph", "pphh"): + for block in matrix.axis_blocks: odiag = matrix.diagonal()[block].to_ndarray() sdiag = shifted.diagonal()[block].to_ndarray() assert np.max(np.abs(sdiag - shift - odiag)) < 1e-12 - def test_matmul(self, system: str): + def test_matmul(self, system: str, method: str): shift = -0.3 - matrix, shifted = self.construct_matrices(system, shift) + matrix, shifted = self.construct_matrices(system, method, shift) + blocks = matrix.axis_blocks - vec = adcc.guess_zero(matrix) + spin_change = 0 + if matrix.method.adc_type is AdcType.IP: + spin_change = -0.5 + elif matrix.method.adc_type is AdcType.EA: + spin_change = 0.5 + + vec = adcc.guess_zero(matrix, spin_change=spin_change) vec.set_random() ores = matrix @ vec sres = shifted @ vec - assert ores.ph.describe_symmetry() == sres.ph.describe_symmetry() - assert ores.pphh.describe_symmetry() == sres.pphh.describe_symmetry() + for block in blocks: + assert ores.get(block).describe_symmetry() == sres.get( + block).describe_symmetry() - diff_s = sres.ph - ores.ph - shift * vec.ph - diff_d = sres.pphh - ores.pphh - shift * vec.pphh - assert np.max(np.abs(diff_s.to_ndarray())) < 1e-12 - assert np.max(np.abs(diff_d.to_ndarray())) < 1e-12 + diff = sres.get(block) - ores.get(block) - shift * vec.get(block) + assert np.max(np.abs(diff.to_ndarray())) < 1e-12 # TODO Test block_view, block_apply diff --git a/adcc/tests/ChargedExcitations_test.py b/adcc/tests/ChargedExcitations_test.py new file mode 100644 index 00000000..16d149d8 --- /dev/null +++ b/adcc/tests/ChargedExcitations_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import pytest + +from adcc.AdcMethod import AdcMethod, AdcType +from adcc.ChargedExcitations import DetachedStates, AttachedStates + +from .testdata_cache import testdata_cache + + +cases_ip_ea = [ + ("h2o_sto3g", "ip-adc2", "gen", "doublet"), + ("h2o_sto3g", "ea-adc2", "gen", "doublet"), +] + + +@pytest.mark.parametrize("system,method,case,kind", cases_ip_ea) +def test_ip_ea_basic_interface(system, method, case, kind): + adc_type = AdcMethod(method).adc_type + + if adc_type is AdcType.IP: + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + assert isinstance(state, DetachedStates) + elif adc_type is AdcType.EA: + state = testdata_cache.adcc_states( + system=system, + method=method, + case=case, + kind=kind, + is_alpha=True, + ) + assert isinstance(state, AttachedStates) + else: + raise AssertionError("Unexpected ADC type") + + # size matches number of excitation vectors + assert state.size == len(state.excitation_vector) + assert state.size == len(state.excitation_energy) diff --git a/adcc/tests/data/SHA256SUMS b/adcc/tests/data/SHA256SUMS index 180ec316..22e907bd 100644 --- a/adcc/tests/data/SHA256SUMS +++ b/adcc/tests/data/SHA256SUMS @@ -5,12 +5,26 @@ eb433c83b7bde2693ef2ae9a6adc16ddfe21ae89dbb86778ff8227be919d237c cn_ccpvdz_adcc dd3b0776c3a1dd46e3720c7f2a2db8dfe26aae30a6a9bf9f14da2e9345742870 cn_ccpvdz_adcc_adc2.hdf5 d28c1ffd6a8bc5574db71889b171c5c535c3558fcfd81bb77d26560ebd7bbce8 cn_ccpvdz_adcc_adc2x.hdf5 8502e612dcf7b18261df569c12924949c1cd348455048da00f8bef70a28f1d68 cn_ccpvdz_adcc_adc3.hdf5 +f9e07576076f1bffd9c439aeec1abb49427eb99366e65d7a0d5ffb9f451c498b cn_ccpvdz_adcc_ea-adc0.hdf5 +d8e0843e97317c214b3f3ed8dd1209c2d097df1a202d843d336b1d995733d48b cn_ccpvdz_adcc_ea-adc2.hdf5 +f8d01a40d3052b87c22f14ab9c643dfb31fbf48d8b0cbad712c57956e972fc85 cn_ccpvdz_adcc_ea-adc2x.hdf5 +7b3ea86eb2b84ba8b69c8c3d985e69fe1fe1c2e40637b480965fdd5651fe98a1 cn_ccpvdz_adcc_ea-adc3.hdf5 +843ef518e0cf3e97c88dd5e1658442d4c0c77c0773aa10b66495d1d5986efe5f cn_ccpvdz_adcc_ip-adc0.hdf5 +52759d5ce71664e9bb5cb33599f3c069a5fb685e7c14df6b82df897f267b559a cn_ccpvdz_adcc_ip-adc2.hdf5 +5019358f1070122e9b23ce8d551016438b36e84ce91788531da19a2940273c62 cn_ccpvdz_adcc_ip-adc2x.hdf5 +a7f7181990ed30cf38cda0a0b88421672b1bf974fa7fa7d5fb2d1955b3d4e4bc cn_ccpvdz_adcc_ip-adc3.hdf5 cefa3b262340d035749887bc065200c20e67de70625ef6f6ebc022b979921d07 cn_ccpvdz_adcc_mpdata.hdf5 8f8c3c86cb9f4b22f8bcfff10e7092fcec4258fbf2a451e3cd6456b1adb7ba85 cn_ccpvdz_adcman_adc0.hdf5 be3f5e6090ba4d6fef72760e982bc268dc49f870db1afc101af60e08be559423 cn_ccpvdz_adcman_adc1.hdf5 259b19562741b25ff08491c446e69af92a34ad320d204b7dcfe5a6a4928a9072 cn_ccpvdz_adcman_adc2.hdf5 8a8349b6d5b7f9eaf1b51f9d68f0ae4bb1b36b5d74ec9a9bac580e27cf77aedc cn_ccpvdz_adcman_adc2x.hdf5 1efce537b0cc5d7cb227acee19241f6ce11ce7d4e29a3cd9570f869c2b2c3f7c cn_ccpvdz_adcman_adc3.hdf5 +3a245b724cb53d058ffd76f664eabed5c93b124f3077de82ab361369d3f5fc0d cn_ccpvdz_adcman_ea-adc0.hdf5 +80d3bb9122f0fd31a65a0c651479e4af8d04968c974faee1ba7d89bd6c7231dc cn_ccpvdz_adcman_ea-adc2.hdf5 +af48346ca95af1a4795d56a43dedd9c76404f52f0babd78d4947c75700dcb509 cn_ccpvdz_adcman_ea-adc3.hdf5 +51b34f119b24040e98d1fa750630146079c8e039948994149b93f6cd0dd45de3 cn_ccpvdz_adcman_ip-adc0.hdf5 +4979368df74815035adf6e4b84ef5da3068a4d7fbbe6e014d3902d28d4e4ef80 cn_ccpvdz_adcman_ip-adc2.hdf5 +a8743b12ccf3ba67716008972be04d58298c28f4b4624d4377861e0a2bc13ff5 cn_ccpvdz_adcman_ip-adc3.hdf5 24069d49f0fae0a16b4141e04ebbaf9e0895adffb22c984f50c1a700a49212cf cn_ccpvdz_adcman_mpdata.hdf5 db27db84b2dea991a4e24b6ce7433f2619769505267bd486723b1b9bd017a5f3 cn_ccpvdz_hfdata.hdf5 8c3f128cd46df98eaaba3658b849150459f738e9b3cc1cc42f2b5ff6ad288c9f cn_ccpvdz_hfimport.hdf5 @@ -20,6 +34,14 @@ db27db84b2dea991a4e24b6ce7433f2619769505267bd486723b1b9bd017a5f3 cn_ccpvdz_hfda fe4da3d8dad6fb394329d6d409860641e99465b8d53f0efc880129238dfb4efd cn_sto3g_adcc_adc2x.hdf5 2a041d39ac6d1bbc1bc0365ba4e191a44f2e059c9c706891746500bb09dd2eb6 cn_sto3g_adcc_adc3.hdf5 7e56ecaf37b8d58da2e98519542623466d51c253be8cc189a0281b2b419390f7 cn_sto3g_adcc_adc4.hdf5 +ffeae310f386671ef10ba52d637576494bdc6ff2ff7e0f027027064405a044fa cn_sto3g_adcc_ea-adc0.hdf5 +f3a17cc0733110ab0bfc7b683cfa042acdf63789cee50447d83fb7d4d677803e cn_sto3g_adcc_ea-adc2.hdf5 +8f79f2c1acfdf7b9789cb0d4c4ca1c62fc4284814c0a89620fe65527c5a8e594 cn_sto3g_adcc_ea-adc2x.hdf5 +a911fec9a9a8ee92e91facdab68be0afa0d6d4c7a725b53b6abf1d21631632ac cn_sto3g_adcc_ea-adc3.hdf5 +c850181d00e405c6c97cd1d1340b13a19909482d61c63981c8ceb5c0301dbfcd cn_sto3g_adcc_ip-adc0.hdf5 +c14114493b7c977102fa553480e6c10230ab2d0d37a3769a65e5ce15c4f66c97 cn_sto3g_adcc_ip-adc2.hdf5 +87feeadbd2eb2e5dcc0500c7c90f6af89494188efae718e38df48deca679e82b cn_sto3g_adcc_ip-adc2x.hdf5 +940d655a575c48ab26257b445e7a2822f75de3b49cf1d887d1ce164d260a6303 cn_sto3g_adcc_ip-adc3.hdf5 17994daf530ea179f872cd73695a7e492a0cd4c39947ded1c97afb663d90fa58 cn_sto3g_adcc_mpdata.hdf5 f6974049ccf8a732ea584dbbebf608f0e17d28bd5320c0c63e1d1a5db80f54a7 cn_sto3g_adcman_adc0.hdf5 9950c0720546a0b313e755945e7fd0d5462967a4366f60470c4a85f500bffb5f cn_sto3g_adcman_adc1.hdf5 @@ -27,6 +49,12 @@ a866f1c06508e1bf48567c2546938e471b83c82ecce7dfce6815288c1e0226a6 cn_sto3g_adcma 7b6e09a40fd22f211119afe8f6f46d2c573ca905fa69c52f6f5d053e2c26447e cn_sto3g_adcman_adc2x.hdf5 2312e925cb33a47bf476c5528531c0c6908b8803a5d5f6785d1ff0f7da946353 cn_sto3g_adcman_adc3.hdf5 6e34b23da0215dd1e3969a5c1159abb9dcbae843183c3823f0ac872e9df7ae48 cn_sto3g_adcman_adc4.hdf5 +333b8b76527bc1e017230aa6e7d030faad001cfa0d5d382677264699dc3b3081 cn_sto3g_adcman_ea-adc0.hdf5 +ef78680d939d2e5204bb7217481c980c2d46cbccbc9da6375b0db975e92b34f4 cn_sto3g_adcman_ea-adc2.hdf5 +4ec353b931bd0986dad3cc9823a3ec6634d39ee2ebd1bea6c800ba047c32012b cn_sto3g_adcman_ea-adc3.hdf5 +b36e8397ee7af7eb627be2ab355a6b20e90b3a417f42b60ead0b5e8bde16c9d8 cn_sto3g_adcman_ip-adc0.hdf5 +cfc731c423999b1b1f79929e180f7d3cc76199079b4fc69dca1b2e0ccf9672ba cn_sto3g_adcman_ip-adc2.hdf5 +99b1ba20f1bc57be2acc083edd5ce31dcd1d2eedb6cb8e286862821bc4adb629 cn_sto3g_adcman_ip-adc3.hdf5 8f7a0fb42f351ffb3577636ae224a03a370a14fcea9d9252bef5c3fa37bb2444 cn_sto3g_adcman_mpdata.hdf5 7d843dd9981ef78f11a78051aea1ea61af8f12dc244b93e3f76fd70602c8f86e cn_sto3g_hfdata.hdf5 73adda09433dab2263e3dc2b37e10d5127e4c54a4296fdf2574ab71ec44d172a cn_sto3g_hfimport.hdf5 @@ -46,12 +74,26 @@ b579b2e62d4d02c0e5d16ab8b8e30009582152dba63ac12cea77f7dbbd80c29d formaldehyde_s 28e018d79c5481938f77177fbeb6f65e6c04122ef7f8fc3db478a5d31592da6c h2o_def2tzvp_adcc_adc2.hdf5 4a35c56f21d7c0ae424225bc756ac9b8a9f9b2f32b90e5aea45351b4249196f2 h2o_def2tzvp_adcc_adc2x.hdf5 6157f5d41dec84612de81005b17cdcc531efc74d5da42e1a49d2cb53492f3248 h2o_def2tzvp_adcc_adc3.hdf5 +528c73962158a58994c1cfd46ef92f05a34b66ca511cde4842290c0e6da8f2ff h2o_def2tzvp_adcc_ea-adc0.hdf5 +fd1d0720f62ff186d5227beb215d25eca9734ac5c132049c718ce38797f7cc18 h2o_def2tzvp_adcc_ea-adc2.hdf5 +40c91d44899604ad3e31558593ce20e0d3bff316bcf231f95f57a51c474f9992 h2o_def2tzvp_adcc_ea-adc2x.hdf5 +3e1f6ea7e08e4c51dba686f730cbaa84d33e5270ddd6e044f32889f1823dcb11 h2o_def2tzvp_adcc_ea-adc3.hdf5 +4a0776fb0ac75c0203bc41e9c25e3be5d80e1e5323071fde500fedb84711bd4c h2o_def2tzvp_adcc_ip-adc0.hdf5 +0381025325cdcb7b6f63ad6acb5901f5609316c1a85baf8475a7aba94457c337 h2o_def2tzvp_adcc_ip-adc2.hdf5 +68c1e39d23afbea60a4a4a4fd6c5dec43d4c7304ecd73b51f8b18308b18ca4c2 h2o_def2tzvp_adcc_ip-adc2x.hdf5 +33800f9d0b2a0edec35710bea483631bdfa29582a99f7ed2ef2f54da7a9321e0 h2o_def2tzvp_adcc_ip-adc3.hdf5 96eefd0a6314651f96cb4480a34652fc866984496aa365313b1521680c984fdd h2o_def2tzvp_adcc_mpdata.hdf5 2a88059266ac32a34cc352e1d2f782565317cec8573bdd81c4e4019ecc9adb65 h2o_def2tzvp_adcman_adc0.hdf5 dcf8ae501f7875705191abf754b33cf82f14065db2c95ab84ef3004b70ee9e75 h2o_def2tzvp_adcman_adc1.hdf5 7cd549b23a8939f5c989882b250284d5f6b20d14e97055f5caba7e6a6138f4b2 h2o_def2tzvp_adcman_adc2.hdf5 24394b48a8218eed94d2c9462df8ecd065ec3ad985e215389fee535268584c2f h2o_def2tzvp_adcman_adc2x.hdf5 86b875b3f7e7695477f2fc34b3860b9736e5d23c8803278f88cda7eadf9d833f h2o_def2tzvp_adcman_adc3.hdf5 +373dad86be62b2f255a58d1d2371d1b6ca4adb8c33bf2a168ad729138973f76f h2o_def2tzvp_adcman_ea-adc0.hdf5 +cae121c0658e8cef2b79d0adc119d4397787a8a5c5f8f4608225ba0266e6a0d0 h2o_def2tzvp_adcman_ea-adc2.hdf5 +cde62e824358e468d10a44b2b8271afec44eb0b350ad2b54f4168c0b90b32e54 h2o_def2tzvp_adcman_ea-adc3.hdf5 +d06b4f8c48e072009cf4f830cf882f9b4b887fe5eeb02bc7d72b69cfc4c3795b h2o_def2tzvp_adcman_ip-adc0.hdf5 +1f29b185958fddd686782425030b3014ddf4e363e8e73a146d9e3f7a20b114d5 h2o_def2tzvp_adcman_ip-adc2.hdf5 +8fc2922530d65cb23d821e662f9dedee62d91ad2508bf246ed3cbba640dbf75c h2o_def2tzvp_adcman_ip-adc3.hdf5 1b8a1e0ee2b80d0aac77876e4ca291fd213f0981587a59d6e4585d953c06d59e h2o_def2tzvp_adcman_mpdata.hdf5 36b3b93aa1071e61611d96b6b59ed13d37ec718303b8ef60a6c10aef07276717 h2o_def2tzvp_hfdata.hdf5 8a447ed3025662a2a71d866c697d345dbace45655e14c8777c10190f7f4c5967 h2o_def2tzvp_hfimport.hdf5 @@ -61,6 +103,14 @@ b56e74c8e71a9ab5ef62d44131c721e8d5ce236f9ed69d82869d702468aff7a8 h2o_sto3g_adcc 8e17428b0d7e93e91e3ba366f7d2a9aded91a5d8d37b47fcc790c20b5b150872 h2o_sto3g_adcc_adc2x.hdf5 de9a094a6da75d063a7d1a4137347409c7dd2ced6ae1bdd2ebece6b2712506d6 h2o_sto3g_adcc_adc3.hdf5 14b54dcc669aeb76815481e881d47dce8f7b949de2dba3ab6e7108e73ae508e5 h2o_sto3g_adcc_adc4.hdf5 +83b8899a0b7851e18b3e246b802a7e8679d91881c3838797ee6fdb596e54f977 h2o_sto3g_adcc_ea-adc0.hdf5 +c166cd64e6a4a8eac9f761c133d685646124b291887407b89a31b02e197eafe7 h2o_sto3g_adcc_ea-adc2.hdf5 +5badc1e98c937938c5e0cdebdc50fa11dd4b9d3e0f989e02e558ee867df04fb0 h2o_sto3g_adcc_ea-adc2x.hdf5 +c6ec9963d3cf5fad77ed6c57a4af166864c207f8af20189e3583b4f7233e2605 h2o_sto3g_adcc_ea-adc3.hdf5 +d2f58d41610467bcda51778d979d9b0d39bcd11146ce7ec6780f7e04134c95ef h2o_sto3g_adcc_ip-adc0.hdf5 +9e7d019e831be4c71a9acd50b27794c31ced89b3877d859df43a5e033a2d9fa4 h2o_sto3g_adcc_ip-adc2.hdf5 +82982e49f56fe32b8fe60a71fd31a3375e6669bae4830a1ebf6f554b11f88764 h2o_sto3g_adcc_ip-adc2x.hdf5 +d0067822d5061ac91091e303d1ad586a415d7c6190af3abce573fbb3a971d998 h2o_sto3g_adcc_ip-adc3.hdf5 4751e791a2ce1968106ffdb5128984a777554d8d03aef6585ef378efe1f396ef h2o_sto3g_adcc_mpdata.hdf5 cc957d99adaf872877b992860c9f0c9c8337f783b9c3ad20b36694391627188f h2o_sto3g_adcman_adc0.hdf5 4b36ac6cce98cf250ad268dc6e51e849acbd8c9383aa16aff233a2cf04118fcd h2o_sto3g_adcman_adc1.hdf5 @@ -68,9 +118,18 @@ cc957d99adaf872877b992860c9f0c9c8337f783b9c3ad20b36694391627188f h2o_sto3g_adcm dbf6b078e8096f8283cc64d1de98dff91b5edebd1d2f1df4f5739cfcecd98db7 h2o_sto3g_adcman_adc2x.hdf5 7a65c053bea13cc431ad127120578d27118ee9f9c026f6402dc07953cfb96c23 h2o_sto3g_adcman_adc3.hdf5 ebc3bd158758a8e8db58ee2a3cc95e85f440db623ca06d220c04d94561d35152 h2o_sto3g_adcman_adc4.hdf5 +84e1e96fdb30971f2e5f1a9501e9445a8dce031f8e36d6e46360a132440269f7 h2o_sto3g_adcman_ea-adc0.hdf5 +fe9806e6f42104c25c4e022484e670588c343bb55761baca599e01cd98b145f9 h2o_sto3g_adcman_ea-adc2.hdf5 +10e6b9d7e3b5d088233b61dd599946f55a579c96cefd0d21feee9c4893bc88c4 h2o_sto3g_adcman_ea-adc3.hdf5 +c936ae6eb312921adcf1a3fc8bac19453eebbe29f8de4a574ac981b2ef55fffa h2o_sto3g_adcman_ip-adc0.hdf5 +abeaeacf2248d3c779a605eaf5d8313029593e22f8656c1baa5bcb0a983e4fcb h2o_sto3g_adcman_ip-adc2.hdf5 +dd39481ff664de5c228919687ea3fbcf1effaa0aaddff4750d008cb25066b956 h2o_sto3g_adcman_ip-adc3.hdf5 4b21f9230140823618c12761ac51bab626e47a82ec04dc05b0a57422fb85b9b1 h2o_sto3g_adcman_mpdata.hdf5 6c64153a6e708edad4c8a07e0ed6e5e1379e8e9c721e49f2aa0cc4ecb9817082 h2o_sto3g_hfdata.hdf5 74733b03dd12561479132a768c2b330b4b8dbf1fe60fe0b14caa4d3fd4da7fce h2o_sto3g_hfimport.hdf5 +057ef124dc4b3fdfe4958f74009500a6507c8f2bb2a24f0a9cb4bc8119334c10 h2s_6311g_hfdata.hdf5 +4354d9a8f676370454deb43116775f111d9e1a32508ef7e0adaa0cca67c1a87d h2s_sto3g_hfdata.hdf5 +93bf9f69b8ab2c524b499bc60790f245401ab0d65cc342e92479c68051b0f34d hf3_631g_hfdata.hdf5 ca45224813877e28332fd27c445320200e34447fca4e1f3dd2fe318948caf3ae hf_631g_adcc_adc0.hdf5 c5b7f42d403183e7e44289d7b3bca21cec217a9cf01fb6bf4e0ae33d17884d21 hf_631g_adcc_adc1.hdf5 d6809c1474a375141a3cd065ceabf84e26ade89e79973c6403110bb77c6a0e7d hf_631g_adcc_adc2.hdf5 @@ -86,4 +145,5 @@ ff8cba744d8820c1877104985454b611ea54e7f1bbbac683079911ef19b9dd25 hf_631g_adcman 4145630c7f3f4426608fbc2c3a883a90b810d7076ea10e6cc157b3f64bdb17f4 hf_631g_adcman_adc4.hdf5 0f2e477d37597331cf955be0d0bdaceb55604fd220e98612bb2ad3fb3fe5d50e hf_631g_adcman_mpdata.hdf5 a1eab1dd706a3f8f7532889b9a4754fa627a47ce5cfb9eb7d95a8365774850f2 hf_631g_hfdata.hdf5 +5877bf4fc4554759b2f0ba35c58cee1e9a7ae2de4d47673a98793d4769eec841 methox_sto3g_hfdata.hdf5 0e86aef221cc541cbade47e99471fa7c0bc1955c80783be4b32a8ea58902b2f6 r2methyloxirane_sto3g_hfdata.hdf5 diff --git a/adcc/tests/data/update_testdata.sh b/adcc/tests/data/update_testdata.sh index c349888f..a304cddd 100755 --- a/adcc/tests/data/update_testdata.sh +++ b/adcc/tests/data/update_testdata.sh @@ -4,7 +4,7 @@ trap "exit 1" TERM export SCRIPT_PID=$$ -SOURCE="https://wwwagdreuw.iwr.uni-heidelberg.de/adcc_test_data/0.8.1/" +SOURCE="https://wwwagdreuw.iwr.uni-heidelberg.de/adcc_test_data/ip_ea/" SHAFILE="SHA256SUMS" @@ -17,6 +17,14 @@ DATAFILES=( cn_sto3g_adcc_adc2x.hdf5 cn_sto3g_adcc_adc3.hdf5 cn_sto3g_adcc_adc4.hdf5 + cn_sto3g_adcc_ip-adc0.hdf5 + cn_sto3g_adcc_ip-adc2.hdf5 + cn_sto3g_adcc_ip-adc2x.hdf5 + cn_sto3g_adcc_ip-adc3.hdf5 + cn_sto3g_adcc_ea-adc0.hdf5 + cn_sto3g_adcc_ea-adc2.hdf5 + cn_sto3g_adcc_ea-adc2x.hdf5 + cn_sto3g_adcc_ea-adc3.hdf5 cn_sto3g_adcc_mpdata.hdf5 cn_sto3g_adcman_adc0.hdf5 cn_sto3g_adcman_adc1.hdf5 @@ -24,6 +32,12 @@ DATAFILES=( cn_sto3g_adcman_adc2x.hdf5 cn_sto3g_adcman_adc3.hdf5 cn_sto3g_adcman_adc4.hdf5 + cn_sto3g_adcman_ip-adc0.hdf5 + cn_sto3g_adcman_ip-adc2.hdf5 + cn_sto3g_adcman_ip-adc3.hdf5 + cn_sto3g_adcman_ea-adc0.hdf5 + cn_sto3g_adcman_ea-adc2.hdf5 + cn_sto3g_adcman_ea-adc3.hdf5 cn_sto3g_adcman_mpdata.hdf5 cn_sto3g_hfdata.hdf5 cn_sto3g_hfimport.hdf5 @@ -39,6 +53,14 @@ DATAFILES=( h2o_sto3g_adcc_adc2x.hdf5 h2o_sto3g_adcc_adc3.hdf5 h2o_sto3g_adcc_adc4.hdf5 + h2o_sto3g_adcc_ip-adc0.hdf5 + h2o_sto3g_adcc_ip-adc2.hdf5 + h2o_sto3g_adcc_ip-adc2x.hdf5 + h2o_sto3g_adcc_ip-adc3.hdf5 + h2o_sto3g_adcc_ea-adc0.hdf5 + h2o_sto3g_adcc_ea-adc2.hdf5 + h2o_sto3g_adcc_ea-adc2x.hdf5 + h2o_sto3g_adcc_ea-adc3.hdf5 h2o_sto3g_adcc_mpdata.hdf5 h2o_sto3g_adcman_adc0.hdf5 h2o_sto3g_adcman_adc1.hdf5 @@ -46,6 +68,12 @@ DATAFILES=( h2o_sto3g_adcman_adc2x.hdf5 h2o_sto3g_adcman_adc3.hdf5 h2o_sto3g_adcman_adc4.hdf5 + h2o_sto3g_adcman_ip-adc0.hdf5 + h2o_sto3g_adcman_ip-adc2.hdf5 + h2o_sto3g_adcman_ip-adc3.hdf5 + h2o_sto3g_adcman_ea-adc0.hdf5 + h2o_sto3g_adcman_ea-adc2.hdf5 + h2o_sto3g_adcman_ea-adc3.hdf5 h2o_sto3g_adcman_mpdata.hdf5 h2o_sto3g_hfdata.hdf5 h2o_sto3g_hfimport.hdf5 @@ -72,12 +100,26 @@ DATAFILES_FULL=( cn_ccpvdz_adcc_adc2.hdf5 cn_ccpvdz_adcc_adc2x.hdf5 cn_ccpvdz_adcc_adc3.hdf5 + cn_ccpvdz_adcc_ip-adc0.hdf5 + cn_ccpvdz_adcc_ip-adc2.hdf5 + cn_ccpvdz_adcc_ip-adc2x.hdf5 + cn_ccpvdz_adcc_ip-adc3.hdf5 + cn_ccpvdz_adcc_ea-adc0.hdf5 + cn_ccpvdz_adcc_ea-adc2.hdf5 + cn_ccpvdz_adcc_ea-adc2x.hdf5 + cn_ccpvdz_adcc_ea-adc3.hdf5 cn_ccpvdz_adcc_mpdata.hdf5 cn_ccpvdz_adcman_adc0.hdf5 cn_ccpvdz_adcman_adc1.hdf5 cn_ccpvdz_adcman_adc2.hdf5 cn_ccpvdz_adcman_adc2x.hdf5 cn_ccpvdz_adcman_adc3.hdf5 + cn_ccpvdz_adcman_ip-adc0.hdf5 + cn_ccpvdz_adcman_ip-adc2.hdf5 + cn_ccpvdz_adcman_ip-adc3.hdf5 + cn_ccpvdz_adcman_ea-adc0.hdf5 + cn_ccpvdz_adcman_ea-adc2.hdf5 + cn_ccpvdz_adcman_ea-adc3.hdf5 cn_ccpvdz_adcman_mpdata.hdf5 cn_ccpvdz_hfdata.hdf5 cn_ccpvdz_hfimport.hdf5 @@ -91,12 +133,26 @@ DATAFILES_FULL=( h2o_def2tzvp_adcc_adc2.hdf5 h2o_def2tzvp_adcc_adc2x.hdf5 h2o_def2tzvp_adcc_adc3.hdf5 + h2o_def2tzvp_adcc_ip-adc0.hdf5 + h2o_def2tzvp_adcc_ip-adc2.hdf5 + h2o_def2tzvp_adcc_ip-adc2x.hdf5 + h2o_def2tzvp_adcc_ip-adc3.hdf5 + h2o_def2tzvp_adcc_ea-adc0.hdf5 + h2o_def2tzvp_adcc_ea-adc2.hdf5 + h2o_def2tzvp_adcc_ea-adc2x.hdf5 + h2o_def2tzvp_adcc_ea-adc3.hdf5 h2o_def2tzvp_adcc_mpdata.hdf5 h2o_def2tzvp_adcman_adc0.hdf5 h2o_def2tzvp_adcman_adc1.hdf5 h2o_def2tzvp_adcman_adc2.hdf5 h2o_def2tzvp_adcman_adc2x.hdf5 h2o_def2tzvp_adcman_adc3.hdf5 + h2o_def2tzvp_adcman_ip-adc0.hdf5 + h2o_def2tzvp_adcman_ip-adc2.hdf5 + h2o_def2tzvp_adcman_ip-adc3.hdf5 + h2o_def2tzvp_adcman_ea-adc0.hdf5 + h2o_def2tzvp_adcman_ea-adc2.hdf5 + h2o_def2tzvp_adcman_ea-adc3.hdf5 h2o_def2tzvp_adcman_mpdata.hdf5 h2o_def2tzvp_hfdata.hdf5 h2o_def2tzvp_hfimport.hdf5 diff --git a/adcc/tests/generators/dump_adcc.py b/adcc/tests/generators/dump_adcc.py index a2003b08..10b9d9c6 100644 --- a/adcc/tests/generators/dump_adcc.py +++ b/adcc/tests/generators/dump_adcc.py @@ -2,6 +2,7 @@ from adcc.AdcMatrix import AdcMatrix from adcc.AmplitudeVector import AmplitudeVector from adcc.ExcitedStates import ExcitedStates +from adcc.ChargedExcitations import DetachedStates, AttachedStates from adcc.hdf5io import emplace_dict from adcc.LazyMp import LazyMp from adcc.State2States import State2States @@ -84,11 +85,51 @@ def dump_groundstate(ground_state: LazyMp, hdf5_file: h5py.Group, hdf5_file.attrs["adcc_version"] = adcc.__version__ -def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, - only_full_mode: bool, - dump_nstates: int | None = None) -> None: +def _pp_adc_properties(states: ExcitedStates, kind_data: dict, n_states: int + ) -> None: + """Collects all properties that are unique for PP-ADC """ - Dump the excited states data to the given hdf5 file/group. + tdm_bb_a = [] # Ground to Excited state tdm AO basis alpha part + tdm_bb_b = [] # Ground to Excited state tdm AO basis beta part + + for n in range(n_states): + # TDMs + bb_a, bb_b = states.transition_dm[n].to_ao_basis(states.reference_state) + tdm_bb_a.append(bb_a.to_ndarray()) + tdm_bb_b.append(bb_b.to_ndarray()) + kind_data["transition_dipole_moments"] = ( + states.transition_dipole_moment[:n_states] + ) + kind_data["transition_dipole_moments_velocity"] = ( + states.transition_dipole_moment_velocity[:n_states] + ) + + gauge_origins = ["origin", "mass_center", "charge_center"] + for g_origin in gauge_origins: + kind_data[f"transition_magnetic_dipole_moments_{g_origin}"] = ( + states.transition_magnetic_dipole_moment(g_origin)[:n_states] + ) + kind_data[f"transition_quadrupole_moments_{g_origin}"] = ( + states.transition_quadrupole_moment(g_origin)[:n_states] + ) + # ground to excited state tdm + kind_data["ground_to_excited_tdm_bb_a"] = np.asarray(tdm_bb_a) + kind_data["ground_to_excited_tdm_bb_b"] = np.asarray(tdm_bb_b) + + +def _ip_ea_adc_properties(states: DetachedStates | AttachedStates, + kind_data: dict, n_states: int) -> None: + """Collects all properties that are unique for IP/EA-ADC + """ + pass + + +def dump_excited_states( + states: ExcitedStates | DetachedStates | AttachedStates, + hdf5_file: h5py.Group, only_full_mode: bool, + dump_nstates: int = None) -> None: + """ + Dump the (charged) excited states data to the given hdf5 file/group. The number of states to dump can be given by dump_nstates. By default all states are dumped. The only_full_mode flag indicates whether the underlying test case is only @@ -96,7 +137,7 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, and therefore not all test data might be dumped in that case. """ # ensure that the calculation converged on a nonzero result - assert states.converged # type: ignore + assert states.converged assert all(abs(e) > 1e-12 for e in states.excitation_energy) n_states = len(states.excitation_energy) @@ -105,50 +146,29 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, dm_bb_a = [] # State diffdm AO basis alpha part dm_bb_b = [] # State diffdm AO basis beta part. - tdm_bb_a = [] # Ground to Excited state tdm AO basis alpha part - tdm_bb_b = [] # Ground to Excited state tdm AO basis beta part # split the eigenvectors according to their excitation degree for all states eigenvectors: dict[int, list] = {} for n in range(n_states): - # densities - bb_a, bb_b = states.state_diffdm[n].to_ao_basis(states.reference_state) - dm_bb_a.append(bb_a.to_ndarray()) - dm_bb_b.append(bb_b.to_ndarray()) - bb_a, bb_b = states.transition_dm[n].to_ao_basis(states.reference_state) - tdm_bb_a.append(bb_a.to_ndarray()) - tdm_bb_b.append(bb_b.to_ndarray()) + if isinstance(states, ExcitedStates): + # densities + bb_a, bb_b = states.state_diffdm[n].to_ao_basis(states.reference_state) + dm_bb_a.append(bb_a.to_ndarray()) + dm_bb_b.append(bb_b.to_ndarray()) # eigenvectors for exdegree, block in enumerate(states.matrix.axis_blocks): if exdegree + 1 not in eigenvectors: eigenvectors[exdegree + 1] = [] - eigenvectors[exdegree + 1].append(getattr( - states.excitation_vector[n], block # type: ignore - ).to_ndarray()) + eigenvectors[exdegree + 1].append( + getattr(states.excitation_vector[n], block).to_ndarray() + ) kind_data = {} + + if isinstance(states, ExcitedStates): + _pp_adc_properties(states, kind_data, n_states) + elif isinstance(states, (DetachedStates, AttachedStates)): + _ip_ea_adc_properties(states, kind_data, n_states) # eigenvalues kind_data["eigenvalues"] = states.excitation_energy[:n_states] - # state and transition dipole moments - kind_data["state_dipole_moments"] = states.state_dipole_moment[:n_states] - kind_data["transition_dipole_moments"] = ( - states.transition_dipole_moment[:n_states] - ) - kind_data["transition_dipole_moments_velocity"] = ( - states.transition_dipole_moment_velocity[:n_states] - ) - - gauge_origins = ["origin", "mass_center", "charge_center"] - for g_origin in gauge_origins: - kind_data[f"transition_magnetic_dipole_moments_{g_origin}"] = ( - states.transition_magnetic_dipole_moment(g_origin)[:n_states] - ) - kind_data[f"transition_quadrupole_moments_{g_origin}"] = ( - states.transition_quadrupole_moment(g_origin)[:n_states] - ) - # state diffdm and ground to excited state tdm - kind_data["state_diffdm_bb_a"] = np.asarray(dm_bb_a) - kind_data["state_diffdm_bb_b"] = np.asarray(dm_bb_b) - kind_data["ground_to_excited_tdm_bb_a"] = np.asarray(tdm_bb_a) - kind_data["ground_to_excited_tdm_bb_b"] = np.asarray(tdm_bb_b) # dump the eigenvectors kind_data["eigenvectors_singles"] = np.asarray(eigenvectors[1]) if 2 in eigenvectors: @@ -156,34 +176,41 @@ def dump_excited_states(states: ExcitedStates, hdf5_file: h5py.Group, # only dump triples for small systems if 3 in eigenvectors and not only_full_mode: kind_data["eigenvectors_triples"] = np.asarray(eigenvectors[3]) - # state to state tdm: not implemented for CVS - if not states.method.is_core_valence_separated: - for ifrom in range(n_states - 1): - state2state = State2States(states, initial=ifrom) - # extract the tdms for the desired states - tdm_bb_a = [] - tdm_bb_b = [] - for j, tdm in enumerate(state2state.transition_dm): - if ifrom + j + 2 > n_states: # ifrom + j + 2 = ito - break - bb_a, bb_b = tdm.to_ao_basis(states.reference_state) - tdm_bb_a.append(bb_a.to_ndarray()) - tdm_bb_b.append(bb_b.to_ndarray()) - kind_data[f"state_to_state/from_{ifrom}/transition_dipole_moments"] = ( - state2state.transition_dipole_moment[:n_states - ifrom - 1] - ) - kind_data[f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_a"] = ( - np.asarray(tdm_bb_a) - ) - kind_data[f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_b"] = ( - np.asarray(tdm_bb_b) - ) - # ssq for unrestriced calculation - if ( - not states.reference_state.restricted - and not states.ground_state.has_core_occupied_space - ): - kind_data["state_ssq"] = states.state_ssq + if isinstance(states, ExcitedStates): + # state and transition dipole moments + kind_data["state_dipole_moments"] = states.state_dipole_moment[:n_states] + + # state diffdm and ground to excited state tdm + kind_data["state_diffdm_bb_a"] = np.asarray(dm_bb_a) + kind_data["state_diffdm_bb_b"] = np.asarray(dm_bb_b) + # state to state tdm: not implemented for CVS + if not states.method.is_core_valence_separated: + for ifrom in range(n_states - 1): + state2state = State2States(states, initial=ifrom) + # extract the tdms for the desired states + tdm_bb_a = [] + tdm_bb_b = [] + for j, tdm in enumerate(state2state.transition_dm): + if ifrom + j + 2 > n_states: # ifrom + j + 2 = ito + break + bb_a, bb_b = tdm.to_ao_basis(states.reference_state) + tdm_bb_a.append(bb_a.to_ndarray()) + tdm_bb_b.append(bb_b.to_ndarray()) + kind_data[ + f"state_to_state/from_{ifrom}/transition_dipole_moments" + ] = (state2state.transition_dipole_moment[:n_states - ifrom - 1]) + kind_data[ + f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_a" + ] = (np.asarray(tdm_bb_a)) + kind_data[ + f"state_to_state/from_{ifrom}/state_to_excited_tdm_bb_b" + ] = (np.asarray(tdm_bb_b)) + # ssq for unrestriced calculation + if ( + not states.reference_state.restricted + and not states.ground_state.has_core_occupied_space + ): + kind_data["state_ssq"] = states.state_ssq # write the data to hdf5 emplace_dict(kind_data, hdf5_file, compression="gzip") hdf5_file.attrs["adcc_version"] = adcc.__version__ diff --git a/adcc/tests/generators/generate_adcc_data.py b/adcc/tests/generators/generate_adcc_data.py index a94caf68..4be88d14 100644 --- a/adcc/tests/generators/generate_adcc_data.py +++ b/adcc/tests/generators/generate_adcc_data.py @@ -4,7 +4,8 @@ from adcc.tests.testdata_cache import testdata_cache from adcc.tests import testcases -from adcc.AdcMethod import AdcMethod +from adcc.AdcMethod import AdcMethod, AdcType +from adcc.AdcMatrix import AdcMatrixlike from adcc.LazyMp import LazyMp from adcc.workflow import run_adc, validate_state_parameters from adcc import copy as adcc_copy @@ -17,8 +18,11 @@ # the base methods for each adc_type for which to generate data # the different cases (cvs, fc, ...) are handled in the generate functions. +# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0 _methods = { - "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3") + "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"), + "ip": ("ip-adc0", "ip-adc2", "ip-adc2x", "ip-adc3"), + "ea": ("ea-adc0", "ea-adc2", "ea-adc2x", "ea-adc3"), } _small_cases_methods = { "pp": _methods["pp"] + ("adc4",) @@ -29,6 +33,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, gs_density_order: int | None = None, n_states: int | None = None, n_singlets: int | None = None, n_triplets: int | None = None, n_spin_flip: int | None = None, + n_doublets: int | None = None, is_alpha: bool | None = None, dump_nstates: int | None = None, **kwargs) -> None: """ Generate and dump the excited states reference data for the given reference case @@ -42,13 +47,20 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, # the kind at this point to check if we need to perform a calculation # for this purpose we need the reference state hf = testdata_cache.refstate(system=test_case, case=case) - _, kind = validate_state_parameters( - hf, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets, - n_spin_flip=n_spin_flip + matrix = AdcMatrixlike() + matrix.reference_state = hf + matrix.method = method + _, kind, is_alpha = validate_state_parameters( + matrix, n_states=n_states, n_singlets=n_singlets, n_triplets=n_triplets, + n_spin_flip=n_spin_flip, n_doublets=n_doublets, is_alpha=is_alpha ) key = f"{case}/{gs_density_order}" if f"{key}/{kind}" in hdf5_file: return None + if method.adc_type in (AdcType.IP, AdcType.EA): + spin = "alpha" if is_alpha else "beta" + if f"{key}/{spin}/{kind}" in hdf5_file: + return None # CVS-ADC(4) not available if "cvs" in case and method.name == "adc4": return None @@ -61,13 +73,20 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, assert gs_density_order is None states = run_adc( hf, method=method, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, **kwargs + n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, + is_alpha=is_alpha, **kwargs ) assert states.kind == kind # maybe we predicted wrong? # type: ignore - if f"{key}/matrix" not in hdf5_file: - # the matrix data is only dumped once for each case. I think it does not - # make sense to dump the data once for a singlet and once for a triplet - # trial vector. + if method.adc_type in (AdcType.IP, AdcType.EA): + key = f"{key}/{spin}" + dump_matrix = (f"{key}/matrix" not in hdf5_file and ( + method.adc_type is AdcType.PP or ( + method.adc_type in (AdcType.IP, AdcType.EA) and is_alpha)) + ) + if dump_matrix: + # the matrix data is only dumped once for each case and only for alpha. + # I think it does not make sense to dump the data once for a singlet + # and once for a triplet trial vector as well as an alpha and a beta one. matrix_group = hdf5_file.create_group(f"{key}/matrix") trial_vec = adcc_copy(states.excitation_vector[0]).set_random() # type: ignore # noqa: E501 dump_matrix_testdata(states.matrix, trial_vec, matrix_group) @@ -80,6 +99,7 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_states: int | None = None, n_singlets: int | None = None, n_triplets: int | None = None, n_spin_flip: int | None = None, + n_doublets: int | None = None, is_alpha: bool | None = None, dump_nstates: int | None = None, states_per_case: dict[str, dict[str, int]] | None = None, **kwargs) -> None: @@ -93,11 +113,14 @@ def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets = states_per_case[case].get("n_singlets", None) n_triplets = states_per_case[case].get("n_triplets", None) n_spin_flip = states_per_case[case].get("n_spin_flip", None) + n_doublets = states_per_case[case].get("n_doublets", None) for density_order in test_case.gs_density_orders: generate_adc( - test_case, method, case, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, - dump_nstates=dump_nstates, gs_density_order=density_order, **kwargs + test_case, method, case, n_states=n_states, + n_singlets=n_singlets, n_triplets=n_triplets, + n_spin_flip=n_spin_flip, n_doublets=n_doublets, + is_alpha=is_alpha, dump_nstates=dump_nstates, + gs_density_order=density_order, **kwargs ) @@ -149,6 +172,29 @@ def generate_h2o_sto3g(): **n_states, **kwargs ) + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level.to_int() < 2: + n_states = {n_states: 1} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + def generate_h2o_def2tzvp(): # RHF, Singlet, 43 basis functions: 5 occ, 38 virt. @@ -166,6 +212,26 @@ def generate_h2o_def2tzvp(): **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, is_alpha=True, + **n_states + ) + def generate_cn_sto3g(): # UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt @@ -175,12 +241,39 @@ def generate_cn_sto3g(): method = AdcMethod(method) for n_states in \ testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): - kwargs = {n_states: 3} + n_states = {n_states: 3} generate_adc_all( test_case=test_case, method=method, dump_nstates=2, - states_per_case=None, **kwargs + states_per_case=None, **n_states ) + for is_alpha in [True, False]: + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level.to_int() < 2: + n_states = {n_states: 2} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + if method.level.to_int() < 2: + n_states = {n_states: 2} + else: + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + def generate_cn_ccpvdz(): # UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt @@ -192,10 +285,31 @@ def generate_cn_ccpvdz(): testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): n_states = {n_states: 3} generate_adc_all( - test_case, method=method, dump_nstates=2, states_per_case=None, - **n_states + test_case, method=method, dump_nstates=2, + states_per_case=None, **n_states ) + for is_alpha in [True, False]: + for method in _methods["ip"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + for n_states in \ + testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + n_states = {n_states: 3} + generate_adc_all( + test_case, method=method, dump_nstates=2, + is_alpha=is_alpha, **n_states + ) + def generate_hf_631g(): # UHF, Triplet @@ -211,6 +325,28 @@ def generate_hf_631g(): **n_states ) + # TODO: PP spin-flip only + # for is_alpha in [True, False]: + # for method in _methods["ip"]: + # method = AdcMethod(method) + # for n_states in \ + # testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + # n_states = {n_states: 3} + # generate_adc_all( + # test_case, method=method, dump_nstates=2, + # is_alpha=is_alpha, **n_states + # ) + + # for method in _methods["ea"]: + # method = AdcMethod(method) + # for n_states in \ + # testcases.kinds_to_nstates(test_case.kinds[method.adc_type]): + # n_states = {n_states: 3} + # generate_adc_all( + # test_case, method=method, dump_nstates=2, + # is_alpha=is_alpha, **n_states + # ) + def main(): generate_h2o_sto3g() diff --git a/adcc/tests/generators/generate_adcman_data.py b/adcc/tests/generators/generate_adcman_data.py index 1df5eff4..a25848e1 100644 --- a/adcc/tests/generators/generate_adcman_data.py +++ b/adcc/tests/generators/generate_adcman_data.py @@ -12,8 +12,12 @@ # the base methods for each adc_type for which to generate data # the different cases (cvs, fc, ...) are handled in the generate functions. +# No need to test ip/ea-adc1 since it is equivalent to ip/ea-adc0 +# ip/ea-adc2x not implemented in Q-Chem _methods = { - "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3") + "pp": ("adc0", "adc1", "adc2", "adc2x", "adc3"), + "ip": ("ip-adc0", "ip-adc2", "ip-adc3"), + "ea": ("ea-adc0", "ea-adc2", "ea-adc3"), } _small_cases_methods = { "pp": _methods["pp"] + ("adc4",) @@ -34,6 +38,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, gs_density_order: int | None = None, n_singlets: int = 0, n_triplets: int = 0, n_spin_flip: int = 0, n_states: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), dump_nstates: int | None = None, **kwargs) -> None: """ Generate and dump the excited state reference data for the given reference case @@ -44,7 +50,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, datadir = Path(__file__).parent.parent / _testdata_dirname datafile = datadir / test_case.adcdata_file_name("adcman", method.name) hdf5_file = h5py.File(datafile, "a") # Read/write if exists, create otherwise - if f"{case}/{gs_density_order}" in hdf5_file: + key = f"{case}/{gs_density_order}" + if key in hdf5_file: return None # skip cvs-adc(0), since it is not available in qchem. if "cvs" in case and method.level is MethodLevel.ZERO: @@ -64,9 +71,10 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, method = AdcMethod(f"cvs-{method.name}") state_data, _ = run_qchem( test_case, method, case, import_states=True, import_gs=False, - n_singlets=n_singlets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, - n_states=n_states, import_nstates=dump_nstates, - gs_density_order=gs_density_order, **kwargs + n_singlets=n_singlets, n_triplets=n_triplets, n_ip_states=n_ip_states, + n_ea_states=n_ea_states, n_spin_flip=n_spin_flip, n_states=n_states, + import_nstates=dump_nstates, gs_density_order=gs_density_order, + **kwargs ) # the data returned from run_qchem should have already been imported # using the correct keys -> just dump them @@ -77,6 +85,8 @@ def generate_adc(test_case: testcases.TestCase, method: AdcMethod, case: str, def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets: int = 0, n_triplets: int = 0, n_spin_flip: int = 0, n_states: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), dump_nstates: int | None = None, states_per_case: dict[str, dict[str, int]] | None = None, **kwargs) -> None: @@ -90,12 +100,16 @@ def generate_adc_all(test_case: testcases.TestCase, method: AdcMethod, n_singlets = states_per_case[case].get("n_singlets", 0) n_triplets = states_per_case[case].get("n_triplets", 0) n_spin_flip = states_per_case[case].get("n_spin_flip", 0) + n_ea_states = states_per_case[case].get("n_ea_states", (0, 0)) + n_ip_states = states_per_case[case].get("n_ip_states", (0, 0)) n_states = states_per_case[case].get("n_states", 0) for density_order in test_case.gs_density_orders: generate_adc( test_case, method, case, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, n_states=n_states, - dump_nstates=dump_nstates, gs_density_order=density_order, + n_triplets=n_triplets, n_spin_flip=n_spin_flip, + n_states=n_states, n_ea_states=n_ea_states, + n_ip_states=n_ip_states, dump_nstates=dump_nstates, + gs_density_order=density_order, **kwargs ) @@ -174,6 +188,25 @@ def generate_h2o_sto3g(): states_per_case=states.get(method.name, None), **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, + states_per_case=states.get(method.name, None), **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + if method.level.to_int() < 2: + n_states = {"n_ea_states": (1, 0)} + else: + n_states = {"n_ea_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, + states_per_case=states.get(method.name, None), **n_states + ) + def generate_h2o_def2tzvp(): # RHF, Singlet, 43 basis functions: 5 occ, 38 virt. @@ -190,6 +223,20 @@ def generate_h2o_def2tzvp(): **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + n_states = {"n_ea_states": (3, 0)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_cn_sto3g(): # UHF, Doublet, 10 basis functions: (7a, 6b) occ, (3a, 4b) virt @@ -204,6 +251,26 @@ def generate_cn_sto3g(): **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + if method.level.to_int() < 2: + n_states = {"n_ip_states": (2, 2)} + else: + n_states = {"n_ip_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + if method.level.to_int() < 2: + n_states = {"n_ea_states": (2, 2)} + else: + n_states = {"n_ea_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_cn_ccpvdz(): # UHF, Doublet @@ -218,6 +285,20 @@ def generate_cn_ccpvdz(): **n_states ) + for method in _methods["ip"]: + method = AdcMethod(method) + n_states = {"n_ip_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + + for method in _methods["ea"]: + method = AdcMethod(method) + n_states = {"n_ea_states": (3, 3)} + generate_adc_all( + test_case, method=method, dump_nstates=2, **n_states + ) + def generate_hf_631g(): # UHF, Triplet @@ -232,6 +313,21 @@ def generate_hf_631g(): **n_states ) + # TODO: PP spin-flip only + # for method in _methods["ip"]: + # method = AdcMethod(method) + # n_states = {"n_ip_states": (3, 3)} + # generate_adc_all( + # test_case, method=method, dump_nstates=2, **n_states + # ) + + # for method in _methods["ea"]: + # method = AdcMethod(method) + # n_states = {"n_ea_states": (3, 3)} + # generate_adc_all( + # test_case, method=method, dump_nstates=2, **n_states + # ) + def generate_formaldehyde_pe(): for test_case in testcases.get(n_expected_cases=2, name="formaldehyde"): diff --git a/adcc/tests/generators/import_qchem_data.py b/adcc/tests/generators/import_qchem_data.py index 15931c71..eea751d6 100644 --- a/adcc/tests/generators/import_qchem_data.py +++ b/adcc/tests/generators/import_qchem_data.py @@ -73,21 +73,35 @@ def import_excited_states(context: h5py.File, method: AdcMethod, """ # define the possible state kinds to import for each adc variant. state_kinds = { - AdcType.PP: [ - ("singlets", True), ("triplets", True), # restricted + AdcType.PP: { + True: ["singlets", "triplets"], # restricted # uhf and spin flip are located in the same ".../uhf/..." subtree - ("any_or_spinflip", False), # unrestricted - ] + False: ["any_or_spinflip"], # unrestricted + }, + AdcType.IP: { + True: [""], # restricted + False: ["alphas", "betas"], # unrestricted + }, + AdcType.EA: { + True: [""], # restricted + False: ["alphas", "betas"], # unrestricted + }, } # Of course the kinds have to have a slightly different name in adcc.... - kind_map = {"singlets": "singlet", "triplets": "triplet"} + kind_map = {"singlets": "singlet", "triplets": "triplet", + "alphas": "alpha", "betas": "beta"} # also the adcc methods have to be translated - method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn - if method_name.endswith("adc2"): # adc2 -> adc2s - method_name += "s" + if method.adc_type is AdcType.PP: + method_name: str = method.name.replace("-", "_") # cvs-adcn -> cvs_adcn + if method_name.endswith("adc2"): # adc2 -> adc2s (Only for PP) + method_name += "s" + else: + method_name: str = method.name.split('-')[-1] # No cvs (yet) + restricted = "rhf" in context[f"adc_{method.adc_type}"][method_name].keys() + # go through the different possible state kinds and import the states. data = {} - for kind, restricted in state_kinds[method.adc_type]: + for kind in state_kinds[method.adc_type][restricted]: states = _import_excited_states( context, method=method_name, only_full_mode=only_full_mode, adc_type=method.adc_type, import_nstates=import_nstates, @@ -107,7 +121,13 @@ def import_excited_states(context: h5py.File, method: AdcMethod, if kind == "any_or_spinflip": kind = "spin_flip" if is_spin_flip else "any" - data[kind_map.get(kind, kind)] = states + if method.adc_type is AdcType.PP: + data[kind_map.get(kind, kind)] = states + elif method.adc_type in (AdcType.IP, AdcType.EA): + if restricted: + data["alpha"] = {"doublet": states} + else: + data[kind_map.get(kind, kind)] = {"any": states} if not data: raise RuntimeError(f"Could not find any states for {method.name} in " f"{context.filename}.") @@ -141,7 +161,8 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool Only import the first n states from the context. state_kind: str, optional The multiplicity of the states, e.g., singlet or triplet for restricted - pp-adc calculations. + pp-adc calculations. In case of an unrestricted IP/EA-ADC calc. it is + "alphas" or "betas" restricted: bool, optional Whether the adc calculation is based on a restricted reference state. dims_pref: str, optional @@ -152,10 +173,15 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool # build the path under which to find the exicted states tree = [f"adc_{adc_type.to_str()}", method] if restricted: - assert state_kind is not None # needs to be defined for restricted calcs - tree.extend(["rhf", state_kind]) + tree.append("rhf") + if adc_type is AdcType.PP: + assert state_kind is not None # needs to be defined for restricted + tree.append(state_kind) else: tree.append("uhf") + if adc_type in (AdcType.IP, AdcType.EA): + assert state_kind is not None + tree.append(state_kind) tree.append("0") # we assume that we only have a single irrep!! tree = "/".join(tree) # check that we have states to read and return if not @@ -170,7 +196,12 @@ def _import_excited_states(context: h5py.File, method: str, only_full_mode: bool # context. data_to_read = {} for n in range(n_states): - state_tree = tree + f"/es{n}" + if adc_type is AdcType.PP: + state_tree = tree + f"/es{n}" + elif adc_type is AdcType.IP: + state_tree = tree + f"/ip{n}" + elif adc_type is AdcType.EA: + state_tree = tree + f"/ea{n}" # ensure that the state is converged _, converged = _extract_dataset(context[f"{state_tree}/converged"]) if not converged: @@ -247,10 +278,15 @@ def _import_state_to_state_data(context: h5py.File, method: str, # build the path under which to look for the state-to-state data. tree = [f"adc_{adc_type.to_str()}", method] if restricted: - assert state_kind is not None # needs to be defined for restricted calcs - tree.extend(["rhf", "isr", state_kind]) + tree.extend(["rhf", "isr"]) + if adc_type is AdcType.PP: + assert state_kind is not None # needs to be defined for restricted + tree.append(state_kind) else: tree.extend(["uhf", "isr"]) + if adc_type in (AdcType.IP, AdcType.EA): + assert state_kind is not None + tree.append(state_kind) tree.append("0-0") # we assume that we only have a single irrep! tree = "/".join(tree) if tree not in context: @@ -352,6 +388,8 @@ def import_data(context: h5py.File, dims_pref: str = "dims/", "optdm/dm_bb_b": "ground_to_excited_tdm_bb_b", # transition dipole moment (vector): only when we have a optdm "tprop/dipole": "transition_dipole_moments", + # Pole strengths for IP/EA-ADC + "pole_strength": "pole_strengths", # doubles and triples part of the amplitude vector "u2": "eigenvectors_doubles", "u3": "eigenvectors_triples", diff --git a/adcc/tests/generators/run_qchem.py b/adcc/tests/generators/run_qchem.py index a79ee1eb..14cb670a 100644 --- a/adcc/tests/generators/run_qchem.py +++ b/adcc/tests/generators/run_qchem.py @@ -29,6 +29,8 @@ def run_qchem(test_case: testcases.TestCase, method: AdcMethod, case: str, run_qchem_scf: bool = False, import_nstates: int | None = None, n_states: int = 0, n_singlets: int = 0, n_triplets: int = 0, n_spin_flip: int = 0, + n_ip_states: tuple[int, int] = (0, 0), + n_ea_states: tuple[int, int] = (0, 0), **kwargs) -> tuple[dict | None, dict | None]: """ Run a qchem calculation for the given test case and method on top @@ -122,6 +124,7 @@ def run_qchem(test_case: testcases.TestCase, method: AdcMethod, case: str, n_core_orbitals=n_core_orbitals, n_frozen_core=n_frozen_core, n_frozen_virtual=n_frozen_virtual, any_states=n_states, singlet_states=n_singlets, triplet_states=n_triplets, + ip_states=n_ip_states, ea_states=n_ea_states, sf_states=n_spin_flip, run_qchem_scf=run_qchem_scf, **args ) # call qchem and wait for completion @@ -351,6 +354,8 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis: bohr: bool = True, any_states: int = 0, singlet_states: int = 0, triplet_states: int = 0, sf_states: int = 0, + ip_states: tuple[int, int] = (0, 0), + ea_states: tuple[int, int] = (0, 0), maxiter: int = 160, conv_tol: int = 10, n_core_orbitals: int | None = None, n_frozen_core: int | None = None, @@ -408,6 +413,10 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis: singlet_states=singlet_states, triplet_states=triplet_states, sf_states=sf_states, + ip_states_alpha=ip_states[0], + ip_states_beta=ip_states[1], + ea_states_alpha=ea_states[0], + ea_states_beta=ea_states[1], n_guesses=nguess_singles, bohr=bohr, maxiter=maxiter, @@ -445,6 +454,10 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis: ee_singlets {singlet_states} ee_triplets {triplet_states} sf_states {sf_states} +eom_ip_alpha {ip_states_alpha} +eom_ip_beta {ip_states_beta} +eom_ea_alpha {ea_states_alpha} +eom_ea_beta {ea_states_beta} input_bohr {bohr} sym_ignore true adc_davidson_maxiter {maxiter} @@ -503,7 +516,17 @@ def generate_qchem_input_file(infile: str | Path, adc_method: AdcMethod, basis: "cvs-adc1": "cvs-adc(1)", "cvs-adc2": "cvs-adc(2)", "cvs-adc2x": "cvs-adc(2)-x", - "cvs-adc3": "cvs-adc(3)" + "cvs-adc3": "cvs-adc(3)", + "ip-adc0": "adc(0)", + "ip-adc1": "adc(1)", + "ip-adc2": "adc(2)", + "ip-adc2x": "adc(2)-x", + "ip-adc3": "adc(3)", + "ea-adc0": "adc(0)", + "ea-adc1": "adc(1)", + "ea-adc2": "adc(2)", + "ea-adc2x": "adc(2)-x", + "ea-adc3": "adc(3)", } _isr_order_dict: dict[MethodLevel, str] = { diff --git a/adcc/tests/guess_test.py b/adcc/tests/guess_test.py index 70dff06e..7b9bcfa3 100644 --- a/adcc/tests/guess_test.py +++ b/adcc/tests/guess_test.py @@ -28,14 +28,20 @@ import adcc import adcc.guess +from adcc.guess import determine_spin_change, estimate_n_guesses +from adcc.AdcMethod import AdcType from .testdata_cache import testdata_cache from . import testcases # The methods to test -singles_methods = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"] -doubles_methods = ["adc2", "adc2x", "adc3", "adc4"] +singles_methods_pp = ["adc0", "adc1", "adc2", "adc2x", "adc3", "adc4"] +doubles_methods_pp = ["adc2", "adc2x", "adc3", "adc4"] +singles_methods_ip = ["ip-adc0", "ip-adc1", "ip-adc2", "ip-adc2x", "ip-adc3"] +doubles_methods_ip = ["ip-adc2", "ip-adc2x", "ip-adc3"] +singles_methods_ea = ["ea-adc0", "ea-adc1", "ea-adc2", "ea-adc2x", "ea-adc3"] +doubles_methods_ea = ["ea-adc2", "ea-adc2x", "ea-adc3"] # the testcases h2o_sto3g = testcases.get_by_filename("h2o_sto3g").pop() cn_sto3g = testcases.get_by_filename("cn_sto3g").pop() @@ -43,6 +49,98 @@ class TestGuess: + def test_determine_spin_change_pp(self): + method = adcc.AdcMethod("adc2") + + spin = determine_spin_change(method, kind="singlet") + assert spin == 0.0 + + spin = determine_spin_change(method, kind="triplet") + assert spin == 0.0 + + spin = determine_spin_change(method, kind="spin_flip") + assert spin == -1.0 + + def test_determine_spin_change_ip(self): + method = adcc.AdcMethod("ip-adc2") + + spin_alpha = determine_spin_change(method, kind="any", is_alpha=True) + spin_beta = determine_spin_change(method, kind="any", is_alpha=False) + + assert spin_alpha == -0.5 + assert spin_beta == +0.5 + + with pytest.raises(TypeError): + determine_spin_change(method, kind="any", is_alpha=None) + + def test_determine_spin_change_ea(self): + method = adcc.AdcMethod("ea-adc2") + + spin_alpha = determine_spin_change(method, kind="any", is_alpha=True) + spin_beta = determine_spin_change(method, kind="any", is_alpha=False) + + assert spin_alpha == +0.5 + assert spin_beta == -0.5 + + with pytest.raises(TypeError): + determine_spin_change(method, kind="any", is_alpha=None) + + def test_determine_spin_change_unknown_adc_type(self): + method = adcc.AdcMethod("adc2") + method.adc_type = "bla" + + with pytest.raises(ValueError, match="Unknown ADC type"): + determine_spin_change(method, kind="any") + + def test_estimate_n_guesses_pp(self): + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + + def test_estimate_n_guesses_ip(self): + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("ip-adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + + # Test different behaviour for IP-ADC(0/1) + matrix = adcc.AdcMatrix("ip-adc0", ground_state) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + assert 5 == estimate_n_guesses(matrix, n_states=5, singles_only=True) + assert 10 == estimate_n_guesses(matrix, n_states=10, singles_only=True) + + def test_estimate_n_guesses_ea(self): + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("ea-adc2", ground_state) + + # Check minimal number of guesses is 4 and at some point + # we get more than four guesses + assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + for i in range(3, 20): + assert i <= estimate_n_guesses(matrix, n_states=i, + singles_only=True) + + # Test different behaviour for EA-ADC(0/1) + matrix = adcc.AdcMatrix("ea-adc0", ground_state) + assert 2 == estimate_n_guesses(matrix, n_states=1, singles_only=True) + assert 2 == estimate_n_guesses(matrix, n_states=2, singles_only=True) + assert 3 == estimate_n_guesses(matrix, n_states=3, singles_only=True) + def assert_symmetry_no_spin_change(self, matrix, guess, block, spin_block_symmetrisation): """ @@ -183,6 +281,152 @@ def assert_symmetry_spin_flip(self, matrix, guess, block): has_babb = np.max(np.abs(gtd[noa:, :nCa, nva:, nva:])) > 0 assert has_aaab or has_aaba or has_abbb or has_babb + def assert_symmetry_ip(self, matrix, guess, block, is_alpha): + """ + Assert a guess vector has the correct symmetry for alpha/beta detachment + """ + # Extract useful quantities + mospaces = matrix.mospaces + nCa = noa = mospaces.n_orbs_alpha("o1") + nCb = nob = mospaces.n_orbs_beta("o1") + nva = mospaces.n_orbs_alpha("v1") + nvb = mospaces.n_orbs_beta("v1") + if mospaces.has_core_occupied_space: + nCa = mospaces.n_orbs_alpha("o2") + nCb = mospaces.n_orbs_beta("o2") + + # Singles + gts = guess.h.to_ndarray() + assert gts.shape == (nCa + nCb,) + if is_alpha: + assert np.max(np.abs(gts[nCa:])) == 0 + else: + assert np.max(np.abs(gts[:nCa])) == 0 + + # Doubles + if "phh" not in matrix.axis_blocks: + return + + gtd = guess.phh.to_ndarray() + assert gtd.shape == (noa + nob, nCa + nCb, nva + nvb) + + if is_alpha: + assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a + assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a + assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b + assert np.max(np.abs(gtd[noa:, nCa:, :nva])) == 0 # bb->a + else: + assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b + assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b + assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a + assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b + + if matrix.reference_state.restricted: + # Restricted automatically means alpha ionization + # Thus forbid spin-flip blocks with right spin + assert np.max(np.abs(gtd[:noa, :nCa, nva:])) == 0 # aa->b + + if not matrix.is_core_valence_separated: + assert_array_equal(gtd.transpose((1, 0, 2)), -gtd) + + if block == "h": + if is_alpha: + assert np.max(np.abs(gtd[:noa, nCa:, nva:])) == 0 # ab->b + assert np.max(np.abs(gtd[noa:, :nCa, nva:])) == 0 # ba->b + assert np.max(np.abs(gtd[:noa, :nCa, :nva])) == 0 # aa->a + + assert np.max(np.abs(gts[:nCa])) > 0 # has_alpha + else: + assert np.max(np.abs(gtd[:noa, nCa:, :nva])) == 0 # ab->a + assert np.max(np.abs(gtd[noa:, :nCa, :nva])) == 0 # ba->a + assert np.max(np.abs(gtd[noa:, nCa:, nva:])) == 0 # bb->b + + assert np.max(np.abs(gts[nCa:])) > 0 # has_beta + elif block == "phh": + if is_alpha: + assert np.max(np.abs(gts[:nCa])) == 0 + has_aaa = np.max(np.abs(gtd[:noa, :nCa, :nva])) > 0 + has_abb = np.max(np.abs(gtd[:noa, nCa:, nva:])) > 0 + has_bab = np.max(np.abs(gtd[noa:, :nCa, nva:])) > 0 + assert has_aaa or has_abb or has_bab + else: + assert np.max(np.abs(gts[nCa:])) == 0 + has_aba = np.max(np.abs(gtd[:noa, nCa:, :nva])) > 0 + has_baa = np.max(np.abs(gtd[noa:, :nCa, :nva])) > 0 + has_bbb = np.max(np.abs(gtd[noa:, nCa:, nva:])) > 0 + assert has_aba or has_baa or has_bbb + + def assert_symmetry_ea(self, matrix, guess, block, is_alpha): + """ + Assert a guess vector has the correct symmetry for alpha/beta attachment + """ + # Extract useful quantities + mospaces = matrix.mospaces + noa = mospaces.n_orbs_alpha("o1") + nob = mospaces.n_orbs_beta("o1") + nva = mospaces.n_orbs_alpha("v1") + nvb = mospaces.n_orbs_beta("v1") + + # Singles + gts = guess.p.to_ndarray() + assert gts.shape == (nva + nvb,) + if is_alpha: + assert np.max(np.abs(gts[nva:])) == 0 + else: + assert np.max(np.abs(gts[:nva])) == 0 + + # Doubles + if "pph" not in matrix.axis_blocks: + return + + gtd = guess.pph.to_ndarray() + assert gtd.shape == (noa + nob, nva + nvb, nva + nvb) + + if is_alpha: + assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab + assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba + assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb + assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb + else: + assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab + assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba + assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa + assert np.max(np.abs(gtd[noa:, :nva, :nva])) == 0 # b->aa + + if matrix.reference_state.restricted: + # Restricted automatically means alpha attachment + # Thus forbid spin-flip blocks with right spin + assert np.max(np.abs(gtd[:noa, nva:, nva:])) == 0 # a->bb + + assert_array_equal(gtd.transpose((0, 2, 1)), -gtd) + + if block == "p": + if is_alpha: + assert np.max(np.abs(gtd[noa:, :nva, nva:])) == 0 # b->ab + assert np.max(np.abs(gtd[noa:, nva:, :nva])) == 0 # b->ba + assert np.max(np.abs(gtd[:noa, :nva, :nva])) == 0 # a->aa + + assert np.max(np.abs(gts[:nva])) > 0 # has_alpha + else: + assert np.max(np.abs(gtd[:noa, :nva, nva:])) == 0 # a->ab + assert np.max(np.abs(gtd[:noa, nva:, :nva])) == 0 # a->ba + assert np.max(np.abs(gtd[noa:, nva:, nva:])) == 0 # b->bb + + assert np.max(np.abs(gts[nva:])) > 0 # has_beta + elif block == "pph": + if is_alpha: + assert np.max(np.abs(gts[:nva])) == 0 + has_aaa = np.max(np.abs(gtd[:noa, :nva, :nva])) > 0 + has_bab = np.max(np.abs(gtd[noa:, :nva, nva:])) > 0 + has_bba = np.max(np.abs(gtd[noa:, nva:, :nva])) > 0 + assert has_aaa or has_bab or has_bba + else: + assert np.max(np.abs(gts[nva:])) == 0 + has_aab = np.max(np.abs(gtd[:noa, :nva, nva:])) > 0 + has_aba = np.max(np.abs(gtd[:noa, nva:, :nva])) > 0 + has_bbb = np.max(np.abs(gtd[noa:, nva:, nva:])) > 0 + assert has_aab or has_aba or has_bbb + def assert_orthonormal(self, guesses): for (i, gi) in enumerate(guesses): for (j, gj) in enumerate(guesses): @@ -190,10 +434,10 @@ def assert_orthonormal(self, guesses): assert adcc.dot(gi, gj) == approx(ref) def assert_guess_values(self, matrix, block, guesses, spin_flip=False, - triplet=False): + triplet=False, is_alpha: bool = None): """ - Assert that the guesses correspond to the smallest - diagonal values. + Assert that the provided guesses correspond to the smallest + allowed diagonal elements for the requested block. """ # Extract useful quantities mospaces = matrix.mospaces @@ -204,40 +448,43 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, # Make a list of diagonal indices, ordered by the corresponding # diagonal values - sidcs = None - if block == "ph": - diagonal = matrix.diagonal().ph.to_ndarray() + diagonal = matrix.diagonal().get(block).to_ndarray() - # Build list of indices, which would sort the diagonal - sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()), - diagonal.shape)) - assert sidcs.shape[0] == 1 + # Doubles guesses are constructed from the 0th order diagonal + if matrix.method.level.to_int() > 1 \ + and not matrix.method.name.endswith("adc2"): + if block == "pphh": + diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0( + matrix.reference_state + ).pphh.to_ndarray() + elif block == "phh": + diagonal = adcc.adc_ip.matrix.diagonal_phh_phh_0( + matrix.reference_state + ).phh.to_ndarray() + elif block == "pph": + diagonal = adcc.adc_ea.matrix.diagonal_pph_pph_0( + matrix.reference_state + ).pph.to_ndarray() + + # Build list of indices, which would sort the diagonal + order = np.argsort(diagonal.ravel()) + sidcs = list(zip(*np.unravel_index(order, diagonal.shape))) + assert sidcs + + if block == "ph": if spin_flip: - sidcs = [idx for idx in sidcs[0] + sidcs = [idx for idx in sidcs if idx[0] < nCa and idx[1] >= nva] else: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs if any((idx[0] >= nCa and idx[1] >= nva, idx[0] < nCa and idx[1] < nva)) # noqa: E221 ] elif block == "pphh": - # the doubles guesses are constructed from the 0th order diagonal - if matrix.method.name.endswith("adc2"): - diagonal = matrix.diagonal().pphh.to_ndarray() - else: - diagonal = adcc.adc_pp.matrix.diagonal_pphh_pphh_0( - matrix.reference_state - ).pphh.to_ndarray() - - # Build list of indices, which would sort the diagonal - sidcs = np.dstack(np.unravel_index(np.argsort(diagonal.ravel()), - diagonal.shape)) - - assert sidcs.shape[0] == 1 if spin_flip: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] >= nva, # noqa: E221,E501 idx[0] < noa and idx[1] < nCa and idx[2] >= nva and idx[3] < nva, # noqa: E221,E501 idx[0] < noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501 @@ -245,7 +492,7 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, ] else: sidcs = [ - idx for idx in sidcs[0] + idx for idx in sidcs # aaaa / bbbb / abab / baba / abba / baab if any((idx[0] < noa and idx[1] < nCa and idx[2] < nva and idx[3] < nva, # noqa: E221,E501 idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva and idx[3] >= nva, # noqa: E221,E501 @@ -266,12 +513,64 @@ def assert_guess_values(self, matrix, block, guesses, spin_flip=False, sidcs = [idx for idx in sidcs if idx[2] != idx[3]] if not matrix.is_core_valence_separated: sidcs = [idx for idx in sidcs if idx[0] != idx[1]] + elif block == "h": + # IP-ADC singles + if is_alpha: + sidcs = [idx for idx in sidcs if idx[0] < noa] + else: + sidcs = [idx for idx in sidcs if idx[0] >= noa] + elif block == "phh": + # IP-ADC doubles + if is_alpha: + sidcs = [ + idx for idx in sidcs + # aaa / abb / bab + if any(( + idx[0] < noa and idx[1] < nCa and idx[2] < nva, + idx[0] < noa and idx[1] >= nCa and idx[2] >= nva, + idx[0] >= noa and idx[1] < nCa and idx[2] >= nva)) + ] + else: + sidcs = [ + idx for idx in sidcs + # aba / baa / bbb + if any(( + idx[0] < noa and idx[1] >= nCa and idx[2] < nva, + idx[0] >= noa and idx[1] < nCa and idx[2] < nva, + idx[0] >= noa and idx[1] >= nCa and idx[2] >= nva)) + ] + sidcs = [idx for idx in sidcs if idx[0] != idx[1]] + elif block == "p": + # EA-ADC singles + if is_alpha: + sidcs = [idx for idx in sidcs if idx[0] < nva] + else: + sidcs = [idx for idx in sidcs if idx[0] >= nva] + elif block == "pph": + # EA-ADC doubles + if is_alpha: + sidcs = [ + idx for idx in sidcs + if any(( + idx[0] < noa and idx[1] < nva and idx[2] < nva, + idx[0] >= noa and idx[1] < nva and idx[2] >= nva, + idx[0] >= noa and idx[1] >= nva and idx[2] < nva)) + ] + else: + sidcs = [ + idx for idx in sidcs + if any(( + idx[0] < noa and idx[1] < nva and idx[2] >= nva, + idx[0] < noa and idx[1] >= nva and idx[2] < nva, + idx[0] >= noa and idx[1] >= nva and idx[2] >= nva)) + ] + sidcs = [idx for idx in sidcs if idx[1] != idx[2]] # Group the indices by corresponding diagonal value def grouping(x): return np.round(diagonal[tuple(x)], decimals=12) gidcs = [[tuple(gitem) for gitem in group] - for _, group in itertools.groupby(sidcs, grouping)] + for key, group in itertools.groupby(sidcs, grouping)] igroup = 0 # The current diagonal value group we are in for (i, guess) in enumerate(guesses): # Extract indices of non-zero elements @@ -328,9 +627,47 @@ def base_test_spin_flip(self, system: str, case: str, method: str, block: str, self.assert_orthonormal(guesses) self.assert_guess_values(matrix, block, guesses, spin_flip=True) - @pytest.mark.parametrize("method", singles_methods) + def base_test_ip(self, system: str, case: str, method: str, block: str, + is_alpha: bool, max_guesses: int = 10): + """ + Test IP-ADC guess construction for alpha/beta detachment + """ + hf = testdata_cache.refstate(system, case=case) + matrix = adcc.AdcMatrix(method, hf) + spin_change = -0.5 if is_alpha else +0.5 + for n_guesses in range(3, max_guesses + 1): + guesses = adcc.guess.guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=spin_change, + is_alpha=is_alpha + ) + assert len(guesses) == n_guesses + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha) + + def base_test_ea(self, system: str, case: str, method: str, block: str, + is_alpha: bool, max_guesses: int = 10): + """ + Test EA-ADC guess construction for alpha/beta attachment + """ + hf = testdata_cache.refstate(system, case=case) + matrix = adcc.AdcMatrix(method, hf) + spin_change = +0.5 if is_alpha else -0.5 + for n_guesses in range(1, max_guesses + 1): + guesses = adcc.guess.guesses_from_diagonal( + matrix, n_guesses, block=block, spin_change=spin_change, + is_alpha=is_alpha + ) + assert len(guesses) == n_guesses + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + self.assert_guess_values(matrix, block, guesses, is_alpha=is_alpha) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", h2o_sto3g.cases) - def test_singles_h2o(self, method: str, case: str): + def test_singles_h2o_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") guesses = { # fewer guesses available @@ -342,9 +679,9 @@ def test_singles_h2o(self, method: str, case: str): max_guesses=guesses.get(case, 10) ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", h2o_sto3g.cases) - def test_doubles_h2o(self, method: str, case: str): + def test_doubles_h2o_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") guesses = { # fewer ocvv guesses available @@ -355,9 +692,41 @@ def test_doubles_h2o(self, method: str, case: str): max_guesses=guesses.get(case, 5) ) - @pytest.mark.parametrize("method", singles_methods) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases(AdcType.IP)) + def test_singles_h2o_ip(self, method: str, case: str): + self.base_test_ip( + "h2o_sto3g", case, method, block="h", is_alpha=True, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases(AdcType.IP)) + def test_doubles_h2o_ip(self, method: str, case: str): + self.base_test_ip( + "h2o_sto3g", case, method, block="phh", is_alpha=True, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases(AdcType.EA)) + def test_singles_h2o_ea(self, method: str, case: str): + self.base_test_ea( + "h2o_sto3g", case, method, block="p", is_alpha=True, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", h2o_sto3g.filter_cases(AdcType.EA)) + def test_doubles_h2o_ea(self, method: str, case: str): + self.base_test_ea( + "h2o_sto3g", case, method, block="pph", is_alpha=True, + max_guesses=4 + ) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", cn_sto3g.cases) - def test_singles_cn(self, method: str, case: str): + def test_singles_cn_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") guesses = { # fewer guesses available @@ -368,9 +737,9 @@ def test_singles_cn(self, method: str, case: str): max_guesses=guesses.get(case, 10) ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", cn_sto3g.cases) - def test_doubles_cn(self, method: str, case: str): + def test_doubles_cn_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") self.base_test_no_spin_change( @@ -378,9 +747,46 @@ def test_doubles_cn(self, method: str, case: str): max_guesses=5 ) - @pytest.mark.parametrize("method", singles_methods) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases(AdcType.IP)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_cn_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "cn_sto3g", case, method, block="h", is_alpha=is_alpha, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases(AdcType.IP)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_cn_ip(self, method: str, case: str, is_alpha: bool): + case = "gen" + self.base_test_ip( + "cn_sto3g", case, method, block="phh", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases(AdcType.EA)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_cn_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "cn_sto3g", case, method, block="p", is_alpha=is_alpha, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", cn_sto3g.filter_cases(AdcType.EA)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_cn_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "cn_sto3g", case, method, block="pph", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_pp) @pytest.mark.parametrize("case", hf_631g.cases) - def test_singles_hf(self, method: str, case: str): + def test_singles_hf_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") self.base_test_spin_flip( @@ -388,9 +794,9 @@ def test_singles_hf(self, method: str, case: str): max_guesses=10 ) - @pytest.mark.parametrize("method", doubles_methods) + @pytest.mark.parametrize("method", doubles_methods_pp) @pytest.mark.parametrize("case", hf_631g.cases) - def test_doubles_hf(self, method: str, case: str): + def test_doubles_hf_pp(self, method: str, case: str): if "cvs" in case and method == "adc4": pytest.skip("CVS-ADC(4) not implemented") self.base_test_spin_flip( @@ -398,10 +804,46 @@ def test_doubles_hf(self, method: str, case: str): max_guesses=5 ) + @pytest.mark.parametrize("method", singles_methods_ip) + @pytest.mark.parametrize("case", hf_631g.filter_cases(AdcType.IP)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_hf_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "hf_631g", case, method, block="h", is_alpha=is_alpha, + max_guesses=3 + ) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("case", hf_631g.filter_cases(AdcType.IP)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_hf_ip(self, method: str, case: str, is_alpha: bool): + self.base_test_ip( + "hf_631g", case, method, block="phh", is_alpha=is_alpha, + max_guesses=5 + ) + + @pytest.mark.parametrize("method", singles_methods_ea) + @pytest.mark.parametrize("case", hf_631g.filter_cases(AdcType.EA)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_singles_hf_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "hf_631g", case, method, block="p", is_alpha=is_alpha, + max_guesses=1 + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("case", hf_631g.filter_cases(AdcType.EA)) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_doubles_hf_ea(self, method: str, case: str, is_alpha: bool): + self.base_test_ea( + "hf_631g", case, method, block="pph", is_alpha=is_alpha, + max_guesses=5 + ) + # # Tests against reference values # - def base_reference(self, matrix, ref): + def base_reference_pp(self, matrix, ref): symmetrisations = ["none"] if matrix.reference_state.restricted: symmetrisations = ["symmetric", "antisymmetric"] @@ -423,15 +865,234 @@ def base_reference(self, matrix, ref): nonzeros = np.dstack(np.where(guess_b != 0)) assert nonzeros.shape[0] == 1 nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] - values = guess_b[guess_b != 0] - assert nonzeros == ref_sb[i][0] - assert_array_equal(values, np.array(ref_sb[i][1])) + indices_sorted = tuple(sorted(nonzeros)) + indices_ref_sorted = tuple(sorted(ref_sb[i][0])) + assert indices_sorted == indices_ref_sorted + + def base_reference_degenerate_pp(self, matrix, ref): + """ + Validate PP guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + symmetrisations = ["none"] + if matrix.reference_state.restricted: + symmetrisations = ["symmetric", "antisymmetric"] + + for block in ["ph", "pphh"]: + for symm in symmetrisations: + ref_sb = ref[(block, symm)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block, spin_change=0, + spin_block_symmetrisation=symm + ) + assert len(guesses) == len(ref_sb) + for gs in guesses: + self.assert_symmetry_no_spin_change(matrix, gs, block, symm) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) + + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) + + def base_reference_ip(self, matrix, ref, is_alpha=True): + spin_change = -0.5 if is_alpha else +0.5 + for block in ["h", "phh"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + for (i, guess) in enumerate(guesses): + guess_b = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(guess_b != 0)) + assert nonzeros.shape[0] == 1 + nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] + values = guess_b[guess_b != 0] + assert nonzeros == ref_sb[i][0] + assert_array_equal(values, np.array(ref_sb[i][1])) + + def base_reference_degenerate_ip(self, matrix, ref, is_alpha=True): + """ + Validate IP guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + spin_change = -0.5 if is_alpha else +0.5 + for block in ["h", "phh"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ip(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) + + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) - @pytest.mark.parametrize("method", doubles_methods) - def test_reference_h2o(self, method: str): + def base_reference_ea(self, matrix, ref, is_alpha=True): + spin_change = +0.5 if is_alpha else -0.5 + for block in ["p", "pph"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + for (i, guess) in enumerate(guesses): + guess_b = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(guess_b != 0)) + assert nonzeros.shape[0] == 1 + nonzeros = [tuple(nzitem) for nzitem in nonzeros[0]] + values = guess_b[guess_b != 0] + assert nonzeros == ref_sb[i][0] + assert_array_equal(values, np.array(ref_sb[i][1])) + + def base_reference_degenerate_ea(self, matrix, ref, is_alpha=True): + """ + Validate EA guesses in presence of orbital degeneracies. + + Ensures that: + - The number of generated guesses matches the reference manifold size. + - Each guess belongs to the correct diagonal energy group. + - Ordering within degenerate subspaces is not enforced. + """ + spin_change = +0.5 if is_alpha else -0.5 + for block in ["p", "pph"]: + ref_sb = ref[(block, is_alpha)] + guesses = adcc.guess.guesses_from_diagonal( + matrix, len(ref_sb), block=block, spin_change=spin_change + ) + assert len(guesses) == len(ref_sb) + + for gs in guesses: + self.assert_symmetry_ea(matrix, gs, block, is_alpha) + self.assert_orthonormal(guesses) + + # Collect diagonal energies of actual guesses + diag_block = matrix.diagonal()[block].to_ndarray() + actual_energies = [] + for guess in guesses: + arr = guess[block].to_ndarray() + nonzeros = np.dstack(np.where(arr != 0)) + assert nonzeros.shape[0] == 1 + idx = tuple(nonzeros[0][0]) + actual_energies.append(diag_block[idx]) + + # Collect diagonal energies of reference guesses + ref_energies = [] + for ref_entry in ref_sb: + ref_indices = ref_entry[0] + values = [diag_block[idx] for idx in ref_indices] + + # enforce internal degeneracy consistency + np.testing.assert_allclose( + values, [values[0]] * len(values), + rtol=1e-12, atol=1e-14 + ) + + ref_energies.append(values[0]) + + # Compare as multisets + np.testing.assert_allclose( + sorted(actual_energies), + sorted(ref_energies), + rtol=1e-12, + atol=1e-14 + ) + + @pytest.mark.parametrize("method", doubles_methods_pp) + def test_reference_h2o_pp(self, method: str): + hf = testdata_cache.refstate("h2o_sto3g", "gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + self.base_reference_pp(matrix=matrix, ref=self.get_ref_h2o_pp()) + + @pytest.mark.parametrize("method", doubles_methods_ip) + def test_reference_h2o_ip(self, method: str): + hf = testdata_cache.refstate("h2o_sto3g", "gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + self.base_reference_ip(matrix=matrix, ref=self.get_ref_h2o_ip()) + + @pytest.mark.parametrize("method", doubles_methods_ea) + def test_reference_h2o_ea(self, method: str): hf = testdata_cache.refstate("h2o_sto3g", "gen") matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) - self.base_reference(matrix=matrix, ref=self.get_ref_h2o()) + self.base_reference_ea(matrix=matrix, ref=self.get_ref_h2o_ea()) # NOTE: This test is a bit weird: the order of the guesses is # ill defined, because some orbitals are degenerate for cn sto3g: @@ -443,13 +1104,46 @@ def test_reference_h2o(self, method: str): # against hard coded reference data. The test against numpy above should be # sufficient. - # @pytest.mark.parametrize("method", doubles_methods) - # def test_reference_cn(self, method: str): - # hf = testdata_cache.refstate("cn_sto3g", case="gen") - # matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) - # self.base_reference(matrix=matrix, ref=self.get_ref_cn()) + # Current workaround: Compare guess energies rather than exact ordering for + # these cases by calling 'base_reference_degenerate_{adc_type}()' - def get_ref_h2o(self): + @pytest.mark.parametrize("method", doubles_methods_pp) + def test_reference_cn_pp(self, method: str): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + # NOTE: doubles guesses for higher ADC levels are constructed + # from the ADC(2) zeroth-order diagonal. + # We enforce this here explicitly to avoid method-dependent + # degeneracy reordering. + matrix._diagonal = adcc.AdcMatrix(method="adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_pp(matrix=matrix, ref=self.get_ref_cn_pp()) + + @pytest.mark.parametrize("method", doubles_methods_ip) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_reference_cn_ip(self, method: str, is_alpha: bool): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + matrix._diagonal = adcc.AdcMatrix( + method="ip-adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_ip( + matrix=matrix, ref=self.get_ref_cn_ip(), is_alpha=is_alpha + ) + + @pytest.mark.parametrize("method", doubles_methods_ea) + @pytest.mark.parametrize("is_alpha", [True, False]) + def test_reference_cn_ea(self, method: str, is_alpha: bool): + hf = testdata_cache.refstate("cn_sto3g", case="gen") + matrix = adcc.AdcMatrix(method=method, hf_or_mp=hf) + if not method.endswith("adc2"): + matrix._diagonal = adcc.AdcMatrix( + method="ea-adc2", hf_or_mp=hf).diagonal() + self.base_reference_degenerate_ea( + matrix=matrix, ref=self.get_ref_cn_ea(), is_alpha=is_alpha + ) + + def get_ref_h2o_pp(self): sq8 = 1 / np.sqrt(8) sq12 = 1 / np.sqrt(12) sq48 = 1 / np.sqrt(48) @@ -535,7 +1229,7 @@ def get_ref_h2o(self): ], } - def get_ref_cn(self): + def get_ref_cn_pp(self): sq8 = 1 / np.sqrt(8) return { ("ph", "none"): [ @@ -565,3 +1259,113 @@ def get_ref_cn(self): [0.5, -0.5, -0.5, 0.5]), ], } + + def get_ref_h2o_ip(self): + sq6 = 1 / np.sqrt(6) + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + return { + ("h", True): [ + ([(4, )], [1]), + ([(3, )], [1]), + ([(2, )], [1]), + ([(1, )], [1]), + ([(0, )], [1]) + ], + ("phh", True): [ + ([(4, 9, 2), (9, 4, 2)], asymm), + ([(3, 4, 0), (3, 9, 2), (4, 3, 0), + (4, 8, 2), (8, 4, 2), (9, 3, 2)], + [-sq6, -sq6, sq6, -sq6, sq6, sq6]), + ([(3, 8, 2), (8, 3, 2)], asymm), + ([(4, 9, 3), (9, 4, 3)], asymm), + ([(3, 4, 1), (3, 9, 3), (4, 3, 1), + (4, 8, 3), (8, 4, 3), (9, 3, 3)], + [-sq6, -sq6, sq6, -sq6, sq6, sq6]) + ], + } + + def get_ref_h2o_ea(self): + sq6 = 1 / np.sqrt(6) + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + return { + ("p", True): [ + ([(0, )], [1]), + ([(1, )], [1]) + ], + ("pph", True): [ + ([(9, 0, 2), (9, 2, 0)], asymm), + ([(8, 0, 2), (8, 2, 0)], asymm), + ([(4, 0, 1), (4, 1, 0), (9, 0, 3), + (9, 1, 2), (9, 2, 1), (9, 3, 0)], + [-sq6, sq6, -sq6, -sq6, sq6, sq6]), + ([(3, 0, 1), (3, 1, 0), (8, 0, 3), + (8, 1, 2), (8, 2, 1), (8, 3, 0)], + [-sq6, sq6, -sq6, -sq6, sq6, sq6]), + ([(7, 0, 2), (7, 2, 0)], asymm) + ], + } + + def get_ref_cn_ip(self): + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)] + return { + ("h", True): [ + ([(6, )], [1]), + ([(4, )], [1]), # occ. 4 and 5 are degenerate + ([(5, )], [1]), # occ. 4 and 5 are degenerate + ([(3, )], [1]), + ([(2, )], [1]) + ], + ("h", False): [ + ([(11, )], [1]), + ([(12, )], [1]), + ([(10, )], [1]), + ([(9, )], [1]), + ([(8, )], [1]) + ], + ("phh", True): [ + ([(6, 11, 3), (11, 6, 3)], asymm1), + ([(6, 12, 3), (12, 6, 3)], asymm1), + ([(4, 11, 3), (11, 4, 3)], asymm), + ([(4, 12, 3), (12, 4, 3)], asymm), + ([(5, 11, 3), (11, 5, 3)], asymm1) + ], + ("phh", False): [ + ([(11, 12, 3), (12, 11, 3)], asymm), + ([(10, 11, 3), (11, 10, 3)], asymm), + ([(10, 12, 3), (12, 10, 3)], asymm), + ([(6, 11, 0), (11, 6, 0)], asymm1), + ([(6, 12, 0), (12, 6, 0)], asymm1) + ], + } + + def get_ref_cn_ea(self): + asymm = [1 / np.sqrt(2), -1 / np.sqrt(2)] + asymm1 = [-1 / np.sqrt(2), 1 / np.sqrt(2)] + return { + ("p", True): [ + ([(0, )], [1]), + ([(1, )], [1]), + ([(2, )], [1]) + ], + ("p", False): [ + ([(3, )], [1]), + ([(4, )], [1]), + ([(5, )], [1]), + ([(6, )], [1]) + ], + ("pph", True): [ + ([(11, 0, 3), (11, 3, 0)], asymm), + ([(12, 0, 3), (12, 3, 0)], asymm), + ([(11, 1, 3), (11, 3, 1)], asymm1), + ([(12, 1, 3), (12, 3, 1)], asymm1), + ([(10, 0, 3), (10, 3, 0)], asymm) + ], + ("pph", False): [ + ([(6, 0, 3), (6, 3, 0)], asymm), + ([(6, 1, 3), (6, 3, 1)], asymm1), + ([(4, 0, 3), (4, 3, 0)], asymm), # occ. 4 and 5 are degenerate + ([(5, 0, 3), (5, 3, 0)], asymm), # occ. 4 and 5 are degenerate + ([(4, 1, 3), (4, 3, 1)], asymm1) + ], + } diff --git a/adcc/tests/properties_test.py b/adcc/tests/properties_test.py index 14ffd2c8..8a38501b 100644 --- a/adcc/tests/properties_test.py +++ b/adcc/tests/properties_test.py @@ -62,7 +62,8 @@ class TestProperties: @pytest.mark.parametrize("system,case,kind", cases) def test_transition_dipole_moments(self, system: str, case: str, kind: str, method: str, generator: str): - if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": + if "cvs" in case and AdcMethod(method).level.to_int() == 0 \ + and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") refdata = testdata_cache._load_data( @@ -95,7 +96,8 @@ def test_transition_dipole_moments(self, system: str, case: str, kind: str, @pytest.mark.parametrize("system,case,kind", cases) def test_oscillator_strengths(self, system: str, case: str, kind: str, method: str, generator: str): - if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": + if "cvs" in case and AdcMethod(method).level.to_int() == 0 \ + and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") refdata = testdata_cache._load_data( @@ -126,7 +128,8 @@ def test_oscillator_strengths(self, system: str, case: str, kind: str, @pytest.mark.parametrize("system,case,kind", cases) def test_state_dipole_moments(self, system: str, case: str, kind: str, method: str, generator: str): - if "cvs" in case and AdcMethod(method).level == 0 and generator == "adcman": + if "cvs" in case and AdcMethod(method).level.to_int() == 0 \ + and generator == "adcman": pytest.skip("No CVS-ADC(0) adcman reference data available.") refdata = testdata_cache._load_data( diff --git a/adcc/tests/testcases.py b/adcc/tests/testcases.py index f41e320d..d2780a01 100644 --- a/adcc/tests/testcases.py +++ b/adcc/tests/testcases.py @@ -94,6 +94,8 @@ def filter_cases(self, adc_type: AdcType) -> tuple[str, ...]: # since cvs is not (yet) implemented for IP. if adc_type is AdcType.PP: return self.cases + elif adc_type in (AdcType.IP, AdcType.EA): + return tuple(case for case in self.cases if "cvs" not in case) raise NotImplementedError(f"Filtering for adc type {adc_type} not " "implemented.") @@ -112,14 +114,26 @@ def validate(self): continue assert component in requirements assert getattr(self, requirements[component], None) is not None - # validate the PP-ADC kinds + # validate the IP/EA/PP-ADC kinds assert len(fields(self.kinds)) == 3 - assert not self.kinds.ip - assert not self.kinds.ea - if self.restricted: - assert all(kind in ["singlet", "triplet"] for kind in self.kinds.pp) - else: - assert all(kind in ["any", "spin_flip"] for kind in self.kinds.pp) + + if self.kinds.pp: + if self.restricted: + assert all(kind in ["singlet", "triplet"] + for kind in self.kinds.pp) + else: + assert all(kind in ["any", "spin_flip"] + for kind in self.kinds.pp) + if self.kinds.ip: + if self.restricted: + assert all(kind in ["doublet"] for kind in self.kinds.ip) + else: + assert all(kind in ["any"] for kind in self.kinds.ip) + if self.kinds.ea: + if self.restricted: + assert all(kind in ["doublet"] for kind in self.kinds.ea) + else: + assert all(kind in ["any"] for kind in self.kinds.ea) def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: @@ -127,9 +141,9 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: Transforms the given kinds to a list of keywords to request states of the corresponding kind in an adc calculation. """ - # singlet, triplet -> n_singlets, n_triplets - # any -> n_states - # spin_flip -> n_spin_flip + # singlet, doublet, triplet -> n_singlets, n_triplets + # any -> n_states + # spin_flip -> n_spin_flip ret = [] for kind in kinds: if kind == "any": @@ -199,8 +213,10 @@ def kinds_to_nstates(kinds: tuple[str, ...]) -> list[str]: def _init_test_cases() -> tuple[TestCase, ...]: test_cases: list[TestCase] = [] # some shared data - restricted_kinds = Kinds(pp=("singlet", "triplet")) - unrestricted_kinds = Kinds(pp=("any",)) + restricted_kinds = Kinds(pp=("singlet", "triplet"), + ip=("doublet",), + ea=("doublet",)) + unrestricted_kinds = Kinds(pp=("any",), ip=("any",), ea=("any",)) spin_flip_kinds = Kinds(pp=("spin_flip",)) # CH2NH2 ref_cases = ("gen", "cvs") diff --git a/adcc/tests/testdata_cache.py b/adcc/tests/testdata_cache.py index 08cedf3a..9c492cb3 100644 --- a/adcc/tests/testdata_cache.py +++ b/adcc/tests/testdata_cache.py @@ -1,12 +1,15 @@ from . import testcases from adcc.AdcMatrix import AdcMatrix +from adcc.AdcMethod import AdcMethod, AdcType from adcc.ExcitedStates import ExcitedStates +from adcc.ChargedExcitations import AttachedStates, DetachedStates from adcc.LazyMp import LazyMp from adcc.misc import cached_member_function from adcc.ReferenceState import ReferenceState from adcc.solver import EigenSolverStateBase -from adcc import hdf5io, guess_zero +from adcc import hdf5io +from adcc.guess import guess_zero, determine_spin_change from pathlib import Path from typing import Optional, Union @@ -118,11 +121,12 @@ def hfimport(self, system: Union[str, testcases.TestCase], @cached_member_function() def _load_data(self, system: Union[str, testcases.TestCase], method: str, case: str, source: str, - gs_density_order: Optional[int] = None) -> dict: + gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None) -> dict: """ Load the reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and if it is an alpha process for IP/EA. Source defines the source which generated the reference data, i.e., either adcman or adcc. """ @@ -132,7 +136,7 @@ def _load_data(self, system: Union[str, testcases.TestCase], system = testcases.get_by_filename(system).pop() return self._load_data( system, method=method, case=case, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) assert isinstance(system, testcases.TestCase) assert case in system.cases @@ -146,54 +150,72 @@ def _load_data(self, system: Union[str, testcases.TestCase], else: # adc data is one level deeper than mpdata: gs_density_order datafile = datadir / system.adcdata_file_name(source, method) key = f"{case}/{gs_density_order}" + if AdcMethod(method).adc_type in (AdcType.IP, AdcType.EA): + assert isinstance(is_alpha, bool) + spin = "alpha" if is_alpha else "beta" + key = f"{case}/{gs_density_order}/{spin}" if not datafile.exists(): raise FileNotFoundError(f"Missing reference data file {datafile}.") with h5py.File(datafile, "r") as hdf5_file: if key not in hdf5_file: - raise ValueError( - f"No data available for case {case} and gs_density_order " - f"{gs_density_order} in file {datafile}." - ) + if is_alpha is None: + raise ValueError( + f"No data available for case {case} and " + f"gs_density_order {gs_density_order} in file {datafile}." + ) + else: + raise ValueError( + f"No data available for case {case}, gs_density_order " + f"{gs_density_order} and spin {spin} in file {datafile}." + ) data = hdf5io.extract_group(hdf5_file[key]) return data def adcc_data(self, system: str, method: str, case: str, - gs_density_order: Optional[int] = None) -> dict: + gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None) -> dict: """ Load the adcc reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._load_data( system=system, method=method, case=case, - gs_density_order=gs_density_order, source="adcc" + gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha ) def adcman_data(self, system: str, method: str, case: str, - gs_density_order: Optional[int] = None) -> dict: + gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None) -> dict: """ Load the adcman reference data for the given system, method (mpn / adcn), reference case (cvs, fc, fv-cvs, ...) and optionally gs_density_order - (2, 3, sigma4+, ...). + (2, 3, sigma4+, ...) and optionally is_alpha for IP/EA data. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._load_data( system=system, method=method, case=case, - gs_density_order=gs_density_order, source="adcman" + gs_density_order=gs_density_order, source="adcman", + is_alpha=is_alpha ) @cached_member_function() def _make_mock_adc_state(self, system: Union[str, testcases.TestCase], method: str, case: str, kind: str, source: str, - gs_density_order: Optional[int] = None - ) -> ExcitedStates: + gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+). - Source refers to the source with which the data were generated - (adcman/adcc). - The excited states object is build on top of the loaded HF data and + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) and optionally is_alpha for IP/EA. Source refers to the + source with which the data were generated (adcman/adcc). + The states object is build on top of the loaded HF data and contains the eigenstates and eigenvalues of the loaded ADC data. """ if isinstance(system, str): @@ -203,7 +225,7 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase], system = testcases.get_by_filename(system).pop() return self._make_mock_adc_state( system, method=method, case=case, kind=kind, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) assert isinstance(system, testcases.TestCase) assert case in system.cases @@ -211,7 +233,7 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase], # load the adc data data = self._load_data( system, method=method, case=case, source=source, - gs_density_order=gs_density_order + gs_density_order=gs_density_order, is_alpha=is_alpha ) adc_data = data.get(kind, None) if adc_data is None: @@ -239,12 +261,15 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase], elif refstate.restricted and kind == "triplet": symm = "antisymmetric" spin_change = 0 + elif refstate.restricted and kind == "doublet": + symm = "none" elif kind in ["spin_flip", "any"]: symm = "none" - spin_change = 0 if kind == "any" else -1 else: raise ValueError(f"Unknown kind: {kind}") + spin_change = determine_spin_change(matrix.method, kind, is_alpha) + n_states = len(adc_data["eigenvalues"]) states.eigenvectors = [guess_zero(matrix, spin_change=spin_change, spin_block_symmetrisation=symm) @@ -261,34 +286,49 @@ def _make_mock_adc_state(self, system: Union[str, testcases.TestCase], evec[blocks[2]].set_from_ndarray( adc_data["eigenvectors_triples"][i], 1e-14 ) - return ExcitedStates(states) + + if matrix.method.adc_type is AdcType.PP: + return ExcitedStates(states) + elif matrix.method.adc_type is AdcType.IP: + return DetachedStates(states, is_alpha) + elif matrix.method.adc_type is AdcType.EA: + return AttachedStates(states, is_alpha) + else: + raise ValueError(f"Unknown ADC method: {method.name}") def adcc_states(self, system: str, method: str, kind: str, - case: str, gs_density_order: Optional[int] = None - ) -> ExcitedStates: + case: str, gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+) using the adcc eigenstates - and eigenvalues. + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) using the adcc eigenstates and eigenvalues. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._make_mock_adc_state( system, method=method, case=case, kind=kind, - gs_density_order=gs_density_order, source="adcc" + gs_density_order=gs_density_order, source="adcc", is_alpha=is_alpha ) def adcman_states(self, system: str, method: str, kind: str, - case: str, gs_density_order: Optional[int] = None - ) -> ExcitedStates: + case: str, gs_density_order: Optional[int] = None, + is_alpha: Optional[bool] = None + ) -> ExcitedStates | AttachedStates | DetachedStates: """ - Create an ExcitedStates instance for the given test case, method (adcn), - reference case (gen/cvs/fc/...), state kind (singlet/triplet/any/...) - and optionally gs_density_order (2/3/sigma4+) using the adcman eigenstates - and eigenvalues. + Create an ExcitedStates/AttachedStates/DetachedStates instance for the + given test case, method (adcn), reference case (gen/cvs/fc/...), + state kind (singlet/triplet/any/...) and optionally gs_density_order + (2/3/sigma4+) using the adcman eigenstates and eigenvalues. """ + if ("ip" in method or "ea" in method) and is_alpha is None: + is_alpha = True return self._make_mock_adc_state( system, method=method, case=case, kind=kind, - gs_density_order=gs_density_order, source="adcman" + gs_density_order=gs_density_order, source="adcman", + is_alpha=is_alpha ) @@ -303,7 +343,7 @@ def read_json_data(name: str) -> dict: return json.load(open(jsonfile, "r"), object_hook=_import_hook) -def _import_hook(data: dict): +def _import_hook(data: dict) -> dict: return {key: np.array(val) if isinstance(val, list) else val for key, val in data.items()} diff --git a/adcc/tests/workflow_test.py b/adcc/tests/workflow_test.py index 6dbc8e6d..930b94f4 100644 --- a/adcc/tests/workflow_test.py +++ b/adcc/tests/workflow_test.py @@ -26,21 +26,30 @@ from adcc import InputError from .testdata_cache import testdata_cache +from adcc.AdcMethod import AdcType class TestWorkflow: - def test_validate_state_parameters_rhf(self): + def test_validate_state_parameters_rhf_pp(self): from adcc.workflow import validate_state_parameters - - refstate = testdata_cache.refstate("h2o_sto3g", case="gen") - - assert 3, "any" == validate_state_parameters(refstate, n_states=3) - assert 4, "singlet" == validate_state_parameters(refstate, n_states=4, - kind="singlet") - assert 2, "triplet" == validate_state_parameters(refstate, n_states=2, - kind="triplet") - assert 2, "triplet" == validate_state_parameters(refstate, n_triplets=2) - assert 6, "singlet" == validate_state_parameters(refstate, n_singlets=6) + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + matrix.method = AdcMethod("adc2") + + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3) + assert (4, "singlet", None) == validate_state_parameters( + matrix, n_states=4, kind="singlet") + assert (2, "triplet", None) == validate_state_parameters( + matrix, n_states=2, kind="triplet") + assert (2, "triplet", None) == validate_state_parameters( + matrix, n_triplets=2) + assert (6, "singlet", None) == validate_state_parameters( + matrix, n_singlets=6) invalid_cases = [ dict(), # No states requested @@ -52,21 +61,30 @@ def test_validate_state_parameters_rhf(self): dict(n_states=2, n_spin_flip=2), # States of two sorts dict(n_triplets=2, kind="singlet"), # kind and n_ do not agree dict(n_states=2, kind="bla"), # Kind invaled + dict(n_states=2, kind="doublet"), # Kind invalid for PP-ADC + dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC + dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC ] for case in invalid_cases: with pytest.raises(InputError): - validate_state_parameters(refstate, **case) + validate_state_parameters(matrix, **case) - def test_validate_state_parameters_uhf(self): + def test_validate_state_parameters_uhf_pp(self): from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod - refstate = testdata_cache.refstate("cn_sto3g", case="gen") + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("adc2") - assert 3, "any" == validate_state_parameters(refstate, n_states=3, - kind="any") - assert 3, "any" == validate_state_parameters(refstate, n_states=3) - assert 2, "spin_flip" == validate_state_parameters(refstate, - n_spin_flip=2) + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", None) == validate_state_parameters( + matrix, n_states=3) + assert (2, "spin_flip", None) == validate_state_parameters( + matrix, n_spin_flip=2) invalid_cases = [ dict(), # No states requested @@ -77,15 +95,170 @@ def test_validate_state_parameters_uhf(self): dict(n_triplets=2, n_singlets=2), # States of two sorts dict(n_states=2, n_spin_flip=2), # States of two sorts dict(n_spin_flip=2, kind="singlet"), # kind and n_ do not agree - dict(n_states=2, kind="bla"), # Kind invaled + dict(n_states=2, kind="bla"), # Kind invalid dict(n_states=4, kind="singlet"), # UHF with singlets dict(n_states=2, kind="triplet"), # UHF with triplets dict(n_triplets=2), # UHF with triplets dict(n_singlets=6), # UHF with singlets + dict(n_doublets=3), # UHF with doublets (only restricted IP/EA) + dict(n_states=2, is_alpha=True), # Parameter only for IP/EA-ADC + dict(n_states=2, is_alpha=False), # Parameter only for IP/EA-ADC + ] + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_rhf_ip(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + matrix.method = AdcMethod("ip-adc2") + + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=False)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=True)) # restricted always beta + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_states=2, kind="doublet")) + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=2)) + assert (6, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=6, is_alpha=True)) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_doublets=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_singlets=2), # Kind invalid for IP/EA-ADC + dict(n_triplets=2), # Kind invalid for IP/EA-ADC + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + dict(n_states=2, n_spin_flip=2), # States of two sorts + dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree + ] + + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_uhf_ip(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("ip-adc2") + + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", False) == validate_state_parameters( + matrix, n_states=3, is_alpha=False) + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, is_alpha=True) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_states=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_doublets=2), # UHF with doublets + dict(n_states=2, kind="doublet"), # UHF with doublets + dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + ] + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_rhf_ea(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("h2o_sto3g", case="gen") + + # IP + matrix.method = AdcMethod("ea-adc2") + + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=False)) + assert (3, "any", True) == (validate_state_parameters( + matrix, n_states=3, is_alpha=True)) # restricted always beta + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_states=2, kind="doublet")) + assert (2, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=2)) + assert (6, "doublet", True) == (validate_state_parameters( + matrix, n_doublets=6, is_alpha=True)) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_doublets=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_singlets=2), # Kind invalid for IP/EA-ADC + dict(n_triplets=2), # Kind invalid for IP/EA-ADC + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean + dict(n_states=2, n_spin_flip=2), # States of two sorts + dict(n_doublets=2, kind="singlet"), # kind and n_ do not agree + ] + + for case in invalid_cases: + with pytest.raises(InputError): + validate_state_parameters(matrix, **case) + + def test_validate_state_parameters_uhf_ea(self): + from adcc.workflow import validate_state_parameters + from adcc.AdcMatrix import AdcMatrixlike + from adcc.AdcMethod import AdcMethod + + # Build empty AdcMatrixlike object and assign ref_state and method + matrix = AdcMatrixlike() + matrix.reference_state = testdata_cache.refstate("cn_sto3g", case="gen") + matrix.method = AdcMethod("ea-adc2") + + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, kind="any") + assert (3, "any", False) == validate_state_parameters( + matrix, n_states=3, is_alpha=False) + assert (3, "any", True) == validate_state_parameters( + matrix, n_states=3, is_alpha=True) + + invalid_cases = [ + dict(), # No states requested + dict(n_states=0), # No states requested + dict(n_states=-2), # Negative number of states requested + dict(n_states=2, kind="bla"), # Kind invalid + dict(n_doublets=2), # UHF with doublets + dict(n_states=2, kind="doublet"), # UHF with doublets + dict(n_singlets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_triplets=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_spin_flip=2), # Kind invalid for IP/EA-ADC and UHF + dict(n_states=2, is_alpha="yes"), # is_alpha not boolean + dict(n_states=2, is_alpha=1), # is_alpha not boolean ] for case in invalid_cases: with pytest.raises(InputError): - validate_state_parameters(refstate, **case) + validate_state_parameters(matrix, **case) def test_construct_adcmatrix(self): from adcc.workflow import construct_adcmatrix @@ -98,6 +271,8 @@ def test_construct_adcmatrix(self): res = construct_adcmatrix(hfdata, method="adc3") assert isinstance(res, adcc.AdcMatrix) assert res.method == adcc.AdcMethod("adc3") + assert res.method.adc_type is AdcType.PP + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [] @@ -105,26 +280,52 @@ def test_construct_adcmatrix(self): res = construct_adcmatrix(hfdata, method="cvs-adc3", core_orbitals=1) assert isinstance(res, adcc.AdcMatrix) assert res.method == adcc.AdcMethod("cvs-adc3") + assert res.method.adc_type is AdcType.PP + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [0, 7] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [] res = construct_adcmatrix(hfdata, method="adc2", frozen_core=1) + assert res.method.adc_type is AdcType.PP + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [0, 7] assert res.mospaces.frozen_virtual == [] res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1) + assert res.method.adc_type is AdcType.PP + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [] assert res.mospaces.frozen_virtual == [6, 13] res = construct_adcmatrix(hfdata, method="adc2", frozen_virtual=1, frozen_core=1) + assert res.method.adc_type is AdcType.PP + assert res.axis_blocks == ["ph", "pphh"] assert res.mospaces.core_orbitals == [] assert res.mospaces.frozen_core == [0, 7] assert res.mospaces.frozen_virtual == [6, 13] + res = construct_adcmatrix(hfdata, method="ip-adc3") + assert isinstance(res, adcc.AdcMatrix) + assert res.method == adcc.AdcMethod("ip-adc3") + assert res.method.adc_type is AdcType.IP + assert res.axis_blocks == ["h", "phh"] + assert res.mospaces.core_orbitals == [] + assert res.mospaces.frozen_core == [] + assert res.mospaces.frozen_virtual == [] + + res = construct_adcmatrix(hfdata, method="ea-adc2") + assert isinstance(res, adcc.AdcMatrix) + assert res.method == adcc.AdcMethod("ea-adc2") + assert res.method.adc_type is AdcType.EA + assert res.axis_blocks == ["p", "pph"] + assert res.mospaces.core_orbitals == [] + assert res.mospaces.frozen_core == [] + assert res.mospaces.frozen_virtual == [] + invalid_cases = [ dict(), # Missing method dict(method="dadadad"), # Unknown method @@ -195,7 +396,7 @@ def test_construct_adcmatrix(self): match=r"^Ignored frozen_virtual parameter"): construct_adcmatrix(mtx_cvs, frozen_virtual=1) - def test_diagonalise_adcmatrix(self): + def test_diagonalise_adcmatrix_pp(self): from adcc.workflow import diagonalise_adcmatrix system = "h2o_sto3g" @@ -209,11 +410,6 @@ def test_diagonalise_adcmatrix(self): matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) - res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, - eigensolver="davidson") - assert res.converged - assert res.eigenvalues[:n_states] == approx(ref_singlets[:n_states]) - guesses = adcc.guesses_singlet(matrix, n_guesses=6, block="ph") res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, guesses=guesses) @@ -222,34 +418,91 @@ def test_diagonalise_adcmatrix(self): with pytest.raises(InputError): # Too low tolerance # SCF tolerance = 1e-14 currently + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="davidson", + conv_tol=1e-15) + + with pytest.raises(InputError): # Wrong solver method + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="blubber") + + with pytest.raises(ValueError): # Too few guesses res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, eigensolver="davidson", + guesses=guesses) + + def test_diagonalise_adcmatrix_ip(self): + from adcc.workflow import diagonalise_adcmatrix + # pytest.skip("adcman referencedata not yet available") + system = "h2o_sto3g" + case = "gen" + method = "ip-adc2" + kind = "doublet" + + refdata = testdata_cache.adcman_data(system, method=method, case=case) + ref_doublets = refdata[kind]["eigenvalues"] + n_states = min(len(ref_doublets), 3) + + matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) + + guesses = adcc.guesses_doublet(matrix, n_guesses=6, block="h", + is_alpha=True) + res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, + guesses=guesses, is_alpha=True) + assert res.converged + assert res.eigenvalues[:n_states] == approx(ref_doublets[:n_states]) + + with pytest.raises(InputError): # Too low tolerance + # SCF tolerance = 1e-14 currently + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="davidson", conv_tol=1e-15) with pytest.raises(InputError): # Wrong solver method res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, - eigensolver="blubber") + guesses=guesses, eigensolver="blubber") - with pytest.raises(InputError): # Too few guesses + with pytest.raises(ValueError): # Too few guesses res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, eigensolver="davidson", guesses=guesses) - def test_estimate_n_guesses(self): - from adcc.workflow import estimate_n_guesses + def test_diagonalise_adcmatrix_ea(self): + from adcc.workflow import diagonalise_adcmatrix + system = "h2o_sto3g" + case = "gen" + method = "ea-adc2" + kind = "doublet" - refstate = testdata_cache.refstate("h2o_sto3g", case="gen") - ground_state = adcc.LazyMp(refstate) - matrix = adcc.AdcMatrix("adc2", ground_state) + refdata = testdata_cache.adcman_data(system, method=method, case=case) + ref_doublets = refdata[kind]["eigenvalues"] + n_states = min(len(ref_doublets), 3) - # Check minimal number of guesses is 4 and at some point - # we get more than four guesses - assert 4 == estimate_n_guesses(matrix, n_states=1, singles_only=True) - assert 4 == estimate_n_guesses(matrix, n_states=2, singles_only=True) - for i in range(3, 20): - assert i <= estimate_n_guesses(matrix, n_states=i, singles_only=True) + matrix = adcc.AdcMatrix(method, testdata_cache.refstate(system, case=case)) + + guesses = adcc.guesses_doublet(matrix, n_guesses=6, block="p", + is_alpha=True) + res = diagonalise_adcmatrix(matrix, n_states=n_states, kind=kind, + guesses=guesses, is_alpha=True) + assert res.converged + assert res.eigenvalues[:n_states] == approx(ref_doublets[:n_states]) + + with pytest.raises(InputError): # Too low tolerance + # SCF tolerance = 1e-14 currently + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="davidson", + conv_tol=1e-15) - def test_obtain_guesses_by_inspection(self): + with pytest.raises(InputError): # Wrong solver method + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + guesses=guesses, eigensolver="blubber") + + with pytest.raises(ValueError): # Too few guesses + res = diagonalise_adcmatrix(matrix, n_states=9, kind=kind, + eigensolver="davidson", + guesses=guesses) + + def test_obtain_guesses_by_inspection_pp(self): from adcc.workflow import obtain_guesses_by_inspection refstate = testdata_cache.refstate("h2o_sto3g", case="gen") @@ -269,8 +522,156 @@ def test_obtain_guesses_by_inspection(self): matrix2, n_guesses=i, kind="triplet", n_guesses_doubles=2) assert len(res) == i + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, kind="singlet") + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="singlet", + n_guesses_doubles=4) + assert len(res) == 4 + with pytest.raises(InputError): obtain_guesses_by_inspection(matrix1, n_guesses=4, kind="any", n_guesses_doubles=2) with pytest.raises(InputError): obtain_guesses_by_inspection(matrix1, n_guesses=40, kind="any") + + def test_obtain_guesses_by_inspection_ip(self): + from adcc.workflow import obtain_guesses_by_inspection + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix2 = adcc.AdcMatrix("ip-adc2", ground_state) + matrix1 = adcc.AdcMatrix("ip-adc1", ground_state) + + # Test that the right number of guesses is returned + for i in range(4, 9): + res = obtain_guesses_by_inspection(matrix2, n_guesses=i, + kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == i + + for i in range(2, 5): + res = obtain_guesses_by_inspection( + matrix1, n_guesses=i, kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == i + + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, + kind="doublet", + spin_change=-0.5, is_alpha=True) + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="doublet", + spin_change=-0.5, is_alpha=True, + n_guesses_doubles=4) + assert len(res) == 4 + + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=6, kind="any", + spin_change=-0.5, is_alpha=True) + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=2, kind="any", + n_guesses_doubles=2, spin_change=-0.5, + is_alpha=True) + + def test_obtain_guesses_by_inspection_ea(self): + from adcc.workflow import obtain_guesses_by_inspection + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix2 = adcc.AdcMatrix("ea-adc2", ground_state) + matrix1 = adcc.AdcMatrix("ea-adc1", ground_state) + + # Test that the right number of guesses is returned + for i in range(4, 9): + res = obtain_guesses_by_inspection(matrix2, n_guesses=i, + kind="doublet", + spin_change=0.5, + is_alpha=True) + assert len(res) == i + + for i in range(1, 2): + res = obtain_guesses_by_inspection( + matrix1, n_guesses=i, kind="doublet", + spin_change=0.5, is_alpha=True) + assert len(res) == i + + # Test right number of guesses if insufficient singles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=20, + kind="doublet", + spin_change=0.5, is_alpha=True) + assert len(res) == 20 + + # Only doubles guesses + res = obtain_guesses_by_inspection(matrix2, n_guesses=4, + kind="doublet", + spin_change=0.5, is_alpha=True, + n_guesses_doubles=4) + assert len(res) == 4 + + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=6, kind="any", + spin_change=0.5, is_alpha=True) + with pytest.raises(InputError): + obtain_guesses_by_inspection(matrix1, n_guesses=2, kind="any", + n_guesses_doubles=2, spin_change=0.5, + is_alpha=True) + + def test_construct_guesses_explicit(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=3, + kind="singlet", + spin_change=0, + n_guesses=5 + ) + + assert len(res) == 5 + + def test_construct_guesses_davidson(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=2, + kind="singlet", + spin_change=0, + n_guesses=None, + eigensolver="davidson" + ) + + assert len(res) == 4 + + def test_construct_guesses_lanczos(self): + from adcc.workflow import construct_guesses + + refstate = testdata_cache.refstate("h2o_sto3g", case="gen") + ground_state = adcc.LazyMp(refstate) + matrix = adcc.AdcMatrix("adc2", ground_state) + + res = construct_guesses( + matrix=matrix, + n_states=2, + kind="singlet", + spin_change=0, + n_guesses=None, + eigensolver="lanczos" + ) + + assert len(res) == 2 diff --git a/adcc/workflow.py b/adcc/workflow.py index a9d27bf6..110a9b32 100644 --- a/adcc/workflow.py +++ b/adcc/workflow.py @@ -25,14 +25,18 @@ from libadcc import ReferenceState +from typing import Optional + from . import solver -from .guess import (guesses_any, guesses_singlet, guesses_spin_flip, - guesses_triplet) +from .guess import (determine_spin_change, estimate_n_guesses, + guesses_from_diagonal, get_spin_block_symmetrisation) from .LazyMp import LazyMp from .AdcMatrix import AdcMatrix, AdcMatrixlike, AdcExtraTerm -from .AdcMethod import AdcMethod +from .AdcMethod import AdcMethod, AdcType +from .AmplitudeVector import AmplitudeVector from .exceptions import InputError from .ExcitedStates import ExcitedStates +from .ChargedExcitations import DetachedStates, AttachedStates from .ReferenceState import ReferenceState as adcc_ReferenceState from .solver.lanczos import lanczos from .solver.davidson import jacobi_davidson @@ -43,11 +47,12 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, - eigensolver=None, guesses=None, n_guesses=None, + eigensolver="davidson", guesses=None, n_guesses=None, n_guesses_doubles=None, output=sys.stdout, core_orbitals=None, frozen_core=None, frozen_virtual=None, method=None, - n_singlets=None, n_triplets=None, n_spin_flip=None, - environment=None, **solverargs): + n_singlets=None, n_doublets=None, n_triplets=None, + n_spin_flip=None, is_alpha=None, environment=None, + **solverargs): """Run an ADC calculation. Main entry point to run an ADC calculation. The reference to build the ADC @@ -70,17 +75,24 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, n_states : int, optional kind : str, optional n_singlets : int, optional + n_doublets : int, optional n_triplets : int, optional n_spin_flip : int, optional Specify the number and kind of states to be computed. Possible values - for kind are "singlet", "triplet", "spin_flip" and "any", which is - the default. For unrestricted references clamping spin-pure + for kind are "singlet", "doublet", "triplet", "spin_flip" and "any", + which is the default. For unrestricted references clamping spin-pure singlets/triplets is currently not possible and kind has to remain as - "any". For restricted references `kind="singlets"` or `kind="triplets"` - may be employed to enforce a particular excited states manifold. + "any". For restricted references `kind="singlets"`, `kind="doublets"` + or `kind="triplets"` may be employed to enforce a particular excited + states manifold. Specifying `n_singlets` is equivalent to setting `kind="singlet"` and - `n_states=5`. Similarly for `n_triplets` and `n_spin_flip`. - `n_spin_flip` is only valid for unrestricted references. + `n_states=5`. Similarly for `n_doublets`, `n_triplets` and + `n_spin_flip`. `n_spin_flip` is only valid for unrestricted references. + + is_alpha : bool, optional + Is the detached/attached electron alpha spin for the respective + IP-/EA-ADC calculation. Per default it will be set to `True` for + IP- and EA-ADC calculations. conv_tol : float, optional Convergence tolerance to employ in the iterative solver for obtaining @@ -91,8 +103,8 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, The eigensolver algorithm to use. n_guesses : int, optional - Total number of guesses to compute. By default only guesses derived from - the singles block of the ADC matrix are employed. See + Total number of guesses to compute. By default only guesses derived + from the singles block of the ADC matrix are employed. See `n_guesses_doubles` for alternatives. If no number is given here `n_guesses = min(4, 2 * number of excited states to compute)` or a smaller number if the number of excitation is estimated to be less @@ -178,38 +190,79 @@ def run_adc(data_or_matrix, n_states=None, kind="any", conv_tol=None, ... mf.kernel() ... ... state = adcc.cvs_adc3(mf, core_orbitals=1, n_singlets=3) - """ + + Run an IP-ADC(2) calculation of water with a detached alpha + electron + + >>> import psi4 + ... import adcc + ... # Run SCF in Psi4 + ... mol = psi4.geometry(''' + ... 0 1 + ... O 0.0000000000 0.0000000000 0.0000000000 + ... H 0.0000000000 0.0000000000 0.9570000000 + ... H 0.9270000000 0.0000000000 -0.2400000000 + ... symmetry c1 + ... units Angstrom + ... ''') + ... psi4.core.be_quiet() + ... psi4.set_options({'basis': "6-31++G(d)", 'e_convergence': 1e-13, + ... 'd_convergence': 1e-7, 'reference': "uhf", + ... 'scf_type': "direct"}) + ... scf_e, wfn = psi4.energy('SCF', return_wfn=True) + ... + ... state = adcc.ip_adc2(wfn, n_doublets=3, is_alpha=True) +""" matrix = construct_adcmatrix( data_or_matrix, core_orbitals=core_orbitals, frozen_core=frozen_core, frozen_virtual=frozen_virtual, method=method) - n_states, kind = validate_state_parameters( - matrix.reference_state, n_states=n_states, n_singlets=n_singlets, - n_triplets=n_triplets, n_spin_flip=n_spin_flip, kind=kind) - - # Determine spin change during excitation. If guesses is not None, - # i.e. user-provided, we cannot guarantee for obtaining a particular - # spin_change in case of a spin_flip calculation. - spin_change = None - if kind == "spin_flip" and guesses is None: - spin_change = -1 - - # Select solver to run - if eigensolver is None: - eigensolver = "davidson" + n_states, kind, is_alpha = validate_state_parameters( + matrix, n_states=n_states, n_singlets=n_singlets, + n_doublets=n_doublets, n_triplets=n_triplets, n_spin_flip=n_spin_flip, + kind=kind, is_alpha=is_alpha) # Setup environment coupling terms and energy corrections - ret = setup_environment(matrix, environment) - env_matrix_term, env_energy_corrections = ret + env_matrix_term, env_energy_corrections = setup_environment(matrix, + environment) # add terms to matrix if env_matrix_term: matrix += env_matrix_term + # Construct guesses and determine spin_change + if guesses is None: + spin_change = determine_spin_change(matrix.method, kind, is_alpha) + guesses = construct_guesses( + matrix, n_states, kind, spin_change, n_guesses, n_guesses_doubles, + is_alpha, eigensolver + ) + else: + if len(guesses) < n_states: + raise InputError(f"Less guesses provided via guesses (=={len(guesses)}" + f") than states to be computed (=={n_states})") + if n_guesses is not None: + warnings.warn("Ignoring n_guesses parameter, since guesses are " + "explicitly provided.") + if n_guesses_doubles is not None: + warnings.warn("Ignoring n_guesses_doubles parameter, since guesses" + " are explicitly provided.") + # Set spin_change to None since we don't know if guesses are provided + spin_change = None + diagres = diagonalise_adcmatrix( - matrix, n_states, kind, guesses=guesses, n_guesses=n_guesses, - n_guesses_doubles=n_guesses_doubles, conv_tol=conv_tol, output=output, - eigensolver=eigensolver, **solverargs) - exstates = ExcitedStates(diagres) + matrix, n_states, guesses, kind=kind, conv_tol=conv_tol, + output=output, eigensolver=eigensolver, is_alpha=is_alpha, + **solverargs) + + if matrix.method.adc_type is AdcType.PP: + exstates = ExcitedStates(diagres) + elif matrix.method.adc_type is AdcType.IP: + exstates = DetachedStates(diagres, is_alpha) + elif matrix.method.adc_type is AdcType.EA: + exstates = AttachedStates(diagres, is_alpha) + else: + raise ValueError(f"Unknown ADC method: {matrix.method.name}") + exstates.kind = kind exstates.spin_change = spin_change @@ -255,20 +308,20 @@ def construct_adcmatrix(data_or_matrix, core_orbitals=None, frozen_core=None, elif core_orbitals is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored core_orbitals parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of core_orbitals={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of core_orbitals={})." "".format(mospaces.n_orbs_alpha("o2"))) elif frozen_core is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored frozen_core parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of frozen_core={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of frozen_core={})." "".format(mospaces.n_orbs_alpha("o3"))) elif frozen_virtual is not None: mospaces = data_or_matrix.mospaces warnings.warn("Ignored frozen_virtual parameter because data_or_matrix" - " is a ReferenceState, a LazyMp or an AdcMatrixlike object " - " (which has a value of frozen_virtual={})." + " is a ReferenceState, a LazyMp or an AdcMatrixlike " + "object (which has a value of frozen_virtual={})." "".format(mospaces.n_orbs_alpha("v2"))) # Make AdcMatrix (if not done) @@ -285,18 +338,23 @@ def construct_adcmatrix(data_or_matrix, core_orbitals=None, frozen_core=None, return data_or_matrix -def validate_state_parameters(reference_state, n_states=None, n_singlets=None, - n_triplets=None, n_spin_flip=None, kind="any"): +def validate_state_parameters(matrix, n_states=None, n_singlets=None, + n_doublets=None, n_triplets=None, + n_spin_flip=None, kind="any", is_alpha=None + ) -> tuple[int, str, Optional[bool]]: """ Check the passed state parameters for consistency with itself and with the passed reference and normalise them. In the end return the number of - states and the corresponding kind parameter selected. + states, the corresponding kind parameter selected and is_alpha which will + only be set to a Boolean for IP- and EA-ADC calculations. Internal function called from run_adc. """ - if sum(nst is not None for nst in [n_states, n_singlets, + reference_state = matrix.reference_state + adc_type = matrix.method.adc_type + if sum(nst is not None for nst in [n_states, n_singlets, n_doublets, n_triplets, n_spin_flip]) > 1: raise InputError("One may only specify one out of n_states, " - "n_singlets, n_triplets and n_spin_flip") + "n_singlets, n_doublets, n_triplets and n_spin_flip") if n_singlets is not None: if not reference_state.restricted: @@ -307,6 +365,16 @@ def validate_state_parameters(reference_state, n_states=None, n_singlets=None, "with n_singlets > 0") kind = "singlet" n_states = n_singlets + if n_doublets is not None: + if not reference_state.restricted: + raise InputError("The n_doublets parameter may only be employed " + "for restricted references") + if kind not in ["doublet", "any"]: + raise InputError(f"Kind parameter {kind} not compatible " + "with n_doublets > 0") + kind = "doublet" + n_states = n_doublets + is_alpha = True if n_triplets is not None: if not reference_state.restricted: raise InputError("The n_triplets parameter may only be employed " @@ -326,35 +394,136 @@ def validate_state_parameters(reference_state, n_states=None, n_singlets=None, kind = "spin_flip" n_states = n_spin_flip + # Check for IP- and EA-ADC parameter is_alpha + if adc_type is AdcType.PP: + if is_alpha is not None: + raise InputError("is_alpha may only be set for IP- and EA-ADC " + "calculations") + else: + if not isinstance(is_alpha, bool) and is_alpha is not None: + raise InputError("is_alpha has to be a Boolean or None.") + if is_alpha is None or reference_state.restricted: + # Per default set to True and for restricted references, only alpha + # states will be computed (beta states are identical) + is_alpha = True + # Check if there are states to be computed if n_states is None or n_states == 0: raise InputError("No excited states to be computed. Specify at least " - "one of n_states, n_singlets, n_triplets, " - "or n_spin_flip") + "one of n_states, n_singlets, n_doublets, " + "n_triplets, or n_spin_flip.") if n_states < 0: raise InputError("n_states needs to be positive") - if kind not in ["any", "spin_flip", "singlet", "triplet"]: + if kind not in ["any", "spin_flip", "singlet", "doublet", "triplet"]: raise InputError("The kind parameter may only take the values 'any', " - "'singlet', 'triplet' or 'spin_flip'") - if kind in ["singlet", "triplet"] and not reference_state.restricted: - raise InputError("kind==singlet and kind==triplet are only valid for " - "ADC calculations in combination with a restricted " - "ground state.") + "'singlet', 'doublet', 'triplet' or 'spin_flip'") + if (kind in ["singlet", "doublet", "triplet"] + and not reference_state.restricted): + raise InputError("kind==singlet, kind==doublet and kind==triplet are " + "only valid for ADC calculations in combination with " + "a restricted ground state.") if kind in ["spin_flip"] and reference_state.restricted: raise InputError("kind==spin_flip is only valid for " - "ADC calculations in combination with an unrestricted " + "ADC calculations in combination with an unrestricted" + " ground state.") + if kind in ["spin_flip", "singlet", "triplet"] and adc_type is not AdcType.PP: + raise InputError("kind==singlet, kind==triplet, and kind==spin_flip " + "are only valid for PP-ADC calculations.") + if kind == "doublet" and adc_type is AdcType.PP: + raise InputError("kind==doublet is only valid for IP/EA-ADC " + "calculations in combination with a restricted " "ground state.") - return n_states, kind + return n_states, kind, is_alpha + + +def obtain_guesses_by_inspection(matrix, n_guesses, kind, + n_guesses_doubles=None, is_alpha=None, + spin_change=0): + """ + Obtain guesses by inspecting the diagonal matrix elements. + If n_guesses_doubles is not None, this number is always adhered to. + Otherwise the number of doubles guesses is adjusted to fill up whatever + the singles guesses cannot provide to reach n_guesses. + matrix The matrix for which guesses are to be constructed + is_alpha Is the detached/attached electron alpha spin for the + respective IP-/EA-ADC calculation. + kwargs Any other argument understood by guesses_from_diagonal. + """ + spin_block_symmetrisation = get_spin_block_symmetrisation(kind) -def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", - guesses=None, n_guesses=None, n_guesses_doubles=None, - conv_tol=None, output=sys.stdout, **solverargs): + # Determine number of singles guesses to request + if n_guesses_doubles is None: + n_guesses_doubles = 0 + + n_guesses_singles = n_guesses - n_guesses_doubles + + guesses = guesses_from_diagonal( + matrix, n_guesses_singles, block=matrix.axis_blocks[0], kind=kind, + is_alpha=is_alpha, spin_change=spin_change, + spin_block_symmetrisation=spin_block_symmetrisation) + + # Determine number of doubles guesses to request if not + # explicitly specified + n_guesses_doubles = n_guesses - len(guesses) + + if n_guesses_doubles > 0: + if matrix.method.level.to_int() < 2: + raise InputError("n_guesses_doubles > 0 is only sensible if the " + "ADC method has a doubles block (i.e. it is *not*" + " ADC(0), ADC(1) or a variant thereof.") + + guesses += guesses_from_diagonal( + matrix, n_guesses_doubles, matrix.axis_blocks[1], kind, + is_alpha, spin_change, spin_block_symmetrisation) + + if len(guesses) < n_guesses: + raise InputError(f"Less guesses found ({len(guesses)}) " + f"than requested {n_guesses}") + return guesses + + +def construct_guesses(matrix: AdcMatrix, + n_states: int, + kind: str, + spin_change: float, + n_guesses: int, + n_guesses_doubles: Optional[int] = None, + is_alpha: Optional[bool] = None, + eigensolver: Optional[str] = "davidson" + ) -> list[AmplitudeVector]: + """ + This function constructs appropriate guesses if not given. + Returns a :class:`Guesses` object containing all crucial guess information. + Internal function called from run_adc. + """ + if n_guesses is None: + # Set solver-specific parameters + if eigensolver == "davidson": + n_guesses_per_state = 2 + else: + n_guesses_per_state = 1 + n_guesses = estimate_n_guesses(matrix, n_states, n_guesses_per_state) + + return obtain_guesses_by_inspection( + matrix, n_guesses, kind, n_guesses_doubles, is_alpha, spin_change + ) + + +def diagonalise_adcmatrix(matrix, n_states, guesses, kind="any", conv_tol=None, + eigensolver="davidson", output=sys.stdout, + is_alpha=None, **solverargs): """ This function seeks appropriate guesses and afterwards proceeds to diagonalise the ADC matrix using the specified eigensolver. Internal function called from run_adc. + + matrix : AdcMatrix + n_states : int + guesses : list[AmplitudeVector] + A list of guess vectors + kind : str """ reference_state = matrix.reference_state @@ -370,149 +539,50 @@ def diagonalise_adcmatrix(matrix, n_states, kind, eigensolver="davidson", # Determine explicit_symmetrisation explicit_symmetrisation = IndexSymmetrisation - if kind in ["singlet", "triplet"]: + if kind in ["singlet", "doublet", "triplet"]: explicit_symmetrisation = IndexSpinSymmetrisation( matrix, enforce_spin_kind=kind ) # Set some solver-specific parameters if eigensolver == "davidson": - n_guesses_per_state = 2 callback = setup_solver_printing( - "Jacobi-Davidson", matrix, kind, solver.davidson.default_print, + "Jacobi-Davidson", matrix, kind, + solver.davidson.default_print, is_alpha=is_alpha, output=output) run_eigensolver = jacobi_davidson elif eigensolver == "lanczos": - n_guesses_per_state = 1 callback = setup_solver_printing( "Lanczos", matrix, kind, solver.lanczos.default_print, - output=output) + is_alpha=is_alpha, output=output) run_eigensolver = lanczos else: raise InputError(f"Solver {eigensolver} unknown, try 'davidson'.") - # Obtain or check guesses - if guesses is None: - if n_guesses is None: - # restrict to the number of available singles guesses if no doubles - # are available - n_guesses = estimate_n_guesses( - matrix=matrix, n_states=n_states, - singles_only=("pphh" not in matrix.axis_blocks), - n_guesses_per_state=n_guesses_per_state - ) - guesses = obtain_guesses_by_inspection(matrix, n_guesses, kind, - n_guesses_doubles) - else: - if len(guesses) < n_states: - raise InputError("Less guesses provided via guesses (== {}) " - "than states to be computed (== {})" - "".format(len(guesses), n_states)) - if n_guesses is not None: - warnings.warn("Ignoring n_guesses parameter, since guesses are " - "explicitly provided.") - if n_guesses_doubles is not None: - warnings.warn("Ignoring n_guesses_doubles parameter, since guesses " - "are explicitly provided.") - solverargs.setdefault("which", "SA") - return run_eigensolver(matrix, guesses, n_ep=n_states, conv_tol=conv_tol, - callback=callback, + return run_eigensolver(matrix, guesses, n_ep=n_states, + conv_tol=conv_tol, callback=callback, explicit_symmetrisation=explicit_symmetrisation, **solverargs) -def estimate_n_guesses(matrix, n_states, singles_only=True, - n_guesses_per_state=2): - """ - Implementation of a basic heuristic to find a good number of guess - vectors to be searched for using the find_guesses function. - Internal function called from run_adc. - - matrix ADC matrix - n_states Number of states to be computed - singles_only Try to stay withing the singles excitation space - with the number of guess vectors. - n_guesses_per_state Number of guesses to search for for each state - """ - # Try to use at least 4 or twice the number of states - # to be computed as guesses - n_guesses = n_guesses_per_state * max(2, n_states) - - if singles_only: - # Compute the maximal number of sensible singles block guesses. - # This is roughly the number of occupied alpha orbitals - # times the number of virtual alpha orbitals - # - # If the system is core valence separated, then only the - # core electrons count as "occupied". - mospaces = matrix.mospaces - sp_occ = "o2" if matrix.is_core_valence_separated else "o1" - n_virt_a = mospaces.n_orbs_alpha("v1") - n_occ_a = mospaces.n_orbs_alpha(sp_occ) - n_guesses = min(n_guesses, n_occ_a * n_virt_a) - - # Adjust if we overshoot the maximal number of sensible singles block - # guesses, but make sure we get at least n_states guesses - return max(n_states, n_guesses) - - -def obtain_guesses_by_inspection(matrix, n_guesses, kind, n_guesses_doubles=None): - """ - Obtain guesses by inspecting the diagonal matrix elements. - If n_guesses_doubles is not None, this is number is always adhered to. - Otherwise the number of doubles guesses is adjusted to fill up whatever - the singles guesses cannot provide to reach n_guesses. - Internal function called from run_adc. - """ - if n_guesses_doubles is not None and n_guesses_doubles > 0 \ - and "pphh" not in matrix.axis_blocks: - raise InputError("n_guesses_doubles > 0 is only sensible if the ADC " - "method has a doubles block (i.e. it is *not* ADC(0), " - "ADC(1) or a variant thereof.") - - # Determine guess function - guess_function = {"any": guesses_any, "singlet": guesses_singlet, - "triplet": guesses_triplet, - "spin_flip": guesses_spin_flip}[kind] - - # Determine number of singles guesses to request - n_guess_singles = n_guesses - if n_guesses_doubles is not None: - n_guess_singles = n_guesses - n_guesses_doubles - singles_guesses = guess_function(matrix, n_guess_singles, block="ph") - - doubles_guesses = [] - if "pphh" in matrix.axis_blocks: - # Determine number of doubles guesses to request if not - # explicitly specified - if n_guesses_doubles is None: - n_guesses_doubles = n_guesses - len(singles_guesses) - if n_guesses_doubles > 0: - doubles_guesses = guess_function(matrix, n_guesses_doubles, - block="pphh") - - total_guesses = singles_guesses + doubles_guesses - if len(total_guesses) < n_guesses: - raise InputError("Less guesses found than requested: {} found, " - "{} requested".format(len(total_guesses), n_guesses)) - return total_guesses - - def setup_solver_printing(solmethod_name, matrix, kind, default_print, - output=None): + is_alpha=None, output=None): """ Setup default printing for solvers. Internal function called from run_adc. """ - kstr = " " + kstr = "" if kind != "any": kstr = " " + kind method_name = f"{matrix}" if hasattr(matrix, "method"): method_name = matrix.method.name + spin_type = "" + if is_alpha is not None: + spin_type = "alpha " if is_alpha else "beta " if output is not None: - print(f"Starting {method_name}{kstr} {solmethod_name} ...", + print(f"Starting {spin_type}{method_name}{kstr} {solmethod_name} ...", file=output) def inner_callback(state, identifier): @@ -525,6 +595,10 @@ def setup_environment(matrix, environment): Setup environment matrix terms and/or energy corrections. Internal function called from run_adc. """ + if environment and matrix.method.adc_type is not AdcType.PP: + raise NotImplementedError("Environment for IP- and EA-ADC calculations" + " not implemented.") + valid_envs = ["ptss", "ptlr", "linear_response"] hf = matrix.reference_state if hf.environment and environment is None: @@ -583,8 +657,8 @@ def setup_environment(matrix, environment): from adcc.adc_pp import environment as adcpp_env block_key = f"block_ph_ph_0_{hf.environment}" if not hasattr(adcpp_env, block_key): - raise NotImplementedError("Matrix term for linear response coupling" - f" with solvent {hf.environment}" + raise NotImplementedError("Matrix term for linear response " + f"coupling with solvent {hf.environment}" " not implemented.") block_fun = getattr(adcpp_env, block_key) env_matrix_term = AdcExtraTerm(matrix, {'ph_ph': block_fun}) diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.cc b/libadcc_src/amplitude_vector_enforce_spin_kind.cc index e5727efc..ac3a3acb 100644 --- a/libadcc_src/amplitude_vector_enforce_spin_kind.cc +++ b/libadcc_src/amplitude_vector_enforce_spin_kind.cc @@ -31,7 +31,8 @@ namespace libadcc { namespace lt = libtensor; void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, - std::string block, std::string spin_kind) { + std::string block, std::string spin_kind, + bool is_ip) { // Nothing to do for singles block if (block == "s") return; @@ -64,10 +65,131 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, return; } + if (spin_kind == "doublet") { + auto& u2 = asbt3(doubles_tensor); + lt::block_tensor_ctrl<3, scalar_type> ctrl(u2); + const lt::symmetry<3, scalar_type>& sym = ctrl.req_const_symmetry(); + + // Extract the number of blocks per dimension + const lt::block_index_space<3>& bis = sym.get_bis(); + lt::dimensions<3> bidims(bis.get_block_index_dims()); + + // Setup i1 to point to 0,0,0 and i2 to the half of the + // full number of blocks, i.e. to the alpha blocks in each + // dimension only. + lt::index<3> i1, i2; + for (size_t i = 0; i < 3; i++) i2[i] = bidims[i] / 2 - 1; + + // Index range over all alpha-alpha-alpha blocks + // in all point group symmetries + const lt::index_range<3> index_range_alpha(i1, i2); + + // This dimensions object contains the number of alpha blocks per dimension + lt::dimensions<3> bidims_alpha(index_range_alpha); + + // Iterate over all alpha-alpha-alpha blocks + lt::abs_index<3> ai(bidims_alpha); + do { + // This gives the block index tuple + const lt::index<3>& ii = ai.get_index(); + + // Construct the orbit corresponding to this index (i.e. the iterator + // running over all elements equivalent by symmetry + // Ignore spin-forbidden, i.e. zero orbits + lt::short_orbit<3, scalar_type> orbi(sym, ii, + /* compute_if_allowed_orbit = */ true); + if (!orbi.is_allowed()) continue; + + // get_acindex -> get absolute canonical index + // Continue if our current index is larger than the canonical index + if (orbi.get_acindex() < ai.get_abs_index()) continue; + + // TODO This might be wrong ... think about it and talk to Adrian + // the point is that orbi might have other strides than bidims_alpha + // Continue if the canonical index is already past the + // alpha-alpha-alpha block + if (orbi.get_acindex() > bidims_alpha.get_size()) continue; + + // Get the index tuple of the canonical block of (alpha, alpha, alpha) + const lt::index<3>& ci = orbi.get_cindex(); + + lt::index<3> i1(ci); + lt::index<3> i2(ci); + + if (is_ip) { // IP-ADC calculation + // set i1 to (alpha, beta, beta) equivalent of the canonical index + // pinned by ai and orbi + i1[1] += bidims_alpha[1]; + i1[2] += bidims_alpha[2]; + + // set i2 to (beta, alpha, beta) + i2[0] += bidims_alpha[0]; + i2[2] += bidims_alpha[2]; + + } else { // EA-ADC calculation + // set i1 to (beta, alpha, beta) equivalent of the canonical index + // pinned by ai and orbi + i1[0] += bidims_alpha[0]; + i1[2] += bidims_alpha[2]; + + // set i2 to (beta, beta, alpha) + i2[0] += bidims_alpha[0]; + i2[1] += bidims_alpha[1]; + } + + // + // What the following code does is that it keeps the spin projection (S^2) + // properly, provided that the symmetry setup is done as in + // contrib/adc_pp/adc_guess_d.C. It assumes the coefficients as setup in + // contrib/adc_pp/adc_guess_d.C adc_guess_d::build_guesses in order to preserve + // S^2 value setup in the guess. + // + + lt::orbit<3, scalar_type> orb1(sym, i1, false), orb2(sym, i2, false); + // Canonical block of (a, b, b) for IP-ADC or (b, a, b) for EA-ADC + const lt::index<3>& ci1 = orb1.get_cindex(); + // Canonical block of (b, a, b) for IP-ADC or (b, b, a) for EA-ADC + const lt::index<3>& ci2 = orb2.get_cindex(); + bool zero1 = ctrl.req_is_zero_block(ci1); + bool zero2 = ctrl.req_is_zero_block(ci2); + if (zero1 && zero2) { + // Set (alpha, alpha, alpha) to zero + // This effectively filters out the quartet components with zero blocks + // in (alpha, beta, beta) and (beta, alpha, beta) (IP-ADC) erroneously + // introduced due to numerical errors. (beta, alpha, beta) and + // (beta, beta, alpha) blocks for EA-ADC. + ctrl.req_zero_block(ci); + continue; + } + + // Get block corresponding to canonical index of (alpha, alpha, alpha) + lt::dense_tensor_wr_i<3, scalar_type>& blk = ctrl.req_block(ci); + + if (!zero1) { + // IP: (alpha, beta, beta) / EA: (beta, alpha, beta) is not zero + lt::dense_tensor_rd_i<3, scalar_type>& blk1 = ctrl.req_const_block(ci1); + lt::tod_copy<3>(blk1, orb1.get_transf(i1)).perform(/* assign= */ true, blk); + ctrl.ret_const_block(ci1); + } + if (!zero2) { + // IP: (beta, alpha, beta) / EA: (beta, beta, alpha) is not zero + lt::dense_tensor_rd_i<3, scalar_type>& blk2 = ctrl.req_const_block(ci2); + + // Assign if (alpha, beta, beta) for IP- and (beta, alpha, beta) for + // EA-ADC is zero, else += + lt::tod_copy<3>(blk2, orb2.get_transf(i2)).perform(zero1, blk); + ctrl.ret_const_block(ci2); + } + ctrl.ret_block(ci); + + } while (ai.inc()); + return; + } + if (spin_kind != "singlet") { throw not_implemented_error( - "Only implemented for spin_kind == 'singlet' and spin_kind == " - "'triplet'."); + "Only implemented for spin_kind == 'singlet', spin_kind == " + "'triplet' or spin_kind == 'doublet'."); } auto& u2 = asbt4(doubles_tensor); @@ -169,4 +291,4 @@ void amplitude_vector_enforce_spin_kind(std::shared_ptr doubles_tensor, } while (ai.inc()); } -} // namespace libadcc +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/amplitude_vector_enforce_spin_kind.hh b/libadcc_src/amplitude_vector_enforce_spin_kind.hh index 6a7a4dbd..0ac1a04e 100644 --- a/libadcc_src/amplitude_vector_enforce_spin_kind.hh +++ b/libadcc_src/amplitude_vector_enforce_spin_kind.hh @@ -37,6 +37,6 @@ namespace libadcc { * @param spin_kind The kind of spin to enforce */ void amplitude_vector_enforce_spin_kind(std::shared_ptr tensor, std::string block, - std::string spin_kind); + std::string spin_kind, bool is_ip); ///@} } // namespace libadcc diff --git a/libadcc_src/fill_ea_doubles_guesses.cc b/libadcc_src/fill_ea_doubles_guesses.cc new file mode 100644 index 00000000..f2e616f1 --- /dev/null +++ b/libadcc_src/fill_ea_doubles_guesses.cc @@ -0,0 +1,75 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "fill_ea_doubles_guesses.hh" +#include "TensorImpl.hh" +#include "guess/ea_adc_guess_d.hh" + +namespace libadcc { + +size_t fill_ea_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, scalar_type degeneracy_tolerance) { + + size_t n_guesses = guesses_d.size(); + if (n_guesses == 0) return 0; + + // Make a copy of the doubles symmetry + libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0])); + libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis()); + libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s); + + // Make ab pointers object + auto make_ab = [](const MoSpaces& mo, const std::string& space) { + const std::vector& block_spin = mo.map_block_spin.at(space); + std::vector ab; + for (size_t i = 0; i < block_spin.size(); ++i) { + ab.push_back(block_spin[i] == 'b'); + } + return ab; + }; + + const std::vector spaces_d = guesses_d[0]->subspaces(); + std::vector> abvectors; + for (size_t i = 0; i < 3; ++i) { + abvectors.push_back(make_ab(*mospaces, spaces_d[i])); + } + libtensor::sequence<3, std::vector*> ab_d; + for (size_t i = 0; i < 3; ++i) { + ab_d[i] = &abvectors[i]; + } + + // Make singles list data structure + std::list*, double>> guesspairs; + for (size_t i = 0; i < n_guesses; i++) { + guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0); + } + + if (abs(spin_change_twice) != 1) { + throw not_implemented_error("spin_change ==" + std::to_string(spin_change_twice) + + " has not been tested."); + } + + return ea_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin, restricted, + doublet, ab_d, spin_change_twice, degeneracy_tolerance); +} + +} // namespace libadcc diff --git a/libadcc_src/fill_ea_doubles_guesses.hh b/libadcc_src/fill_ea_doubles_guesses.hh new file mode 100644 index 00000000..15ad4644 --- /dev/null +++ b/libadcc_src/fill_ea_doubles_guesses.hh @@ -0,0 +1,49 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#pragma once +#include "Tensor.hh" + +namespace libadcc { + +/** Fill the passed vector of doubles blocks with doubles guesses using + * the O and V matrices. + * + * guesses_d Vectors of guesses, all elements are assumed to be initialised + * to zero and the symmetry is assumed to be properly set up. + * mospaces Mospaces object + * d_o Fock matrix to construct guesses from (occ.) + * d_v Fock matrix to construct guesses from (virt.) + * a_spin If alpha ionization (false: beta) + * restricted Is this a restricted calculation + * doublet Doublet or quartet states (only in case of restricted calc.) + * spin_change_twice Twice the value of spin change to enforce in an excitation. + * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered + * degenerate, i.e. identical. + * + * \returns The number of guess vectors which have been properly initialised + * (the others are invalid and should be discarded). + */ +size_t fill_ea_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, scalar_type degeneracy_tolerance); + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/fill_ip_doubles_guesses.cc b/libadcc_src/fill_ip_doubles_guesses.cc new file mode 100644 index 00000000..4ca9bc21 --- /dev/null +++ b/libadcc_src/fill_ip_doubles_guesses.cc @@ -0,0 +1,75 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "fill_ip_doubles_guesses.hh" +#include "TensorImpl.hh" +#include "guess/ip_adc_guess_d.hh" + +namespace libadcc { + +size_t fill_ip_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, scalar_type degeneracy_tolerance) { + + size_t n_guesses = guesses_d.size(); + if (n_guesses == 0) return 0; + + // Make a copy of the doubles symmetry + libtensor::block_tensor_ctrl<3, scalar_type> ctrl(asbt3(guesses_d[0])); + libtensor::symmetry<3, scalar_type> sym_s(ctrl.req_const_symmetry().get_bis()); + libtensor::so_copy<3, scalar_type>(ctrl.req_const_symmetry()).perform(sym_s); + + // Make ab pointers object + auto make_ab = [](const MoSpaces& mo, const std::string& space) { + const std::vector& block_spin = mo.map_block_spin.at(space); + std::vector ab; + for (size_t i = 0; i < block_spin.size(); ++i) { + ab.push_back(block_spin[i] == 'b'); + } + return ab; + }; + + const std::vector spaces_d = guesses_d[0]->subspaces(); + std::vector> abvectors; + for (size_t i = 0; i < 3; ++i) { + abvectors.push_back(make_ab(*mospaces, spaces_d[i])); + } + libtensor::sequence<3, std::vector*> ab_d; + for (size_t i = 0; i < 3; ++i) { + ab_d[i] = &abvectors[i]; + } + + // Make singles list data structure + std::list*, double>> guesspairs; + for (size_t i = 0; i < n_guesses; i++) { + guesspairs.emplace_back(&(asbt3(guesses_d[i])), 0.0); + } + + if (abs(spin_change_twice) != 1) { + throw not_implemented_error("spin_change ==" + std::to_string(spin_change_twice) + + " has not been tested."); + } + + return ip_adc_guess_d(guesspairs, asbt1(d_o), asbt1(d_v), sym_s, a_spin, restricted, + doublet, ab_d, spin_change_twice, degeneracy_tolerance); +} + +} // namespace libadcc diff --git a/libadcc_src/fill_ip_doubles_guesses.hh b/libadcc_src/fill_ip_doubles_guesses.hh new file mode 100644 index 00000000..2ff123c2 --- /dev/null +++ b/libadcc_src/fill_ip_doubles_guesses.hh @@ -0,0 +1,50 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#pragma once +#include "Tensor.hh" + +namespace libadcc { + +/** Fill the passed vector of doubles blocks with doubles guesses using + * the O and V matrices. + * + * + * guesses_d Vectors of guesses, all elements are assumed to be initialised + * to zero and the symmetry is assumed to be properly set up. + * mospaces Mospaces object + * d_o Fock matrix to construct guesses from (occ.) + * d_v Fock matrix to construct guesses from (virt.) + * a_spin If alpha ionization (false: beta) + * restricted Is this a restricted calculation + * doublet Doublet or quartet states (only in case of restricted calc.) + * spin_change_twice Twice the value of spin change to enforce in an excitation. + * degeneracy_tolerance Tolerance for two entries of the diagonal to be considered + * degenerate, i.e. identical. + * + * \returns The number of guess vectors which have been properly initialised + * (the others are invalid and should be discarded). + */ +size_t fill_ip_doubles_guesses(std::vector> guesses_d, + std::shared_ptr mospaces, + std::shared_ptr d_o, std::shared_ptr d_v, + bool a_spin, bool restricted, bool doublet, + int spin_change_twice, scalar_type degeneracy_tolerance); + +} // namespace libadcc diff --git a/libadcc_src/fill_pp_doubles_guesses.cc b/libadcc_src/fill_pp_doubles_guesses.cc index 2bd3ebaf..b5baec55 100644 --- a/libadcc_src/fill_pp_doubles_guesses.cc +++ b/libadcc_src/fill_pp_doubles_guesses.cc @@ -1,5 +1,5 @@ // -// Copyright (C) 2020 by the adcc authors +// Copyright (C) 2026 by the adcc authors // // This file is part of adcc. // diff --git a/libadcc_src/fill_pp_doubles_guesses.hh b/libadcc_src/fill_pp_doubles_guesses.hh index b2e40158..355c1df8 100644 --- a/libadcc_src/fill_pp_doubles_guesses.hh +++ b/libadcc_src/fill_pp_doubles_guesses.hh @@ -1,5 +1,5 @@ // -// Copyright (C) 2020 by the adcc authors +// Copyright (C) 2026 by the adcc authors // // This file is part of adcc. // diff --git a/libadcc_src/guess/ea_adc_guess_d.cc b/libadcc_src/guess/ea_adc_guess_d.cc new file mode 100644 index 00000000..ea5e047d --- /dev/null +++ b/libadcc_src/guess/ea_adc_guess_d.cc @@ -0,0 +1,483 @@ +#include "ea_adc_guess_d.hh" +#include "../exceptions.hh" + +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#include +#include +#include +#include +#include +#pragma GCC visibility pop + +namespace libadcc { + +// TODO This file definitely needs a cleanup. + +using namespace libtensor; +using libtensor::index; + +/** \brief Element type for guess vectors + **/ +template +struct guess_element { + libtensor::index bidx; //!< Block index + libtensor::index idx; //!< In block index + double coeff; //!< Coefficient; + + guess_element(const libtensor::index& bidx_, const libtensor::index& idx_, + const double& coeff_) + : bidx(bidx_), idx(idx_), coeff(coeff_) {} +}; + +/** \brief Base class for guess formation **/ +template +class index_handler { + public: + protected: + libtensor::sequence*> m_ab; //!< Alpha-beta block markers + + private: + libtensor::sequence m_na; //!< Number of alpha spin blocks + + public: + /** \brief Constructor + \param ab Alpha-beta block markers (for N orbital spaces) + \param sym Symmetry of guess vectors + \param ms Spin multiplicity + **/ + index_handler(const libtensor::sequence*>& ab) + : m_ab(ab), m_na(0) { + for (size_t i = 0; i < N; i++) { + for (size_t j = 0; j < m_ab[i]->size(); j++) { + if (!m_ab[i]->at(j)) m_na[i]++; + } + } + } + + /** \brief Calculates the spin projection \f$ m_s \f$ of the block. + \param bidx Block index + \param orb_type Orbital type per dim (true = occupied) + \return -1 or +1 for ionization of an alpha or beta electron + */ + int get_spin_proj(const libtensor::mask& orb_type, + const libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + if (bidx[i] > m_ab[i]->size()) { + throw runtime_error("Block index exceeds dim"); + } + } + + int ms = 0; + for (size_t i = 0; i < N; i++) { + // Left side is true if orbital is occ. + // Right side is true if orbital has beta spin + // Hence it is true for occ. beta orbitals and virt. alpha orbitals + if (orb_type[i] == m_ab[i]->at(bidx[i])) + ms += 1; + else + ms -= 1; + } + return ms; + } + + /** \brief Split block index into spatial part and spin part + \param bidx Input block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + **/ + void split_block_index(const libtensor::index& bidx, libtensor::mask& sp, + libtensor::index& sbidx) const { + for (size_t i = 0; i < N; i++) { + sp[i] = m_ab[i]->at(bidx[i]); + sbidx[i] = (sp[i] ? bidx[i] - m_na[i] : bidx[i]); + } + } + + /** \brief Merge spatial part and spin part of block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + \param bidx Input block index + **/ + void merge_block_index(const libtensor::mask& sp, const libtensor::index& sbidx, + libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + bidx[i] = (sp[i] ? sbidx[i] + m_na[i] : sbidx[i]); + } + } +}; + +namespace { +typedef libtensor::compare4min compare_t; +typedef libtensor::btod_select<1, compare_t>::list_type list1d_t; +typedef libtensor::btod_select<3, compare_t>::list_type list3d_t; +typedef std::list*, double>> list_t; + +/** Determine if occupied indices should be symmetrized */ +void determine_sym(const symmetry<3, double>& sym, bool& sym_v) { + + sym_v = false; + for (symmetry<3, double>::iterator it1 = sym.begin(); it1 != sym.end(); it1++) { + + const symmetry_element_set<3, double>& set = sym.get_subset(it1); + const std::string& id = set.get_id(); + + if (id.compare(se_perm<3, double>::k_sym_type) != 0) continue; + if (set.is_empty()) return; + + typedef symmetry_element_set_adapter<3, double, se_perm<3, double>> adapter_t; + + adapter_t ad(set); + for (adapter_t::iterator it2 = ad.begin(); it2 != ad.end(); it2++) { + + const se_perm<3, double>& el = ad.get_elem(it2); + + const permutation<3>& p = el.get_perm(); + sym_v |= (p[1] == 2 && p[2] == 1); + } + } +} + +/** Determine the spin of the guess vectors */ +unsigned determine_spin(bool restricted, bool doublet) { + + if (restricted) { + if (doublet) + return 2; + else + return 4; + } else { + return 0; + } +} + +/** Transfers the elements of a 1D list to a 3D list */ +void transfer_elements(const list1d_t& o, const list1d_t& v, index_group_map_h2p& to, + const libtensor::symmetry<3, double>& sym, + const index_handler<3>& base, int dm_s) { + + // Determine symmetry + bool sym_v; // Are the two virtual indices identical + determine_sym(sym, sym_v); + + to.clear(); + + dimensions<3> bidims = sym.get_bis().get_block_index_dims(); + + for (list1d_t::const_iterator ita = o.begin(); ita != o.end(); ita++) { + + for (list1d_t::const_iterator itb = v.begin(); itb != v.end(); itb++) { + + for (list1d_t::const_iterator itc = v.begin(); itc != v.end(); itc++) { + + // Discard element combinations which are not allowed due to the + // permutational symmetry!!! + const index<1>& bidxa = ita->get_block_index(); + const index<1>& idxa = ita->get_in_block_index(); + const index<1>& bidxb = itb->get_block_index(); + const index<1>& idxb = itb->get_in_block_index(); + const index<1>& bidxc = itc->get_block_index(); + const index<1>& idxc = itc->get_in_block_index(); + + if (sym_v && bidxb[0] == bidxc[0] && idxb[0] == idxc[0]) continue; + + double value = ita->get_value() + itb->get_value() + itc->get_value(); + + libtensor::index<3> bidx, idx; + bidx[0] = bidxa[0]; + bidx[1] = bidxb[0]; + bidx[2] = bidxc[0]; + idx[0] = idxa[0]; + idx[1] = idxb[0]; + idx[2] = idxc[0]; + + if (sym_v && bidx[1] > bidx[2]) { + std::swap(bidx[1], bidx[2]); + std::swap(idx[1], idx[2]); + } else if (sym_v && bidx[1] == bidx[2] && idx[1] > idx[2]) { + std::swap(idx[1], idx[2]); + } + + // Ignore blocks where the targeted spin_change is not achieved + mask<3> orb_type; + orb_type[0] = true; // occ. + orb_type[1] = false; // virt. + orb_type[2] = false; // virt. + if (base.get_spin_proj(orb_type, bidx) != dm_s) continue; + + // Check if the block is allowed in the symmetry of the guess + orbit<3, double> orb(sym, bidx); + if (!orb.is_allowed()) continue; + + // Find canonical index + abs_index<3> abi(orb.get_acindex(), bidims); + const tensor_transf<3, double>& tr = orb.get_transf(bidx); + bidx = abi.get_index(); + permutation<3> pinv(tr.get_perm(), true); + idx.permute(pinv); + + // Split block index into spin part and spatial part + mask<3> spm; + index<3> spi; + base.split_block_index(bidx, spm, spi); + + to.add_index(value, spm, spi, idx); + } // for itc + } // for itb + } // for ita +} + +size_t build_guesses(list_t::iterator& cur_guess, list_t::iterator end, + const index_group_h2p& ig, double value, + const symmetry<3, double>& sym, bool a_spin, bool restricted, + bool doublet, index_handler<3>& base) { + bool sym_v; // Are the two occupied indices identical + determine_sym(sym, sym_v); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + if (cur_guess == end) return 0; + + int ms = a_spin ? 1 : -1; + + const index<3>& spidx = ig.get_spatial_bidx(); + const index<3>& idx = ig.get_idx(); + + std::vector>> lv; + + // No specific spin create as many guesses as there are available in the + // index group + if (spin == 0) { + lv.resize(ig.size()); + + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + static const double coeff[3][3] = {// in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + {1.0, -1.0, -1.0}, // quartet + {0.0, -1.0, 1.0}, // doublet 1 + {-2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + for (size_t i = 0; i < ig.size(); i++) + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } else if (spin == 2) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + lv.resize(1); + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + lv.resize(2); + + static const double coeff[2][3] = {// in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + {0.0, -1.0, 1.0}, // doublet 1 + {-2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } else if (spin == 4) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_h2p::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 3) { + lv.resize(1); + + static const double coeff[1][3] = {// in case of ms == 1 (alpha attachment) + // aaa bab bba + // and in case of ms == -1 (beta attachment) + // bbb aba aab + {1.0, -1.0, -1.0}}; // quartet + + if (ms == 1) { // alpha attachment + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta attachment + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } + + size_t i = 0; + for (; i < lv.size() && cur_guess != end; i++, cur_guess++) { + libtensor::btensor<3, double>& bt = *(cur_guess->first); + { // Setup up the symmetry + libtensor::block_tensor_wr_ctrl<3, double> ctrl(bt); + ctrl.req_zero_all_blocks(); + libtensor::symmetry<3, double>& sym_to = ctrl.req_symmetry(); + libtensor::so_copy<3, double>(sym).perform(sym_to); + } + + // Set the elements + libtensor::btod_set_elem<3> set_op; + for (auto it = lv[i].begin(); it != lv[i].end(); it++) { + set_op.perform(bt, it->bidx, it->idx, it->coeff); + } + + // Normalise + double norm = libtensor::btod_dotprod<3>(bt, bt).calculate(); + if (norm != 1.0) { + libtensor::btod_scale<3>(bt, 1.0 / sqrt(norm)).perform(); + } + cur_guess->second = value; + } + return i; +} + +} // namespace + +size_t ea_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, int dm_s, + double degeneracy_tolerance) { + + size_t nguesses = va.size(); + if (nguesses == 0) return 0; + + // TODO sym_v should be stored in an adc_guess_base-like object + // Determine symmetry and spin + bool sym_v; // Are the two occupied indices identical + determine_sym(sym, sym_v); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + size_t ns = nguesses; + index_group_map_h2p igm(degeneracy_tolerance, sym_v); + + bool max_reached = false; + // Create empty 1d symmetry to use with btod_select + symmetry<1, double> sym1(d_o.get_bis()), sym2(d_v.get_bis()); + + // Search for smallest elements until we have found enough. + size_t size = 0; + while (size < nguesses && !max_reached) { + + igm.clear(); + size = 0; + + ns *= 2; + list1d_t ilx_o, ilx_v; + btod_select<1, compare_t>(d_o, sym1).perform(ilx_o, ns); + btod_select<1, compare_t>(d_v, sym2).perform(ilx_v, ns); + + max_reached = ilx_o.size() < ns; + + index_handler<3> base(ab); + transfer_elements(ilx_o, ilx_v, igm, sym, base, dm_s); + ilx_o.clear(); + ilx_v.clear(); + + // size++; // we want to have the real size of the index group guesses + // Count the number of elements + if (spin == 0) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + size += igm.get_group(it).size(); + } + } else if (spin == 2) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 2; // two doublets, one quartet + } else if (igm.get_group(it).size() == 1) { + size += 1; // only a doublet in this case + } + } + } else if (spin == 4) { + for (index_group_map_h2p::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 1; // one quartet + } + } + } + } // while + + // Now form the guess vectors + nguesses = 0; + + list_t::iterator guess = va.begin(); + // Loop until list is empty or we have constructed all guesses + index_group_map_h2p::iterator it = igm.begin(); + while (it != igm.end() && guess != va.end()) { + index_handler<3> base(ab); + nguesses += build_guesses(guess, va.end(), igm.get_group(it), igm.get_value(it), sym, + a_spin, restricted, doublet, base); + it++; + } + + return nguesses; +} + +} // namespace libadcc diff --git a/libadcc_src/guess/ea_adc_guess_d.hh b/libadcc_src/guess/ea_adc_guess_d.hh new file mode 100644 index 00000000..9a080678 --- /dev/null +++ b/libadcc_src/guess/ea_adc_guess_d.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "index_group_h2p.hh" + +namespace libadcc { + +/** \brief Forms a list of doubles guess vectors. + Selects the smallest elements from the provided OV matrices (for Koopman's + guess this should be the delta Fock matrix) and combines two of these + elements to form the doubles guesses. + \param va List of doubles-value pairs to initialize. + \param d_o Fock matrix to construct guesses from (occ.) + \param d_v Fock matrix to construct guesses from. (virt.) + \param sym Symmetry of guess vectors. + \param a_spin If alpha ionization (false: beta) + \param restricted Is this a restricted calculation + \param doublet Doublet or quartet states (only in case of restricted + calculation) + \param ab Alpha/beta spin blocks of occupied orbitals. + \param dm_s Delta m_s, spin-change twice + \return Number of guess vectors created + **/ +size_t ea_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, int dm_s, + double degeneracy_tolerance); + +} // namespace libadcc diff --git a/libadcc_src/guess/index_group_h2p.cc b/libadcc_src/guess/index_group_h2p.cc new file mode 100644 index 00000000..e5a86ab8 --- /dev/null +++ b/libadcc_src/guess/index_group_h2p.cc @@ -0,0 +1,85 @@ +#include "index_group_h2p.hh" +#include "../exceptions.hh" +#include + +namespace libadcc { + +using namespace libtensor; +using libtensor::index; + +libtensor::mask<3> index_group_h2p::get_spin_mask(size_t sp) const { + if (m_s.count(sp) == 0) { + throw runtime_error("Could not find spin state sp ==" + std::to_string(sp) + "."); + } + return compute_spin_mask(sp); +} + +size_t index_group_h2p::compute_spin(const mask<3>& spm) { + + size_t s = 0; + for (size_t i = 0; i < 3; i++) s = s * 2 + (spm[i] ? 1 : 0); + + return s; +} + +mask<3> index_group_h2p::compute_spin_mask(size_t sp) { + + mask<3> m; + size_t i = 0, curbit = 1 << 2; + while (sp != 0 && i < 3) { + m[i++] = (sp & curbit); + curbit >>= 1; + } + return m; +} + +void index_group_map_h2p::add_index(double val, mask<3> spm, index<3> spidx, + index<3> idx) { + + find_canonical_index(spm, spidx, idx); + + // Loop over group map and look for similar value + std::multimap::iterator it = m_idxmap.begin(); + for (; it != m_idxmap.end(); it++) { + if (fabs(val - it->first) < m_thresh) break; + } + + // Try to add element to index groups which belong to similar + // values + bool added = false; + while (it != m_idxmap.end() && fabs(val - it->first) < m_thresh && !added) { + + index_group_h2p& grp = it->second; + if (spidx == grp.get_spatial_bidx() && idx == grp.get_idx()) { + grp.add(spm); + added = true; + } + it++; + } + + // If no index group found start a new one. + if (!added) { + std::multimap::iterator ic = m_idxmap.insert( + std::pair(val, index_group_h2p(spidx, idx))); + ic->second.add(spm); + } +} + +void index_group_map_h2p::find_canonical_index(mask<3>& m, index<3>& spidx, + index<3>& idx) const { + + if (m_sym_v) { + if (spidx[1] == spidx[2]) { + if (idx[1] > idx[2]) { + std::swap(idx[1], idx[2]); + std::swap(m[1], m[2]); + } + } else if (spidx[1] > spidx[2]) { + std::swap(spidx[1], spidx[2]); + std::swap(idx[1], idx[2]); + std::swap(m[1], m[2]); + } + } +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_h2p.hh b/libadcc_src/guess/index_group_h2p.hh new file mode 100644 index 00000000..fe25fad7 --- /dev/null +++ b/libadcc_src/guess/index_group_h2p.hh @@ -0,0 +1,164 @@ +#pragma once +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#pragma GCC visibility pop +#include +#include + +namespace libadcc { + +/** \brief Group of 3D block tensor elements with common spatial index + An index group is constructed by passing the spatial block index and + in-block index of a 3D block tensor element. Those two indices define + the index group. + The spin states belonging to the index group can be added using the + functions + \code + void add(const libtensor::mask<3> &); + \endcode + \code + void add(size_t); + \endcode + **/ +class index_group_h2p { + public: + typedef std::set::const_iterator iterator; + + enum { + aaa = 0, + aab = 1, + aba = 2, + abb = 3, + baa = 4, + bab = 5, + bba = 6, + bbb = 7, + }; + + private: + libtensor::index<3> m_spidx; + libtensor::index<3> m_idx; + std::set m_s; + + public: + /** \brief Constructor + \param spidx Spatial block index. + \param idx In-block index + **/ + index_group_h2p(const libtensor::index<3>& spidx, const libtensor::index<3>& idx) + : m_spidx(spidx), m_idx(idx) {} + + /** \brief Add spin state to index group + \param s Spin states index (see enum) + **/ + void add(size_t s) { m_s.insert(s); } + + /** \brief Add spin state to index group + \param spm Mask representing the spin states (beta == true) + **/ + void add(const libtensor::mask<3>& spm) { add(compute_spin(spm)); } + + /** \brief Return in-block index of index group + **/ + const libtensor::index<3>& get_idx() const { return m_idx; } + + /** \brief Return spatial block index of index group + **/ + const libtensor::index<3>& get_spatial_bidx() const { return m_spidx; } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(size_t sp) const { return m_s.find(sp) != m_s.end(); } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(const libtensor::mask<3>& spm) const { + return has_spin_state(compute_spin(spm)); + } + + /** \brief Return the number of spin states + **/ + size_t size() const { return m_s.size(); } + + /** \brief STL-style iterator to the start of the list of spin states + **/ + iterator begin() const { return m_s.begin(); } + + /** \brief STL-style iterator to the end of the list of spin states + **/ + iterator end() const { return m_s.end(); } + + /** \brief Get current spin state + **/ + size_t get_spin_state(iterator it) const { return *it; } + + /** \brief Get spin state as mask + **/ + libtensor::mask<3> get_spin_mask(size_t sp) const; + + /** \brief Get current spin state as mask + **/ + libtensor::mask<3> get_spin_mask(iterator it) const { return get_spin_mask(*it); } + + private: + static size_t compute_spin(const libtensor::mask<3>& spm); + static libtensor::mask<3> compute_spin_mask(size_t sp); +}; + +/** \brief Map of (value, index group) pairs + \sa ea_adc_guess_d, ea_adc_guess_d + **/ +class index_group_map_h2p { + public: + typedef std::multimap::const_iterator iterator; + + private: + bool m_sym_v; //!< Permutational anti-symmetry of virt indices + double m_thresh; //!< Threshold for identical values + + std::multimap m_idxmap; + + public: + /** \brief Constructor + \param thresh Threshold for identical values + \param sym_v Virt. indices have perm. anti-symmetry + */ + index_group_map_h2p(double thresh, bool sym_v = true) + : m_sym_v(sym_v), m_thresh(thresh) {} + + /** \brief Remove all elements from list + **/ + void clear() { m_idxmap.clear(); } + + /** \brief Add an index to the map + \param val Value assigned to the index + \param spm Spin state mask + \param spidx Spatial block index + \param idx In-block index + **/ + void add_index(double val, libtensor::mask<3> spm, libtensor::index<3> spidx, + libtensor::index<3> idx); + + /** \brief STL-style iterator to first element + **/ + iterator begin() const { return m_idxmap.begin(); } + + /** \brief STL-style iterator to end + **/ + iterator end() const { return m_idxmap.end(); } + + /** \brief Return the value at the current position + **/ + double get_value(iterator it) const { return it->first; } + + /** \brief Return the index group at the current position + **/ + const index_group_h2p& get_group(iterator it) const { return it->second; } + + private: + void find_canonical_index(libtensor::mask<3>& m, libtensor::index<3>& spidx, + libtensor::index<3>& idx) const; +}; + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/index_group_p2h.cc b/libadcc_src/guess/index_group_p2h.cc new file mode 100644 index 00000000..873f560b --- /dev/null +++ b/libadcc_src/guess/index_group_p2h.cc @@ -0,0 +1,85 @@ +#include "index_group_p2h.hh" +#include "../exceptions.hh" +#include + +namespace libadcc { + +using namespace libtensor; +using libtensor::index; + +libtensor::mask<3> index_group_p2h::get_spin_mask(size_t sp) const { + if (m_s.count(sp) == 0) { + throw runtime_error("Could not find spin state sp ==" + std::to_string(sp) + "."); + } + return compute_spin_mask(sp); +} + +size_t index_group_p2h::compute_spin(const mask<3>& spm) { + + size_t s = 0; + for (size_t i = 0; i < 3; i++) s = s * 2 + (spm[i] ? 1 : 0); + + return s; +} + +mask<3> index_group_p2h::compute_spin_mask(size_t sp) { + + mask<3> m; + size_t i = 0, curbit = 1 << 2; + while (sp != 0 && i < 3) { + m[i++] = (sp & curbit); + curbit >>= 1; + } + return m; +} + +void index_group_map_p2h::add_index(double val, mask<3> spm, index<3> spidx, + index<3> idx) { + + find_canonical_index(spm, spidx, idx); + + // Loop over group map and look for similar value + std::multimap::iterator it = m_idxmap.begin(); + for (; it != m_idxmap.end(); it++) { + if (fabs(val - it->first) < m_thresh) break; + } + + // Try to add element to index groups which belong to similar + // values + bool added = false; + while (it != m_idxmap.end() && fabs(val - it->first) < m_thresh && !added) { + + index_group_p2h& grp = it->second; + if (spidx == grp.get_spatial_bidx() && idx == grp.get_idx()) { + grp.add(spm); + added = true; + } + it++; + } + + // If no index group found start a new one. + if (!added) { + std::multimap::iterator ic = m_idxmap.insert( + std::pair(val, index_group_p2h(spidx, idx))); + ic->second.add(spm); + } +} + +void index_group_map_p2h::find_canonical_index(mask<3>& m, index<3>& spidx, + index<3>& idx) const { + + if (m_sym_o) { + if (spidx[0] == spidx[1]) { + if (idx[0] > idx[1]) { + std::swap(idx[0], idx[1]); + std::swap(m[0], m[1]); + } + } else if (spidx[0] > spidx[1]) { + std::swap(spidx[0], spidx[1]); + std::swap(idx[0], idx[1]); + std::swap(m[0], m[1]); + } + } +} + +} // namespace libadcc diff --git a/libadcc_src/guess/index_group_p2h.hh b/libadcc_src/guess/index_group_p2h.hh new file mode 100644 index 00000000..af80da81 --- /dev/null +++ b/libadcc_src/guess/index_group_p2h.hh @@ -0,0 +1,164 @@ +#pragma once +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#pragma GCC visibility pop +#include +#include + +namespace libadcc { + +/** \brief Group of 3D block tensor elements with common spatial index + An index group is constructed by passing the spatial block index and + in-block index of a 3D block tensor element. Those two indices define + the index group. + The spin states belonging to the index group can be added using the + functions + \code + void add(const libtensor::mask<3> &); + \endcode + \code + void add(size_t); + \endcode + **/ +class index_group_p2h { + public: + typedef std::set::const_iterator iterator; + + enum { + aaa = 0, + aab = 1, + aba = 2, + abb = 3, + baa = 4, + bab = 5, + bba = 6, + bbb = 7, + }; + + private: + libtensor::index<3> m_spidx; + libtensor::index<3> m_idx; + std::set m_s; + + public: + /** \brief Constructor + \param spidx Spatial block index. + \param idx In-block index + **/ + index_group_p2h(const libtensor::index<3>& spidx, const libtensor::index<3>& idx) + : m_spidx(spidx), m_idx(idx) {} + + /** \brief Add spin state to index group + \param s Spin states index (see enum) + **/ + void add(size_t s) { m_s.insert(s); } + + /** \brief Add spin state to index group + \param spm Mask representing the spin states (beta == true) + **/ + void add(const libtensor::mask<3>& spm) { add(compute_spin(spm)); } + + /** \brief Return in-block index of index group + **/ + const libtensor::index<3>& get_idx() const { return m_idx; } + + /** \brief Return spatial block index of index group + **/ + const libtensor::index<3>& get_spatial_bidx() const { return m_spidx; } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(size_t sp) const { return m_s.find(sp) != m_s.end(); } + + /** \brief Check if the spin state exists for index group + **/ + bool has_spin_state(const libtensor::mask<3>& spm) const { + return has_spin_state(compute_spin(spm)); + } + + /** \brief Return the number of spin states + **/ + size_t size() const { return m_s.size(); } + + /** \brief STL-style iterator to the start of the list of spin states + **/ + iterator begin() const { return m_s.begin(); } + + /** \brief STL-style iterator to the end of the list of spin states + **/ + iterator end() const { return m_s.end(); } + + /** \brief Get current spin state + **/ + size_t get_spin_state(iterator it) const { return *it; } + + /** \brief Get spin state as mask + **/ + libtensor::mask<3> get_spin_mask(size_t sp) const; + + /** \brief Get current spin state as mask + **/ + libtensor::mask<3> get_spin_mask(iterator it) const { return get_spin_mask(*it); } + + private: + static size_t compute_spin(const libtensor::mask<3>& spm); + static libtensor::mask<3> compute_spin_mask(size_t sp); +}; + +/** \brief Map of (value, index group) pairs + \sa ip_adc_guess_d, ip_adc_guess_d + **/ +class index_group_map_p2h { + public: + typedef std::multimap::const_iterator iterator; + + private: + bool m_sym_o; //!< Permutational anti-symmetry of occ indices + double m_thresh; //!< Threshold for identical values + + std::multimap m_idxmap; + + public: + /** \brief Constructor + \param thresh Threshold for identical values + \param sym_o Occ. indices have perm. anti-symmetry + */ + index_group_map_p2h(double thresh, bool sym_o = true) + : m_sym_o(sym_o), m_thresh(thresh) {} + + /** \brief Remove all elements from list + **/ + void clear() { m_idxmap.clear(); } + + /** \brief Add an index to the map + \param val Value assigned to the index + \param spm Spin state mask + \param spidx Spatial block index + \param idx In-block index + **/ + void add_index(double val, libtensor::mask<3> spm, libtensor::index<3> spidx, + libtensor::index<3> idx); + + /** \brief STL-style iterator to first element + **/ + iterator begin() const { return m_idxmap.begin(); } + + /** \brief STL-style iterator to end + **/ + iterator end() const { return m_idxmap.end(); } + + /** \brief Return the value at the current position + **/ + double get_value(iterator it) const { return it->first; } + + /** \brief Return the index group at the current position + **/ + const index_group_p2h& get_group(iterator it) const { return it->second; } + + private: + void find_canonical_index(libtensor::mask<3>& m, libtensor::index<3>& spidx, + libtensor::index<3>& idx) const; +}; + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/guess/ip_adc_guess_d.cc b/libadcc_src/guess/ip_adc_guess_d.cc new file mode 100644 index 00000000..b710874a --- /dev/null +++ b/libadcc_src/guess/ip_adc_guess_d.cc @@ -0,0 +1,483 @@ +#include "ip_adc_guess_d.hh" +#include "../exceptions.hh" + +// Change visibility of libtensor singletons to public +#pragma GCC visibility push(default) +#include +#include +#include +#include +#include +#include +#pragma GCC visibility pop + +namespace libadcc { + +// TODO This file definitely needs a cleanup. + +using namespace libtensor; +using libtensor::index; + +/** \brief Element type for guess vectors + **/ +template +struct guess_element { + libtensor::index bidx; //!< Block index + libtensor::index idx; //!< In block index + double coeff; //!< Coefficient; + + guess_element(const libtensor::index& bidx_, const libtensor::index& idx_, + const double& coeff_) + : bidx(bidx_), idx(idx_), coeff(coeff_) {} +}; + +/** \brief Base class for guess formation **/ +template +class index_handler { + public: + protected: + libtensor::sequence*> m_ab; //!< Alpha-beta block markers + + private: + libtensor::sequence m_na; //!< Number of alpha spin blocks + + public: + /** \brief Constructor + \param ab Alpha-beta block markers (for N orbital spaces) + \param sym Symmetry of guess vectors + \param ms Spin multiplicity + **/ + index_handler(const libtensor::sequence*>& ab) + : m_ab(ab), m_na(0) { + for (size_t i = 0; i < N; i++) { + for (size_t j = 0; j < m_ab[i]->size(); j++) { + if (!m_ab[i]->at(j)) m_na[i]++; + } + } + } + + /** \brief Calculates the spin projection \f$ m_s \f$ of the block. + \param bidx Block index + \param orb_type Orbital type per dim (true = occupied) + \return -1 or +1 for ionization of an alpha or beta electron + */ + int get_spin_proj(const libtensor::mask& orb_type, + const libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + if (bidx[i] > m_ab[i]->size()) { + throw runtime_error("Block index exceeds dim"); + } + } + + int ms = 0; + for (size_t i = 0; i < N; i++) { + // Left side is true if orbital is occ. + // Right side is true if orbital has beta spin + // Hence it is true for occ. beta orbitals and virt. alpha orbitals + if (orb_type[i] == m_ab[i]->at(bidx[i])) + ms += 1; + else + ms -= 1; + } + return ms; + } + + /** \brief Split block index into spatial part and spin part + \param bidx Input block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + **/ + void split_block_index(const libtensor::index& bidx, libtensor::mask& sp, + libtensor::index& sbidx) const { + for (size_t i = 0; i < N; i++) { + sp[i] = m_ab[i]->at(bidx[i]); + sbidx[i] = (sp[i] ? bidx[i] - m_na[i] : bidx[i]); + } + } + + /** \brief Merge spatial part and spin part of block index + \param sp Spin index (alpha = false, beta = true) + \param sbidx Spatial block index + \param bidx Input block index + **/ + void merge_block_index(const libtensor::mask& sp, const libtensor::index& sbidx, + libtensor::index& bidx) const { + for (size_t i = 0; i < N; i++) { + bidx[i] = (sp[i] ? sbidx[i] + m_na[i] : sbidx[i]); + } + } +}; + +namespace { +typedef libtensor::compare4min compare_t; +typedef libtensor::btod_select<1, compare_t>::list_type list1d_t; +typedef libtensor::btod_select<3, compare_t>::list_type list3d_t; +typedef std::list*, double>> list_t; + +/** Determine if occupied indices should be symmetrized */ +void determine_sym(const symmetry<3, double>& sym, bool& sym_o) { + + sym_o = false; + for (symmetry<3, double>::iterator it1 = sym.begin(); it1 != sym.end(); it1++) { + + const symmetry_element_set<3, double>& set = sym.get_subset(it1); + const std::string& id = set.get_id(); + + if (id.compare(se_perm<3, double>::k_sym_type) != 0) continue; + if (set.is_empty()) return; + + typedef symmetry_element_set_adapter<3, double, se_perm<3, double>> adapter_t; + + adapter_t ad(set); + for (adapter_t::iterator it2 = ad.begin(); it2 != ad.end(); it2++) { + + const se_perm<3, double>& el = ad.get_elem(it2); + + const permutation<3>& p = el.get_perm(); + sym_o |= (p[0] == 1 && p[1] == 0); + } + } +} + +/** Determine the spin of the guess vectors */ +unsigned determine_spin(bool restricted, bool doublet) { + + if (restricted) { + if (doublet) + return 2; + else + return 4; + } else { + return 0; + } +} + +/** Transfers the elements of a 1D list to a 3D list */ +void transfer_elements(const list1d_t& o, const list1d_t& v, index_group_map_p2h& to, + const libtensor::symmetry<3, double>& sym, + const index_handler<3>& base, int dm_s) { + + // Determine symmetry + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + + to.clear(); + + dimensions<3> bidims = sym.get_bis().get_block_index_dims(); + + for (list1d_t::const_iterator ita = o.begin(); ita != o.end(); ita++) { + + for (list1d_t::const_iterator itb = o.begin(); itb != o.end(); itb++) { + + for (list1d_t::const_iterator itc = v.begin(); itc != v.end(); itc++) { + + // Discard element combinations which are not allowed due to the + // permutational symmetry!!! + const index<1>& bidxa = ita->get_block_index(); + const index<1>& idxa = ita->get_in_block_index(); + const index<1>& bidxb = itb->get_block_index(); + const index<1>& idxb = itb->get_in_block_index(); + const index<1>& bidxc = itc->get_block_index(); + const index<1>& idxc = itc->get_in_block_index(); + + if (sym_o && bidxa[0] == bidxb[0] && idxa[0] == idxb[0]) continue; + + double value = ita->get_value() + itb->get_value() + itc->get_value(); + + libtensor::index<3> bidx, idx; + bidx[0] = bidxa[0]; + bidx[1] = bidxb[0]; + bidx[2] = bidxc[0]; + idx[0] = idxa[0]; + idx[1] = idxb[0]; + idx[2] = idxc[0]; + + if (sym_o && bidx[0] > bidx[1]) { + std::swap(bidx[0], bidx[1]); + std::swap(idx[0], idx[1]); + } else if (sym_o && bidx[0] == bidx[1] && idx[0] > idx[1]) { + std::swap(idx[0], idx[1]); + } + + // Ignore blocks where the targeted spin_change is not achieved + mask<3> orb_type; + orb_type[0] = true; // occ. + orb_type[1] = true; // occ. + orb_type[2] = false; // virt. + if (base.get_spin_proj(orb_type, bidx) != dm_s) continue; + + // Check if the block is allowed in the symmetry of the guess + orbit<3, double> orb(sym, bidx); + if (!orb.is_allowed()) continue; + + // Find canonical index + abs_index<3> abi(orb.get_acindex(), bidims); + const tensor_transf<3, double>& tr = orb.get_transf(bidx); + bidx = abi.get_index(); + permutation<3> pinv(tr.get_perm(), true); + idx.permute(pinv); + + // Split block index into spin part and spatial part + mask<3> spm; + index<3> spi; + base.split_block_index(bidx, spm, spi); + + to.add_index(value, spm, spi, idx); + } // for itc + } // for itb + } // for ita +} + +size_t build_guesses(list_t::iterator& cur_guess, list_t::iterator end, + const index_group_p2h& ig, double value, + const symmetry<3, double>& sym, bool a_spin, bool restricted, + bool doublet, index_handler<3>& base) { + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + if (cur_guess == end) return 0; + + int ms = a_spin ? -1 : 1; + + const index<3>& spidx = ig.get_spatial_bidx(); + const index<3>& idx = ig.get_idx(); + + std::vector>> lv; + + // No specific spin create as many guesses as there are available in the + // index group + if (spin == 0) { + lv.resize(ig.size()); + + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + static const double coeff[3][3] = {// in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + {1.0, -1.0, -1.0}, // quartet + {0.0, -1.0, 1.0}, // doublet 1 + {-2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 3; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + for (size_t i = 0; i < ig.size(); i++) + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } else if (spin == 2) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 1) { + lv.resize(1); + + static double coeff[1] = {1.0}; + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 1; j++) + lv[j].push_back(guess_element<3>(bidx[i], idx, coeff[j])); + } + } else if (ig.size() == 3) { + lv.resize(2); + + static const double coeff[2][3] = {// in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + {0.0, -1.0, 1.0}, // doublet 1 + {-2.0, -1.0, -1.0}}; // doublet 2 + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } else if (spin == 4) { + // Reform full block indices + size_t i = 0; + std::vector> bidx(ig.size()); + for (index_group_p2h::iterator it = ig.begin(); it != ig.end(); it++, i++) { + base.merge_block_index(ig.get_spin_mask(it), spidx, bidx[i]); + } + + if (ig.size() == 3) { + lv.resize(1); + + static const double coeff[1][3] = {// in case of ms == -1 (alpha ionization) + // aaa abb bab + // and in case of ms == 1 (beta ionization) + // bbb baa aba + {1.0, -1.0, -1.0}}; // quartet + + if (ms == -1) { // alpha ionization + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[j], idx, coeff[i][j])); + } + } else { // beta ionization + for (size_t i = 0; i < 1; i++) { + for (size_t j = 0; j < 3; j++) + lv[i].push_back(guess_element<3>(bidx[2 - j], idx, coeff[i][j])); + } + } + } else { + // Form spin elements + lv.resize(ig.size()); + for (size_t i = 0; i < ig.size(); i++) { + lv[i].push_back(guess_element<3>(bidx[i], idx, 1.0)); + } + } + } + + size_t i = 0; + for (; i < lv.size() && cur_guess != end; i++, cur_guess++) { + libtensor::btensor<3, double>& bt = *(cur_guess->first); + { // Setup up the symmetry + libtensor::block_tensor_wr_ctrl<3, double> ctrl(bt); + ctrl.req_zero_all_blocks(); + libtensor::symmetry<3, double>& sym_to = ctrl.req_symmetry(); + libtensor::so_copy<3, double>(sym).perform(sym_to); + } + + // Set the elements + libtensor::btod_set_elem<3> set_op; + for (auto it = lv[i].begin(); it != lv[i].end(); it++) { + set_op.perform(bt, it->bidx, it->idx, it->coeff); + } + + // Normalise + double norm = libtensor::btod_dotprod<3>(bt, bt).calculate(); + if (norm != 1.0) { + libtensor::btod_scale<3>(bt, 1.0 / sqrt(norm)).perform(); + } + cur_guess->second = value; + } + return i; +} + +} // namespace + +size_t ip_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, int dm_s, + double degeneracy_tolerance) { + + size_t nguesses = va.size(); + if (nguesses == 0) return 0; + + // TODO sym_o should be stored in an adc_guess_base-like object + // Determine symmetry and spin + bool sym_o; // Are the two occupied indices identical + determine_sym(sym, sym_o); + const unsigned spin = determine_spin(restricted, doublet); // Spin of the symmetry + + size_t ns = nguesses; + index_group_map_p2h igm(degeneracy_tolerance, sym_o); + + bool max_reached = false; + // Create empty 1d symmetry to use with btod_select + symmetry<1, double> sym1(d_o.get_bis()), sym2(d_v.get_bis()); + + // Search for smallest elements until we have found enough. + size_t size = 0; + while (size < nguesses && !max_reached) { + + igm.clear(); + size = 0; + + ns *= 2; + list1d_t ilx_o, ilx_v; + btod_select<1, compare_t>(d_o, sym1).perform(ilx_o, ns); + btod_select<1, compare_t>(d_v, sym2).perform(ilx_v, ns); + + max_reached = ilx_o.size() < ns; + + index_handler<3> base(ab); + transfer_elements(ilx_o, ilx_v, igm, sym, base, dm_s); + ilx_o.clear(); + ilx_v.clear(); + + // size++; // we want to have the real size of the index group guesses + // Count the number of elements + if (spin == 0) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + size += igm.get_group(it).size(); + } + } else if (spin == 2) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 2; // two doublets, one quartet + } else if (igm.get_group(it).size() == 1) { + size += 1; // only a doublet in this case + } + } + } else if (spin == 4) { + for (index_group_map_p2h::iterator it = igm.begin(); it != igm.end(); it++) { + if (igm.get_group(it).size() == 3) { + size += 1; // one quartet + } + } + } + } // while + + // Now form the guess vectors + nguesses = 0; + + list_t::iterator guess = va.begin(); + // Loop until list is empty or we have constructed all guesses + index_group_map_p2h::iterator it = igm.begin(); + while (it != igm.end() && guess != va.end()) { + index_handler<3> base(ab); + nguesses += build_guesses(guess, va.end(), igm.get_group(it), igm.get_value(it), sym, + a_spin, restricted, doublet, base); + it++; + } + + return nguesses; +} + +} // namespace libadcc diff --git a/libadcc_src/guess/ip_adc_guess_d.hh b/libadcc_src/guess/ip_adc_guess_d.hh new file mode 100644 index 00000000..9661b59b --- /dev/null +++ b/libadcc_src/guess/ip_adc_guess_d.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "index_group_p2h.hh" + +namespace libadcc { + +/** \brief Forms a list of doubles guess vectors. + Selects the smallest elements from the provided OV matrices (for Koopman's + guess this should be the delta Fock matrix) and combines two of these + elements to form the doubles guesses. + \param va List of doubles-value pairs to initialize. + \param d_o Fock matrix to construct guesses from (occ.) + \param d_v Fock matrix to construct guesses from. (virt.) + \param sym Symmetry of guess vectors. + \param a_spin If alpha ionization (false: beta) + \param restricted Is this a restricted calculation + \param doublet Doublet or quartet states (only in case of restricted + calculation) + \param ab Alpha/beta spin blocks of occupied orbitals. + \param dm_s Delta m_s, spin-change twice + \return Number of guess vectors created + **/ +size_t ip_adc_guess_d(std::list*, double>>& va, + libtensor::btensor_i<1, double>& d_o, + libtensor::btensor_i<1, double>& d_v, + const libtensor::symmetry<3, double>& sym, bool a_spin, + bool restricted, bool doublet, + const libtensor::sequence<3, std::vector*>& ab, int dm_s, + double degeneracy_tolerance); + +} // namespace libadcc diff --git a/libadcc_src/pyiface/ExportAdcc.cc b/libadcc_src/pyiface/ExportAdcc.cc index a88aad09..b96bfc89 100644 --- a/libadcc_src/pyiface/ExportAdcc.cc +++ b/libadcc_src/pyiface/ExportAdcc.cc @@ -28,6 +28,8 @@ namespace libadcc { void export_AdcMemory(py::module& m); void export_adc_pp(py::module& m); +void export_adc_ip(py::module& m); +void export_adc_ea(py::module& m); void export_HartreeFockProvider(py::module& m); void export_MoIndexTranslation(py::module& m); void export_MoSpaces(py::module& m); @@ -40,6 +42,9 @@ void export_threading(py::module& m); PYBIND11_MODULE(libadcc, m) { libadcc::export_AdcMemory(m); + libadcc::export_adc_pp(m); + libadcc::export_adc_ip(m); + libadcc::export_adc_ea(m); libadcc::export_threading(m); libadcc::export_HartreeFockProvider(m); libadcc::export_MoSpaces(m); @@ -47,7 +52,6 @@ PYBIND11_MODULE(libadcc, m) { libadcc::export_Symmetry(m); libadcc::export_Tensor(m); libadcc::export_ReferenceState(m); - libadcc::export_adc_pp(m); // Set metadata about libtensor py::dict tensor_backend; diff --git a/libadcc_src/pyiface/export_adc_ea.cc b/libadcc_src/pyiface/export_adc_ea.cc new file mode 100644 index 00000000..6a095ad2 --- /dev/null +++ b/libadcc_src/pyiface/export_adc_ea.cc @@ -0,0 +1,56 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "../amplitude_vector_enforce_spin_kind.hh" +#include "../fill_ea_doubles_guesses.hh" +#include +#include + +namespace libadcc { + +namespace py = pybind11; +using namespace pybind11::literals; + +void export_adc_ea(py::module& m) { + m.def("amplitude_vector_enforce_spin_kind", &litude_vector_enforce_spin_kind, + "Apply the spin symmetrisation required to make the doubles and higher parts of " + "an amplitude vector consist of components for a particular spin kind only."); + + m.def("fill_ea_doubles_guesses", &fill_ea_doubles_guesses, "guesses_d"_a, "mospaces"_a, + "d_o"_a, "d_v"_a, "a_spin"_a, "restricted"_a, "doublet"_a, "spin_change_twice"_a, + "degeneracy_tolerance"_a, + "Fill the passed vector of doubles blocks with doubles guesses using " + "the O and V matrices, which are the two Fock matrices " + "involved in the doubles block.\n\nguesses_d Vectors of guesses, " + "all elements are assumed to be initialised to zero and the symmetry " + "is assumed to be properly set up.\nmospaces Mospaces object" + "\nd_o Matrix to construct guesses from (occ.)" + "\nd_v Matrix to construct guesses from (virt.)" + "\na_spin If alpha ionization (false: beta)" + "\nrestricted Is this a restricted calculation" + "\ndoublet Doublet or quartet states (only in case of" + "restricted calculation)" + "\nspin_change_twice Twice the value of the spin change to enforce " + "in an excitation.\ndegeneracy_tolerance Tolerance for two entries of " + "the diagonal to be considered degenerate, i.e. identical." + "\nReturns The number of guess vectors which have been properly " + "initialised (the others are invalid and should be discarded)."); +} + +} // namespace libadcc \ No newline at end of file diff --git a/libadcc_src/pyiface/export_adc_ip.cc b/libadcc_src/pyiface/export_adc_ip.cc new file mode 100644 index 00000000..6b45334d --- /dev/null +++ b/libadcc_src/pyiface/export_adc_ip.cc @@ -0,0 +1,56 @@ +// +// Copyright (C) 2026 by the adcc authors +// +// This file is part of adcc. +// +// adcc is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// adcc is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with adcc. If not, see . +// + +#include "../amplitude_vector_enforce_spin_kind.hh" +#include "../fill_ip_doubles_guesses.hh" +#include +#include + +namespace libadcc { + +namespace py = pybind11; +using namespace pybind11::literals; + +void export_adc_ip(py::module& m) { + m.def("amplitude_vector_enforce_spin_kind", &litude_vector_enforce_spin_kind, + "Apply the spin symmetrisation required to make the doubles and higher parts of " + "an amplitude vector consist of components for a particular spin kind only."); + + m.def("fill_ip_doubles_guesses", &fill_ip_doubles_guesses, "guesses_d"_a, "mospaces"_a, + "d_o"_a, "d_v"_a, "a_spin"_a, "restricted"_a, "doublet"_a, "spin_change_twice"_a, + "degeneracy_tolerance"_a, + "Fill the passed vector of doubles blocks with doubles guesses using " + "the O and V matrices., which are the two Fock matrices " + "involved in the doubles block.\n\nguesses_d Vectors of guesses, " + "all elements are assumed to be initialised to zero and the symmetry " + "is assumed to be properly set up.\nmospaces Mospaces object" + "\nd_o Matrix to construct guesses from (occ.)" + "\nd_v Matrix to construct guesses from (virt.)" + "\na_spin If alpha ionization (false: beta)" + "\nrestricted Is this a restricted calculation" + "\ndoublet Doublet or quartet states (only in case of" + "restricted calculation)" + "\nspin_change_twice Twice the value of the spin change to enforce " + "in an excitation.\ndegeneracy_tolerance Tolerance for two entries of " + "the diagonal to be considered degenerate, i.e. identical." + "\nReturns The number of guess vectors which have been properly " + "initialised (the others are invalid and should be discarded)."); +} + +} // namespace libadcc \ No newline at end of file