Exemple #1
0
def test_restricted_RPA_triplet_c1():
    "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine"
    h2o = psi4.geometry("""
    O
    H 1 0.96
    H 1 0.96 2 104.5
    symmetry c1
    """)
    psi4.set_options({"scf_type": "pk", 'save_jk': True})
    e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True)
    A_ref, B_ref = build_RHF_AB_C1_triplet(wfn)
    ni, na, _, _ = A_ref.shape
    nia = ni * na
    A_ref = A_ref.reshape((nia, nia))
    B_ref = B_ref.reshape((nia, nia))
    P_ref = A_ref + B_ref
    M_ref = A_ref - B_ref
    # Build engine
    eng = TDRSCFEngine(wfn, ptype='rpa', triplet=True)
    # our "guess"" vectors
    ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)]
    Px, Mx = eng.compute_products(ID)[:-1]
    P_test = np.column_stack([x.to_array().flatten() for x in Px])
    assert compare_arrays(P_ref, P_test, 8, "RHF (A+B)x C1 products")
    M_test = np.column_stack([x.to_array().flatten() for x in Mx])
    assert compare_arrays(M_ref, M_test, 8, "RHF (A-B)x C1 products")
Exemple #2
0
def test_restricted_RPA_triplet_c1():
    "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine"
    h2o = psi4.geometry("""
    O
    H 1 0.96
    H 1 0.96 2 104.5
    symmetry c1
    """)
    psi4.set_options({"scf_type": "pk", 'save_jk': True})
    e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True)
    A_ref, B_ref = build_RHF_AB_C1_triplet(wfn)
    ni, na, _, _ = A_ref.shape
    nia = ni * na
    A_ref = A_ref.reshape((nia, nia))
    B_ref = B_ref.reshape((nia, nia))
    P_ref = A_ref + B_ref
    M_ref = A_ref - B_ref
    # Build engine
    eng = TDRSCFEngine(wfn, ptype='rpa', triplet=True)
    # our "guess"" vectors
    ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)]
    Px, Mx = eng.compute_products(ID)[:-1]
    P_test = np.column_stack([x.to_array().flatten() for x in Px])
    assert compare_arrays(P_ref, P_test, 8, "RHF (A+B)x C1 products")
    M_test = np.column_stack([x.to_array().flatten() for x in Mx])
    assert compare_arrays(M_ref, M_test, 8, "RHF (A-B)x C1 products")
Exemple #3
0
def test_restricted_TDA_singlet_df():
    "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine"
    h2o = psi4.geometry("""
    O
    H 1 0.96
    H 1 0.96 2 104.5
    """)
    psi4.set_options({"scf_type": "df", 'save_jk': True})
    e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True)
    A_blocks, B_blocks = build_RHF_AB_singlet_df(wfn)
    eng = TDRSCFEngine(wfn, ptype='tda', triplet=False)
    vir_dim = wfn.nmopi() - wfn.doccpi()
    for hia, A_block in enumerate(A_blocks):
        ID = []
        # Construct a matrix for each (O, V) pair with hia symmetry.
        for hi in range(wfn.nirrep()):
            for i in range(wfn.Ca_subset("SO", "OCC").coldim()[hi]):
                for a in range(wfn.Ca_subset("SO", "VIR").coldim()[hi ^ hia]):
                    matrix = psi4.core.Matrix("Test Matrix", wfn.doccpi(), vir_dim, hia)
                    matrix.set(hi, i, a, 1)
                    ID.append(matrix)
        x = eng.compute_products(ID)[0][0]
        # Assemble the A values as a single (ia, jb) matrix, all possible ia and jb of symmetry hia.
        A_test = np.column_stack([np.concatenate([y.flatten() for y in x.to_array()]) for x in eng.compute_products(ID)[0]])
        assert compare_arrays(A_block, A_test, 8, "DF-RHF Ax C2v products")
