-
Notifications
You must be signed in to change notification settings - Fork 21
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
base: master
Are you sure you want to change the base?
Conversation
…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
Added ip/ea adc2x for consistency with pp basemethods Added tests for IP/EA-ADC:
Modified adcc testdata generation: Modified read-in of reference data and creating MockAdcStates Generated ip/ea reference data for h2o_sto3g and cn_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 |
There was a problem hiding this 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!
adcc/adc_ea/matrix.py
Outdated
__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) |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
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. |
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 |
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
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 |
I don't understand the current separation of methods between |
I agree with @jonasleitner. I also don't understand the current separation between |
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 |
Ups sorry 😂 I was away from my computer, so didn't check again... |
@@ -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: |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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"]) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"), |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
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