Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IP/EA-ADC up to 3rd order #168

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open

IP/EA-ADC up to 3rd order #168

wants to merge 40 commits into from

Conversation

fedy9
Copy link

@fedy9 fedy9 commented Jul 17, 2023

  • added IP/EA ADC folders like adc_pp
  • added type attribute to ADC matrix to distinguish between IP/EA/PP (e.g. matrix.type="ip"). Accessible wherever it's necessary
  • IP/EA ADC matrices up to 3rd order yielding excitation energies
  • added pole strengths attribute to ExcitedStates class in analogy to oscillator strengths
  • IP/EA pole strengths up to 3rd order computing f amplitudes as intermediate
  • added state_diffdm's and s2s transition dm's up to 2nd order for IP/EA
  • new guess setup for IP/EA: appropriate estimate of guesses, symmetry setup, guess construction on C++ side
  • generalisation of multiple PP specific routines, e.g. the singles block is selected dynamically (p/h/ph) and no longer automatically "ph"
  • New and adapted error handling
  • modified 'validate_state_parameters()' function in workflow to check for valid IP/EA inputs and returns a new Boolean 'is_alpha' which is None for PP and specifies whether an alpha or beta electron is attached/detached for EA/IP. Default value is "True". Added keyword 'n_doublets' specifying the doublet states for IP/EA
  • spin_change calculation was updated for IP/EA case in workflow
  • New enforce_spin_kind routines for IP/EA
  • adapted output: during calc. (solver printing) and adaptations for the ExcitedStates methods 'describe()' and 'describe_amplitudes()'
  • added total dipole moment property in ExcitedStates for all calculations (PP/IP/EA)
  • Warning for state dipole moments of IP/EA states since dipole moments of charged molecules are gauge dependent

New methods on user side:
ip_adc{0..3}()
ea_adc{0..3}()
New keywords on user side:
n_doublets, is_alpha
e.g. ip_adc3(scf_result, n_doublets=5, is_alpha=True)

Remarks:
Energies and properties were verified against Q-Chem calculations.

Pole strengths of 3rd order were implemented for completeness. They are currently not used in the calculation since all properties are computed at most at 2nd order even for ADC(3) calculations. Hence they were not verified yet.

No simultaneous calculation of alpha and beta states to simplify the code and error handling and to keep consistency with PP calculations. If alpha and beta would be computed in the same adcc instance, intermediates could be reused. However, these intermediates are not the bottleneck of the calculation and thus recomputed for the other spin.

Quartets not yet implemented since they are pure doubles states which are inaccurate up to 3rd order.

To Do
Testing procedures for IP/EA ADC
Verify 3rd order pole strengths

Observed issue
True for PP/IP/EA
If 'kind="any"' and the user requests a significant amount of states getting somewhere near the number of total accessible states, the Davidson will quickly converge to 0-vector. This is not true if kind is specified.
Example input

from pyscf import gto, scf
import adcc

# Run SCF in pyscf
mol = gto.M(
    atom='O 0.0 0.0 0.0;'
         'H 0.0 0.0 0.957;'
         'H 0.927 0.0 -0.240',
    basis='sto-3g',
    spin=0
)
scfres = scf.RHF(mol)
scfres.conv_tol = 1e-13
scfres.kernel()

state = adcc.adc2(scfres, n_states=65)

Adrian Mueller and others added 30 commits August 24, 2022 15:49
…od.at_level()' + prettying up output of 'ExcitedStates.describe_amplitudes()'
There was a name conflict leading to (only) wrong Adc3 equations.
Intermediates could be grouped together
moved 'available_kinds' one level lower to 'method' instead of the
system/case since it is method dependent
adapted AdcMockState to have right attributes and gets the right blocks
Adapted ref data and read-in so that 'available_kinds' is tried to be
set from system level (normal ref data) and from adc level for adcc ref
data. De facto checks flexible where 'available_kinds' is stored
added 'pole_strength' tot the property blacklist for pp tests
added 'is_alpha' attribute to Ex.St. object for the 'describe()' method
to work properly
for cn_sto3g, UHF ref. alpha and beta ip/ea ref data generated and
adapted read-in for those files
pole_strenghts tests added to test_properties_consistency
since PP-ADC calculations uses the same intermediates they are now only
defined once
PP and IP/EA ADc calcs have different excitation properties
Blacklist the non-existing ones
@fedy9
Copy link
Author