Exemple #4
0
def test_RU_TDA_C1():
    h2o = psi4.geometry("""0 1
    O          0.000000    0.000000    0.135446
    H         -0.000000    0.866812   -0.541782
    H         -0.000000   -0.866812   -0.541782
    symmetry c1
    no_reorient
    no_com
    """)
    psi4.set_options({"scf_type": "pk", 'save_jk': True})
    e, wfn = psi4.energy("hf/sto-3g", molecule=h2o, return_wfn=True)
    A_ref, _ = build_UHF_AB_C1(wfn)
    ni, na, _, _ = A_ref['IAJB'].shape
    nia = ni * na
    A_sing_ref = A_ref['IAJB'] + A_ref['IAjb']
    A_sing_ref = A_sing_ref.reshape(nia, nia)
    A_trip_ref = A_ref['IAJB'] - A_ref['IAjb']
    A_trip_ref = A_trip_ref.reshape(nia, nia)
    sing_vals, _ = np.linalg.eigh(A_sing_ref)
    trip_vals, _ = np.linalg.eigh(A_trip_ref)

    trip_eng = TDRSCFEngine(wfn, ptype='tda', triplet=True)
    sing_eng = TDRSCFEngine(wfn, ptype='tda', triplet=False)
    ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)]
    psi4.core.print_out("\nA sing:\n" + str(A_sing_ref) + "\n\n")
    psi4.core.print_out("\nA trip:\n" + str(A_trip_ref) + "\n\n")
    A_trip_test = np.column_stack([x.to_array().flatten() for x in trip_eng.compute_products(ID)[0]])
    assert compare_arrays(A_trip_ref, A_trip_test, 8, "Triplet Ax C1 products")
    A_sing_test = np.column_stack([x.to_array().flatten() for x in sing_eng.compute_products(ID)[0]])
    assert compare_arrays(A_sing_ref, A_sing_test, 8, "Singlet Ax C1 products")

    sing_vals_2, _ = np.linalg.eigh(A_sing_test)
    trip_vals_2, _ = np.linalg.eigh(A_trip_test)

    psi4.core.print_out("\n\n SINGLET EIGENVALUES\n")
    for x, y in zip(sing_vals, sing_vals_2):
        psi4.core.print_out("{:10.6f}  {:10.6f}\n".format(x, y))
        # assert compare_values(x, y, 4, "Singlet ROOT")
    psi4.core.print_out("\n\n Triplet EIGENVALUES\n")
    for x, y in zip(trip_vals, trip_vals_2):
        psi4.core.print_out("{:10.6f}  {:10.6f}\n".format(x, y))
        # assert compare_values(x, y, 4, "Triplet Root")

    for x, y in zip(sing_vals, sing_vals_2):
        assert compare_values(x, y, 4, "Singlet ROOT")
    for x, y in zip(trip_vals, trip_vals_2):
        assert compare_values(x, y, 4, "Triplet Root")
Exemple #5
0
def test_restricted_TDA_singlet_df_c1():
    "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine"
    h2o = psi4.geometry("""
    O
    H 1 0.96
    H 1 0.96 2 104.5
    symmetry c1
    """)
    psi4.set_options({"scf_type": "df", 'save_jk': True})
    e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True)
    A_ref, _ = build_RHF_AB_C1_singlet_df(wfn)
    ni, na, _, _ = A_ref.shape
    nia = ni * na
    A_ref = A_ref.reshape((nia, nia))
    # Build engine
    eng = TDRSCFEngine(wfn, ptype='tda', triplet=False)
    # our "guess"" vectors
    ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)]
    A_test = np.column_stack([x.to_array().flatten() for x in eng.compute_products(ID)[0]])
    assert compare_arrays(A_ref, A_test, 8, "DF-RHF Ax C1 products")
def test_RU_TDA_C1():
    h2o = psi4.geometry("""0 1
    O          0.000000    0.000000    0.135446
    H         -0.000000    0.866812   -0.541782
    H         -0.000000   -0.866812   -0.541782
    symmetry c1
    no_reorient
    no_com
    """)
    psi4.set_options({"scf_type": "pk", 'save_jk': True})
    e, wfn = psi4.energy("hf/sto-3g", molecule=h2o, return_wfn=True)
    A_ref, _ = build_UHF_AB_C1(wfn)
    ni, na, _, _ = A_ref['IAJB'].shape
    nia = ni * na
    A_sing_ref = A_ref['IAJB'] + A_ref['IAjb']
    A_sing_ref = A_sing_ref.reshape(nia, nia)
    A_trip_ref = A_ref['IAJB'] - A_ref['IAjb']
    A_trip_ref = A_trip_ref.reshape(nia, nia)
    sing_vals, _ = np.linalg.eigh(A_sing_ref)
    trip_vals, _ = np.linalg.eigh(A_trip_ref)

    trip_eng = TDRSCFEngine(wfn, ptype='tda', triplet=True)
    sing_eng = TDRSCFEngine(wfn, ptype='tda', triplet=False)
    ID = [
        psi4.core.Matrix.from_array(v.reshape((ni, na)))
        for v in tuple(np.eye(nia).T)
    ]
    psi4.core.print_out("\nA sing:\n" + str(A_sing_ref) + "\n\n")
    psi4.core.print_out("\nA trip:\n" + str(A_trip_ref) + "\n\n")
    A_trip_test = np.column_stack(
        [x.to_array().flatten() for x in trip_eng.compute_products(ID)[0]])
    assert compare_arrays(A_trip_ref, A_trip_test, 8, "Triplet Ax C1 products")
    A_sing_test = np.column_stack(
        [x.to_array().flatten() for x in sing_eng.compute_products(ID)[0]])
    assert compare_arrays(A_sing_ref, A_sing_test, 8, "Singlet Ax C1 products")

    sing_vals_2, _ = np.linalg.eigh(A_sing_test)
    trip_vals_2, _ = np.linalg.eigh(A_trip_test)

    psi4.core.print_out("\n\n SINGLET EIGENVALUES\n")
    for x, y in zip(sing_vals, sing_vals_2):
        psi4.core.print_out("{:10.6f}  {:10.6f}\n".format(x, y))
        # assert compare_values(x, y, 4, "Singlet ROOT")
    psi4.core.print_out("\n\n Triplet EIGENVALUES\n")
    for x, y in zip(trip_vals, trip_vals_2):
        psi4.core.print_out("{:10.6f}  {:10.6f}\n".format(x, y))
        # assert compare_values(x, y, 4, "Triplet Root")

    for x, y in zip(sing_vals, sing_vals_2):
        assert compare_values(x, y, 4, "Singlet ROOT")
    for x, y in zip(trip_vals, trip_vals_2):
        assert compare_values(x, y, 4, "Triplet Root")
Exemple #7
0
def _solve_loop(wfn,
                ptype,
                solve_function,
                states_per_irrep: List[int],
                maxiter: int,
                restricted: bool = True,
                spin_mult: str = "singlet") -> List[_TDSCFResults]:
    """

    References
    ----------
    For the expression of the transition moments in length and velocity gauges:

    - T. B. Pedersen, A. E. Hansen, "Ab Initio Calculation and Display of the
    Rotary Strength Tensor in the Random Phase Approximation. Method and Model
    Studies." Chem. Phys. Lett., 246, 1 (1995)
    - P. J. Lestrange, F. Egidi, X. Li, "The Consequences of Improperly
    Describing Oscillator Strengths beyond the Electric Dipole Approximation."
    J. Chem. Phys., 143, 234103 (2015)
    """

    core.print_out("\n  ==> Requested Excitations <==\n\n")
    for nstate, state_sym in zip(states_per_irrep,
                                 wfn.molecule().irrep_labels()):
        core.print_out(
            f"      {nstate} {spin_mult} states with {state_sym} symmetry\n")

    # construct the engine
    if restricted:
        if spin_mult == "triplet":
            engine = TDRSCFEngine(wfn, ptype=ptype.lower(), triplet=True)
        else:
            engine = TDRSCFEngine(wfn, ptype=ptype.lower(), triplet=False)
    else:
        engine = TDUSCFEngine(wfn, ptype=ptype.lower())

    # collect results and compute some spectroscopic observables
    mints = core.MintsHelper(wfn.basisset())
    results = []
    irrep_GS = wfn.molecule().irrep_labels()[engine.G_gs]
    for state_sym, nstates in enumerate(states_per_irrep):
        if nstates == 0:
            continue
        irrep_ES = wfn.molecule().irrep_labels()[state_sym]
        core.print_out(
            f"\n\n  ==> Seeking the lowest {nstates} {spin_mult} states with {irrep_ES} symmetry"
        )
        engine.reset_for_state_symm(state_sym)
        guess_ = engine.generate_guess(nstates * 4)

        # ret = {"eigvals": ee, "eigvecs": (rvecs, rvecs), "stats": stats} (TDA)
        # ret = {"eigvals": ee, "eigvecs": (rvecs, lvecs), "stats": stats} (RPA)
        ret = solve_function(engine, nstates, guess_, maxiter)

        # check whether all roots converged
        if not ret["stats"][-1]["done"]:
            # raise error
            raise TDSCFConvergenceError(
                maxiter, wfn, f"singlet excitations in irrep {irrep_ES}",
                ret["stats"][-1])

        # flatten dictionary: helps with sorting by energy
        # also append state symmetry to return value
        for e, (R, L) in zip(ret["eigvals"], ret["eigvecs"]):
            irrep_trans = wfn.molecule().irrep_labels()[engine.G_gs
                                                        ^ state_sym]

            # length-gauge electric dipole transition moment
            edtm_length = engine.residue(R, mints.so_dipole())
            # length-gauge oscillator strength
            f_length = ((2 * e) / 3) * np.sum(edtm_length**2)
            # velocity-gauge electric dipole transition moment
            edtm_velocity = engine.residue(L, mints.so_nabla())
            ## velocity-gauge oscillator strength
            f_velocity = (2 / (3 * e)) * np.sum(edtm_velocity**2)
            # length gauge magnetic dipole transition moment
            # 1/2 is the Bohr magneton in atomic units
            mdtm = 0.5 * engine.residue(L, mints.so_angular_momentum())
            # NOTE The signs for rotatory strengths are opposite WRT the cited paper.
            # This is becasue Psi4 defines length-gauge dipole integral to include the electron charge (-1.0)
            # length gauge rotatory strength
            R_length = np.einsum("i,i", edtm_length, mdtm)
            # velocity gauge rotatory strength
            R_velocity = -np.einsum("i,i", edtm_velocity, mdtm) / e

            results.append(
                _TDSCFResults(e, irrep_GS, irrep_ES, irrep_trans, edtm_length,
                              f_length, edtm_velocity, f_velocity, mdtm,
                              R_length, R_velocity, spin_mult, R, L))

    return results