fedy9 commented Mar 12, 2024

Added ip/ea adc2x for consistency with pp basemethods

Added tests for IP/EA-ADC:

  • workflow
  • state_densities
  • consistent properties
  • guesses

Modified adcc testdata generation:
the adcc hdf5 reference data have to be regenerated so the pp tests pass,
since I moved "available_kinds" to the "adc" level

Modified read-in of reference data and creating MockAdcStates

Generated ip/ea reference data for h2o_sto3g and cn_sto3g
Testing alpha and beta attachment/detachment for unrestricted cn_sto3g
Only alpha for restricted h2o_sto3g

Removed some intermediates in ip and ea matrix.py to avoid conflicts and reuse code in pp matrix.py

Introduced a blacklist for excitation_properties that depend on the type (pp vs ip/ea)

All former tests are running
(except: pcm-lr/ptlr using pyscf that fails with pyscf version 2.3.0, scf energy fails,
pcm with ddx solver since not installed)

Copy link
Member

@maxscheurer maxscheurer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice I think! If you can come up with some minimal test for each method, that'd be great!

Comment on lines 31 to 86
__all__ = ["block"]

# TODO One thing one could still do to improve timings is implement a "fast einsum"
# that does not call opt_einsum, but directly dispatches to libadcc. This could
# lower the call overhead in the applies for the cases where we have only a
# trivial einsum to do. For the moment I'm not convinced that is worth the
# effort ... I suppose it only makes a difference for the cheaper ADC variants
# (ADC(0), ADC(1), CVS-ADC(0-2)-x), but then on the other hand they are not
# really so much our focus.


#
# Dispatch routine
#
"""
`apply` is a function mapping an AmplitudeVector to the contribution of this
block to the result of applying the ADC matrix. `diagonal` is an
`AmplitudeVector` containing the expression to the diagonal of the ADC matrix
from this block.
"""
AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"])


def block(ground_state, spaces, order, variant=None, intermediates=None):
"""
Gets ground state, potentially intermediates, spaces (ph, pphh and so on)
and the perturbation theory order for the block,
variant is "cvs" or sth like that.

It is assumed largely, that CVS is equivalent to
mp.has_core_occupied_space, while one would probably want in the long run
that one can have an "o2" space, but not do CVS.
"""
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)])

if fn not in globals():
raise ValueError("Could not dispatch: "
f"spaces={spaces} order={order} variant=variant")
return globals()[fn](reference_state, ground_state, intermediates)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the globals() stuff, this is identical to the other matrix functions, right? I wonder if there's a way to reduce code duplication...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What could be done is adding a new function in adc_pp/matrix.py or maybe in a new general file:

def get_block_prereqs(ground_state, spaces, order, variant=None,
                        intermediates=None):
    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.")

    return ("_".join(["block"] + variant + spaces + [str(order)]),
            reference_state)

and then importing it in the ip and ea matrix.py files and using the block routine as:

def block(ground_state, spaces, order, variant=None, intermediates=None):
    fn, reference_state = get_block_prereqs(ground_state, spaces, order,
                                       variant=None, intermediates=None)

    if fn not in globals():
        raise ValueError("Could not dispatch: "
                         f"spaces={spaces} order={order} variant=variant")
    return globals()[fn](reference_state, ground_state, intermediates)

I think more cannot be reduced while keeping this structure. Also the
AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"])
seems necessary.
I'm not sure about the _all_= line since I think wildcard imports don't occur for the matrices. So if it is needed in general?

if fn not in globals():
raise ValueError("Could not dispatch: "
f"spaces={spaces} order={order} variant=variant")
return globals()[fn](reference_state, ground_state, intermediates)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@fedy9
Copy link
Author

fedy9 commented Sep 5, 2024

Looks nice I think! If you can come up with some minimal test for each method, that'd be great!

And thanks for reviewing :)

Since I already added some tests, I'm not sure what you have in mind here. The obvious missing tests are testing against Q-Chem. However I struggled with that on the one hand with the interface and on the other hand due to differing different SCF solutions (psi4/pyscf vs qchem energies) that occured for some of the small set of open-shell test molecules leading to error propagation using more or less default inputs. But that should be doable.

@maxscheurer
Copy link
Member