Exemple #8
0
def engines():
    return {
        'RHF-1': lambda w, p: TDRSCFEngine(w, ptype=p.lower(), triplet=False),
        'RHF-3': lambda w, p: TDRSCFEngine(w, ptype=p.lower(), triplet=True),
        'UHF': lambda w, p: TDUSCFEngine(w, ptype=p.lower())
    }
Exemple #9
0
def tdscf_excitations(wfn, **kwargs):
    """Compute excitations from a scf(HF/KS) wavefunction:

    Parameters
    -----------
    wfn : :py:class:`psi4.core.Wavefunction`
       The reference wavefunction
    states_per_irrep : list (int), {optional}
       The solver will find this many lowest excitations for each irreducible representation of the computational point group.
       The default is to find the lowest excitation of each symmetry. If this option is provided it must have the same number of elements
       as the number of irreducible representations as the computational point group.
    triplets : str {optional, ``none``, ``only``, ``also``}
       The default ``none`` will solve for no triplet states, ``only`` will solve for triplet states only, and ``also``
       will solve for the requested number of states of both triplet and singlet. This option is only valid for restricted references,
       and is ignored otherwise. The triplet and singlet solutions are found separately so using the ``also`` option will roughly double
       the computational cost of the calculation.
    tda :  bool {optional ``False``}
       If true the Tamm-Dancoff approximation (TDA) will be employed. For HF references this is equivalent to CIS.
    e_tol : float, {optional, 1.0e-6}
       The convergence threshold for the excitation energy
    r_tol : float, {optional, 1.0e-8}
       The convergence threshold for the norm of the residual vector
    max_ss_vectors: int {optional}
       The maximum number of ss vectors that will be stored before a collapse is done.
    guess : str
       If string the guess that will be used. Allowed choices:
       - ``denominators``: {default} uses orbital energy differences to generate guess vectors.


    ..note:: The algorithm employed to solve the non-Hermitian eigenvalue problem
             (when ``tda`` is False) will fail when the SCF wavefunction has a triplet instability.
    """
    # gather arguments
    e_tol = kwargs.pop('e_tol', 1.0e-6)
    r_tol = kwargs.pop('r_tol', 1.0e-8)
    max_ss_vec = kwargs.pop('max_ss_vectors', 50)
    verbose = kwargs.pop('print_lvl', 0)

    # how many states
    passed_spi = kwargs.pop('states_per_irrep',
                            [0 for _ in range(wfn.nirrep())])

    #TODO:states_per_irrep = _validate_states_args(wfn, passed_spi, nstates)
    states_per_irrep = passed_spi

    #TODO: guess types, user guess
    guess_type = kwargs.pop("guess", "denominators")
    if guess_type != "denominators":
        raise ValidationError("Guess type {} is not valid".format(guess_type))

    # which problem
    ptype = 'rpa'
    solve_function = solvers.hamiltonian_solver
    if kwargs.pop('tda', False):
        ptype = 'tda'
        solve_function = solvers.davidson_solver

    restricted = wfn.same_a_b_orbs()
    if restricted:
        triplet = kwargs.pop('triplet', False)
    else:
        triplet = None

    _print_tdscf_header(
        etol=e_tol,
        rtol=r_tol,
        states=[(count, label)
                for count, label in zip(states_per_irrep,
                                        wfn.molecule().irrep_labels())],
        guess_type=guess_type,
        restricted=restricted,
        triplet=triplet,
        ptype=ptype)

    # construct the engine
    if restricted:
        engine = TDRSCFEngine(wfn, triplet=triplet, ptype=ptype)
    else:
        engine = TDUSCFEngine(wfn, ptype=ptype)

    # just energies for now
    solver_results = []
    for state_sym, nstates in enumerate(states_per_irrep):
        if nstates == 0:
            continue
        engine.reset_for_state_symm(state_sym)
        guess_ = engine.generate_guess(nstates * 2)

        vecs_per_root = max_ss_vec // nstates

        # ret = (ee, rvecs, stats) (TDA)
        # ret = (ee, rvecs, lvecs, stats) (full TDSCF)
        ret = solve_function(engine=engine,
                             e_tol=e_tol,
                             r_tol=r_tol,
                             max_vecs_per_root=vecs_per_root,
                             nroot=nstates,
                             guess=guess_,
                             verbose=verbose)

        # store excitation energies tagged with final state symmetry (for printing)
        # TODO: handle R eigvecs (TDA) R/L eigvecs(full TDSCF): solver maybe should return dicts
        for ee in ret[0]:
            solver_results.append((ee, state_sym))

    # sort by energy symmetry is just meta data
    solver_results.sort(key=lambda x: x[0])

    # print excitation energies
    core.print_out("\n\nFinal Energetic Summary:\n")
    core.print_out("        " + (" " * 20) + " " +
                   "Excitation Energy".center(31) +
                   " {:^15}\n".format("Total Energy"))
    core.print_out("    {:^4} {:^20} {:^15} {:^15} {:^15}\n".format(
        "#", "Sym: GS->ES (Trans)", "[au]", "[eV]", "(au)"))
    core.print_out("    {:->4} {:->20} {:->15} {:->15} {:->15}\n".format(
        "-", "-", "-", "-", "-"))

    irrep_GS = wfn.molecule().irrep_labels()[engine.G_gs]
    for i, (E_ex_au, final_sym) in enumerate(solver_results):
        irrep_ES = wfn.molecule().irrep_labels()[final_sym]
        irrep_trans = wfn.molecule().irrep_labels()[engine.G_gs ^ final_sym]
        sym_descr = "{}->{} ({})".format(irrep_GS, irrep_ES, irrep_trans)

        #TODO: psivars/wfnvars

        E_ex_ev = constants.conversion_factor('hartree', 'eV') * E_ex_au

        E_tot_au = wfn.energy() + E_ex_au
        core.print_out(
            "    {:^4} {:^20} {:< 15.5f} {:< 15.5f} {:< 15.5f}\n".format(
                i + 1, sym_descr, E_ex_au, E_ex_ev, E_tot_au))

    core.print_out("\n")

    #TODO: output table

    #TODO: oscillator strengths

    #TODO: check/handle convergence failures

    return solver_results
Exemple #10
0
def tdscf_excitations(wfn, **kwargs):
    """Compute excitations from a scf(HF/KS) wavefunction:

    Parameters
    -----------
    wfn : :py:class:`psi4.core.Wavefunction`
       The reference wavefunction
    states_per_irrep : list (int), {optional}
       The solver will find this many lowest excitations for each irreducible representation of the computational point group.
       The default is to find the lowest excitation of each symmetry. If this option is provided it must have the same number of elements
       as the number of irreducible representations as the computational point group.
    triplets : str {optional, ``none``, ``only``, ``also``}
       The default ``none`` will solve for no triplet states, ``only`` will solve for triplet states only, and ``also``
       will solve for the requested number of states of both triplet and singlet. This option is only valid for restricted references,
       and is ignored otherwise. The triplet and singlet solutions are found separately so using the ``also`` option will roughly double
       the computational cost of the calculation.
    tda :  bool {optional ``False``}
       If true the Tamm-Dancoff approximation (TDA) will be employed. For HF references this is equivalent to CIS.
    e_tol : float, {optional, 1.0e-6}
       The convergence threshold for the excitation energy
    r_tol : float, {optional, 1.0e-8}
       The convergence threshold for the norm of the residual vector
    max_ss_vectors: int {optional}
       The maximum number of ss vectors that will be stored before a collapse is done.
    guess : str
       If string the guess that will be used. Allowed choices:
       - ``denominators``: {default} uses orbital energy differences to generate guess vectors.


    ..note:: The algorithm employed to solve the non-Hermitian eigenvalue problem
             (when ``tda`` is False) will fail when the SCF wavefunction has a triplet instability.
    """
    # gather arguments
    e_tol = kwargs.pop('e_tol', 1.0e-6)
    r_tol = kwargs.pop('r_tol', 1.0e-8)
    max_ss_vec = kwargs.pop('max_ss_vectors', 50)
    verbose = kwargs.pop('print_lvl', 0)

    # how many states
    passed_spi = kwargs.pop('states_per_irrep', [0 for _ in range(wfn.nirrep())])

    #TODO:states_per_irrep = _validate_states_args(wfn, passed_spi, nstates)
    states_per_irrep = passed_spi

    #TODO: guess types, user guess
    guess_type = kwargs.pop("guess", "denominators")
    if guess_type != "denominators":
        raise ValidationError("Guess type {} is not valid".format(guess_type))

    # which problem
    ptype = 'rpa'
    solve_function = solvers.hamiltonian_solver
    if kwargs.pop('tda', False):
        ptype = 'tda'
        solve_function = solvers.davidson_solver

    restricted = wfn.same_a_b_orbs()
    if restricted:
        triplet = kwargs.pop('triplet', False)
    else:
        triplet = None

    _print_tdscf_header(
        etol=e_tol,
        rtol=r_tol,
        states=[(count, label) for count, label in zip(states_per_irrep,
                                                       wfn.molecule().irrep_labels())],
        guess_type=guess_type,
        restricted=restricted,
        triplet=triplet,
        ptype=ptype)

    # construct the engine
    if restricted:
        engine = TDRSCFEngine(wfn, triplet=triplet, ptype=ptype)
    else:
        engine = TDUSCFEngine(wfn, ptype=ptype)

    # just energies for now
    solver_results = []
    for state_sym, nstates in enumerate(states_per_irrep):
        if nstates == 0:
            continue
        engine.reset_for_state_symm(state_sym)
        guess_ = engine.generate_guess(nstates * 2)

        vecs_per_root = max_ss_vec // nstates

        # ret = (ee, rvecs, stats) (TDA)
        # ret = (ee, rvecs, lvecs, stats) (full TDSCF)
        ret = solve_function(
            engine=engine,
            e_tol=e_tol,
            r_tol=r_tol,
            max_vecs_per_root=vecs_per_root,
            nroot=nstates,
            guess=guess_,
            verbose=verbose)

        # store excitation energies tagged with final state symmetry (for printing)
        # TODO: handle R eigvecs (TDA) R/L eigvecs(full TDSCF): solver maybe should return dicts
        for ee in ret[0]:
            solver_results.append((ee, state_sym))

    # sort by energy symmetry is just meta data
    solver_results.sort(key=lambda x: x[0])

    # print excitation energies
    core.print_out("\n\nFinal Energetic Summary:\n")
    core.print_out("        " + (" " * 20) + " " + "Excitation Energy".center(31) + " {:^15}\n".format("Total Energy"))
    core.print_out("    {:^4} {:^20} {:^15} {:^15} {:^15}\n".format("#", "Sym: GS->ES (Trans)", "[au]", "[eV]",
                                                                    "(au)"))
    core.print_out("    {:->4} {:->20} {:->15} {:->15} {:->15}\n".format("-", "-", "-", "-", "-"))

    irrep_GS = wfn.molecule().irrep_labels()[engine.G_gs]
    for i, (E_ex_au, final_sym) in enumerate(solver_results):
        irrep_ES = wfn.molecule().irrep_labels()[final_sym]
        irrep_trans = wfn.molecule().irrep_labels()[engine.G_gs ^ final_sym]
        sym_descr = "{}->{} ({})".format(irrep_GS, irrep_ES, irrep_trans)

        #TODO: psivars/wfnvars

        E_ex_ev = constants.conversion_factor('hartree', 'eV') * E_ex_au

        E_tot_au = wfn.energy() + E_ex_au
        core.print_out("    {:^4} {:^20} {:< 15.5f} {:< 15.5f} {:< 15.5f}\n".format(
            i + 1, sym_descr, E_ex_au, E_ex_ev, E_tot_au))

    core.print_out("\n")

    #TODO: output table

    #TODO: oscillator strengths

    #TODO: check/handle convergence failures

    return solver_results