Since I already added some tests, I'm not sure what you have in mind here. The obvious missing tests are testing against Q-Chem. However I struggled with that on the one hand with the interface and on the other hand due to differing different SCF solutions (psi4/pyscf vs qchem energies) that occured for some of the small set of open-shell test molecules leading to error propagation using more or less default inputs. But that should be doable.

I was talking about testing against Q-Chem. That would be great, because then one can be sure nothing gets broken with the IP/EA feature.

The adcc-testdata setup is somewhat complicated, right. For this reason, for the PE-ADC checks against Q-Chem, I wrote this file which essentially just invokes Q-Chem, runs PE-ADC, and dumps the results to a YAML file. Maybe you could do the same for IP/EA-ADC, which would be totally fine for me. Then we'd have some minimal functionality tests that run on CI.

added qchem ref data to yml file and added new test file probing
ip/ea-adc(2/3) energies and pole strengths against qchem implementation
@fedy9
Copy link
Author

fedy9 commented Sep 6, 2024

I reuse some of the code that appears in all three matrix files by adding a new function covering that which can be called in the other matrix files

Further I added minimal testing against Q-Chem reference data, i.e. h2o-sto3g excitation energies and pole strengths for the first 2-3 states of IP/EA-ADC(2). I dumped the reference values in the qchem yml file

@jonasleitner jonasleitner self-assigned this Oct 23, 2024
@jonasleitner
Copy link
Contributor

jonasleitner commented Oct 24, 2024

I don't understand the current separation of methods between ElectronicTransition and ExcitedStates. Some methods on both classes are PP-ADC specific while others are general for PP/IP/EA-ADC. For instance, describe and the amplitude printout on ExcitedStates or transition_dipole_moment and oscillator_strength on ElectronicTransition are PP specific. I think it would make more sense to put all methods that are general for PP/IP/EA-ADC on the ElectronicTransition class and then have a PP-ADC specific ExcitedStates child class and another IonizedStates child class for IP/EA. And similarly, FormatExcitationVector should also be refactored using a generic base class and specific child classes for PP/IP/EA.
What do you think @apapapostolou @maxscheurer @fedy9?

@apapapostolou
Copy link
Contributor

I don't understand the current separation of methods between ElectronicTransition and ExcitedStates. Some methods on both classes are PP-ADC specific while others are general for PP/IP/EA-ADC. For instance, describe and the amplitude printout on ExcitedStates or transition_dipole_moment and oscillator_strength on ElectronicTransition are PP specific. I think it would make more sense to put all methods that are general for PP/IP/EA-ADC on the ElectronicTransition class and then have a PP-ADC specific ExcitedStates child class and another IonizedStates child class for IP/EA. And similarly, FormatExcitationVector should also be refactored using a generic base class and specific child classes for PP/IP/EA. What do you think @apapapostolou @maxscheurer @fedy9?

I agree with @jonasleitner. I also don't understand the current separation between ElectronicTransition and ExcitedStates. How was it decided which methods are implemented where @maxscheurer? I think it would make sense to have a parent class for methods that are needed for all variants (PP/IP/EA) and then more specific child classes.

@maxscheurer
Copy link
Member

From the top of my head, it can be rationalized as follows: Electronic excitation refers to a single excitation and the corresponding properties, whereas ExcitedStates is kind of a collection of ElectronicExcitations (list of structs vs struct of lists). That way, specific excitations can be addressed individually, for example when computing the gradient or so. Let's put this on the agenda for the next meeting, and I will look thru the code again. Please try to play around with the features of both classes also. I think there is a simple function on ExcitedStates that converts it to a list of ElectronicExcitations.

@jonasleitner
Copy link
Contributor

jonasleitner commented Oct 26, 2024

From the top of my head, it can be rationalized as follows: Electronic excitation refers to a single excitation and the corresponding properties, whereas ExcitedStates is kind of a collection of ElectronicExcitations (list of structs vs struct of lists). That way, specific excitations can be addressed individually, for example when computing the gradient or so. Let's put this on the agenda for the next meeting, and I will look thru the code again. Please try to play around with the features of both classes also. I think there is a simple function on ExcitedStates that converts it to a list of ElectronicExcitations.

I think you are confusing ElectronicTransition with Excitation. But Excitation will certainly also be affected when we restructure ElectronicTransition and ExcitedStates. So good point. Depending on the use case it might be a good idea to refactor Excitation anyway, because currently it computes e.g., the diff_dm of all states and then extracts the result of the state of interest, which is very convenient. But we don't gain any performance if we are only interested in the properties of a single state, which was kind of my initial expectation.

@maxscheurer
Copy link
Member

Ups sorry 😂 I was away from my computer, so didn't check again...
I need to think again why it was split up this way, it's been a while.

@@ -123,6 +137,12 @@ def __init__(self, method, hf_or_mp, block_orders=None, intermediates=None,

self.timer = Timer()
self.method = method
if "ip" in method.name:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be handled within the AdcMethod class. Maybe add a adc_type / adc_variant / ... member variable to the class. You can also keep a copy (probably with matching name) on the matrix class for better accessibility.


split = method.split("-")
self.__base_method = split[-1]
split = split[:-1]
self.__base_method = split.pop()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think base_method should just be adc2. The adc variant (pp/ip/ea) should be stored in a separate variable that needs to be taken into account below (at_level and name) to reassemble the correct method string.

but not do CVS.
"""
fn, reference_state = get_block_prereqs(
ground_state, spaces, order, variant, intermediates)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a good idea to use a common function to build the function name and to perform some common sanity checks. But it should only return the function name (we have access to the reference_state through the ground state). Probably something like get_block_name is a more fitting name then.
Also a small bug: If intermediates is passed as None the initialization currently happens in get_block_prereqs and is not returned. Probably the lines

if intermediates is None:
    intermediates = Intermediates(ground_state)

should be moved back into the block function

`AmplitudeVector` containing the expression to the diagonal of the ADC matrix
from this block.
"""
AdcBlock = namedtuple("AdcBlock", ["apply", "diagonal"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to import AdcBlock from adc_pp.matrix (same for ip)

if fn not in globals():
raise ValueError("Could not dispatch: "
f"spaces={spaces} order={order} variant=variant")
return globals()[fn](reference_state, ground_state, intermediates)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should raise an explicit exception if cvs is defined for EA/IP, since get_block_prereqs only checks if it is consistently set in the ground_state and the variant.


# 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
spin_change = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to set spin_change to the default value None if guesses are provided. Since we don't run the guess function in this case we should be fine. (If it can't be removed entirely from here)

spin_flip=dict(spin_block_symmetrisation="none", spin_change=-1),
any=dict(spin_block_symmetrisation="none", spin_change=0),
singlet=dict(spin_block_symmetrisation="symmetric"),
doublet=dict(spin_block_symmetrisation="none"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it not be possible to give guess_kwargs_kind is_alpha to determine the spin change for IP and EA? Then it would not be necessary to forward spin_change all the way from run_adc.
Maybe the spin change can then be fully handled down here? (Also for spin_flip?)

"ip_adc0": pole_strength_ip_adc0,
"ip_adc1": pole_strength_ip_adc0,
"ip_adc2": pole_strength_ip_adc2,
"ip_adc3": pole_strength_ip_adc3,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly adc3 pole strengths are implemented but not used yet, right?
So PP-, IP- and EA-ADC(3) all consistently use ISR2 for properties?
Rename to ip-adcn (see AdcMethod.py)

valid_bases = ["adc0", "adc1", "adc2", "adc2x", "adc3"]
valid_bases = ["adc0", "adc1", "adc2", "adc2x", "adc3",
"ip_adc0", "ip_adc1", "ip_adc2", "ip_adc2x", "ip_adc3",
"ea_adc0", "ea_adc1", "ea_adc2", "ea_adc2x", "ea_adc3"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intuitively I would expect ip-adc2 to be a valid method (- vs _). Probably it would be best to accept both (for instance with a replace in AdcMethod.__init__). However, internally we should only use one form. I think ip-adcn to stay consistent with cvs-adcn.

"ea_adc1": dict(p_p=1, p_pph=None, pph_p=None, pph_pph=None),
"ea_adc2": dict(p_p=2, p_pph=1, pph_p=1, pph_pph=0),
"ea_adc2x": dict(p_p=2, p_pph=1, pph_p=1, pph_pph=1),
"ea_adc3": dict(p_p=3, p_pph=2, pph_p=2, pph_pph=1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in AdcMethod.py. I think we should consistently use ip-adcn and ea-adcn internally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants