From f3a7aad7bca36387be636fbb1e22267301199218 Mon Sep 17 00:00:00 2001 From: Maximilian Scheurer Date: Thu, 13 Jan 2022 12:54:11 +0100 Subject: [PATCH] Make pandas and mpl dependencies for optional features (#141) * make pandas and mpl deps for optional features * fix broken examples * add docs * improve docs * fix and amend docs --- adcc/ElectronicTransition.py | 5 +- adcc/ExcitedStates.py | 5 +- adcc/LazyMp.py | 2 +- adcc/__init__.py | 8 ++- adcc/backends/__init__.py | 32 +----------- adcc/misc.py | 50 +++++++++++++++++++ adcc/visualisation/Spectrum.py | 4 +- docs/calculations.rst | 12 +++-- docs/conf.py | 3 ++ docs/installation.rst | 25 ++++++++++ .../hydrogen_fluoride/psi4_631g_sf_adc2.py | 2 +- .../hydrogen_fluoride/pyscf_631g_sf_adc2.py | 1 + .../pyscf_631g_sf_adc2_dissociation.py | 2 +- examples/neon/test_cg_alpha.py | 13 ++--- examples/water/pyscf_ccpvdz_fv_adc3.py | 9 ---- examples/water/pyscf_sto3g_adc2.py | 6 --- examples/water/pyscf_sto3g_uhf_adc2.py | 9 ---- examples/water/tpa.py | 6 +-- setup.py | 5 +- 19 files changed, 113 insertions(+), 86 deletions(-) diff --git a/adcc/ElectronicTransition.py b/adcc/ElectronicTransition.py index b9d5cb13..c2f7e4a0 100644 --- a/adcc/ElectronicTransition.py +++ b/adcc/ElectronicTransition.py @@ -23,14 +23,13 @@ import warnings import numpy as np -from .misc import cached_property +from .misc import cached_property, requires_module from .timings import Timer, timed_member_call from .visualisation import ExcitationSpectrum from .OneParticleOperator import product_trace from .AdcMethod import AdcMethod from scipy import constants -from matplotlib import pyplot as plt from .Excitation import mark_excitation_property from .solver.SolverStateBase import EigenSolverStateBase @@ -237,6 +236,7 @@ def cross_section(self): prefac = 2.0 * np.pi ** 2 / fine_structure_au return prefac * self.oscillator_strength + @requires_module("matplotlib") def plot_spectrum(self, broadening="lorentzian", xaxis="eV", yaxis="cross_section", width=0.01, **kwargs): """One-shot plotting function for the spectrum generated by all states @@ -265,6 +265,7 @@ def plot_spectrum(self, broadening="lorentzian", xaxis="eV", gamma parameter. The value should be given in atomic units and will be converted to the unit of the energy axis. """ + from matplotlib import pyplot as plt if xaxis == "eV": eV = constants.value("Hartree energy in eV") energies = self.excitation_energy * eV diff --git a/adcc/ExcitedStates.py b/adcc/ExcitedStates.py index 0e5e45d5..4614da46 100644 --- a/adcc/ExcitedStates.py +++ b/adcc/ExcitedStates.py @@ -22,13 +22,12 @@ ## --------------------------------------------------------------------- import warnings import numpy as np -import pandas as pd from adcc import dot from scipy import constants from . import adc_pp -from .misc import cached_property +from .misc import cached_property, requires_module from .timings import timed_member_call from .Excitation import Excitation, mark_excitation_property from .FormatIndex import (FormatIndexAdcc, FormatIndexBase, @@ -439,11 +438,13 @@ def describe_amplitudes(self, tolerance=0.01, index_format=None): ret += separator return ret[:-1] + @requires_module("pandas") def to_dataframe(self): """ Exports the ExcitedStates object as :class:`pandas.DataFrame`. Atomic units are used for all values. """ + import pandas as pd propkeys = self.excitation_property_keys propkeys.extend([k.name for k in self._excitation_energy_corrections]) data = { diff --git a/adcc/LazyMp.py b/adcc/LazyMp.py index ae8d4f9c..70e4eb43 100644 --- a/adcc/LazyMp.py +++ b/adcc/LazyMp.py @@ -36,7 +36,7 @@ class LazyMp: def __init__(self, hf): """ - Initialise the class dealing with the M/oller-Plesset ground state. + Initialise the class dealing with the Møller-Plesset ground state. """ if isinstance(hf, libadcc.HartreeFockSolution_i): hf = ReferenceState(hf) diff --git a/adcc/__init__.py b/adcc/__init__.py index 6b74935a..d3ea8257 100644 --- a/adcc/__init__.py +++ b/adcc/__init__.py @@ -28,6 +28,7 @@ from .LazyMp import LazyMp from .Tensor import Tensor from .Symmetry import Symmetry +from .MoSpaces import MoSpaces from .AdcMatrix import AdcBlockView, AdcMatrix from .AdcMethod import AdcMethod from .functions import (contract, copy, direct_sum, dot, einsum, empty_like, @@ -36,6 +37,8 @@ from .memory_pool import memory_pool from .State2States import State2States from .ExcitedStates import ExcitedStates +from .Excitation import Excitation +from .ElectronicTransition import ElectronicTransition from .DataHfProvider import DataHfProvider, DictHfProvider from .ReferenceState import ReferenceState from .AmplitudeVector import AmplitudeVector @@ -49,13 +52,14 @@ from .exceptions import InputError __all__ = ["run_adc", "InputError", "AdcMatrix", "AdcBlockView", - "AdcMethod", "Symmetry", "ReferenceState", + "AdcMethod", "Symmetry", "ReferenceState", "MoSpaces", "einsum", "contract", "copy", "dot", "empty_like", "evaluate", "lincomb", "nosym_like", "ones_like", "transpose", "linear_combination", "zeros_like", "direct_sum", "memory_pool", "set_n_threads", "get_n_threads", "AmplitudeVector", "HartreeFockProvider", "ExcitedStates", "State2States", - "Tensor", "DictHfProvider", "DataHfProvider", "OneParticleOperator", + "Excitation", "ElectronicTransition", "Tensor", "DictHfProvider", + "DataHfProvider", "OneParticleOperator", "guesses_singlet", "guesses_triplet", "guesses_any", "guess_symmetries", "guesses_spin_flip", "guess_zero", "LazyMp", "adc0", "cis", "adc1", "adc2", "adc2x", "adc3", diff --git a/adcc/backends/__init__.py b/adcc/backends/__init__.py index 025e8ae8..8abc3424 100644 --- a/adcc/backends/__init__.py +++ b/adcc/backends/__init__.py @@ -24,41 +24,11 @@ import h5py import warnings -from pkg_resources import parse_version +from ..misc import is_module_available __all__ = ["import_scf_results", "run_hf", "have_backend", "available"] -def is_module_available(module, min_version=None): - """Check using importlib if a module is available.""" - import importlib - - try: - mod = importlib.import_module(module) - except ImportError: - return False - - if not min_version: # No version check - return True - - if not hasattr(mod, "__version__"): - warnings.warn( - "Could not check host program {} minimal version, " - "since __version__ tag not found. Proceeding anyway." - "".format(module) - ) - return True - - if parse_version(mod.__version__) < parse_version(min_version): - warnings.warn( - "Found host program module {}, but its version {} is below " - "the least required (== {}). This host program will be ignored." - "".format(module, mod.__version__, min_version) - ) - return False - return True - - # Cache for the list of available backends ... cannot be filled right now, # since this can lead to import loops when adcc is e.g. used from Psi4 __status = dict() diff --git a/adcc/misc.py b/adcc/misc.py index b73f22c8..353e0bc7 100644 --- a/adcc/misc.py +++ b/adcc/misc.py @@ -20,8 +20,10 @@ ## along with adcc. If not, see . ## ## --------------------------------------------------------------------- +import warnings import numpy as np from functools import wraps +from pkg_resources import parse_version def cached_property(f): @@ -115,6 +117,54 @@ def caller(self, fctn=fctn, args=args): return inner_decorator +def is_module_available(module, min_version=None): + """Check using importlib if a module is available.""" + import importlib + + try: + mod = importlib.import_module(module) + except ImportError: + return False + + if not min_version: # No version check + return True + + if not hasattr(mod, "__version__"): + warnings.warn( + f"Could not check module {module} minimal version, " + "since __version__ tag not found. Proceeding anyway." + ) + return True + + if parse_version(mod.__version__) < parse_version(min_version): + warnings.warn( + f"Found module {module}, but its version {mod.__version__} is below " + f"the least required (== {min_version}). This module will be ignored." + ) + return False + return True + + +def requires_module(name, min_version=None): + """ + Decorator to check if the module 'name' is available, + throw ModuleNotFoundError on call if not. + """ + def inner(function): + def wrapper(*args, **kwargs): + fname = function.__name__ + if not is_module_available(name, min_version): + raise ModuleNotFoundError( + f"Function '{fname}' needs module {name}, but it was " + f"not found. Solve by running 'pip install {name}' or " + f"'conda install {name}' on your system." + ) + return function(*args, **kwargs) + wrapper.__doc__ = function.__doc__ + return wrapper + return inner + + def assert_allclose_signfix(actual, desired, atol=0, **kwargs): """ Call assert_allclose, but beforehand normalise the sign diff --git a/adcc/visualisation/Spectrum.py b/adcc/visualisation/Spectrum.py index 84855e1a..70d08016 100644 --- a/adcc/visualisation/Spectrum.py +++ b/adcc/visualisation/Spectrum.py @@ -23,7 +23,7 @@ import numpy as np from . import shapefctns -from matplotlib import pyplot as plt +from ..misc import requires_module class Spectrum: @@ -127,6 +127,7 @@ def copy(self): cpy.ylabel = self.ylabel return cpy + @requires_module("matplotlib") def plot(self, *args, style=None, **kwargs): """Plot the Spectrum represented by this class. @@ -139,6 +140,7 @@ def plot(self, *args, style=None, **kwargs): types of spectra commonly plotted. Valid are "discrete" and "continuous". By default no special style is chosen. """ + from matplotlib import pyplot as plt if style == "discrete": p = plt.plot(self.x, self.y, "x", *args, **kwargs) plt.vlines(self.x, 0, self.y, linestyle="dashed", diff --git a/docs/calculations.rst b/docs/calculations.rst index 478e3a56..80ebfc3f 100644 --- a/docs/calculations.rst +++ b/docs/calculations.rst @@ -324,6 +324,10 @@ obtained using the function ``adcc.get_n_threads()``. Plotting spectra ---------------- +.. note:: + For plotting spectra, `Matplotlib `_ + needs to be installed. See :ref:`optional-dependencies` for details. + Having computed a set of ADC excited states as discussed in the previous sections, these can be visualised in a simulated absorption spectrum @@ -354,7 +358,7 @@ as shown in the next example. state.plot_spectrum() plt.show() -This code uses the :func:`adcc.ExcitedStates.plot_spectrum` +This code uses the :func:`adcc.ElectronicTransition.plot_spectrum` function and the `Matplotlib `_ package to produce a plot such as @@ -364,13 +368,13 @@ In this image crosses represent the actual computed value for the absorption cross section for the obtained excited states. To form the actual spectrum (solid blue line) these discrete peaks are artificially broadened with an empirical broadening parameter. -Notice, that the :func:`adcc.ExcitedStates.plot_spectrum` +Notice, that the :func:`adcc.ElectronicTransition.plot_spectrum` function does only prepare the spectrum inside Matplotlib, such that ``plt.show()`` needs to be called in order to actuall *see* the plot. This allows to *simulaneously* plot the spectrum from multiple calculations in one figure if desired. -The :func:`adcc.ExcitedStates.plot_spectrum` function takes a number +The :func:`adcc.ElectronicTransition.plot_spectrum` function takes a number of parameters to alter the default plotting behaviour: - **Broadening parameters**: The default broadening can be completely disabled @@ -401,7 +405,7 @@ of parameters to alter the default plotting behaviour: See the `Matplotlib documentation `_ for details. In the same manner, one can model the ECD spectrum of chiral molecules -with the :func:`adcc.ExcitedStates.plot_spectrum` function. An example +with the :func:`adcc.ElectronicTransition.plot_spectrum` function. An example script for obtaining the ECD spectrum of (R)- and (S)-2-methyloxirane with ADC(2) can be found in the `examples folder `_. The only difference to plotting a UV/Vis spectrum as shown above is to specify diff --git a/docs/conf.py b/docs/conf.py index f16145ef..70cfb16e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -222,6 +222,9 @@ def determine_tag(): napoleon_numpy_docstring = True napoleon_include_init_with_doc = True +# __init__ docstring appears in documentation +autoclass_content = "init" + # Breathe settings libadcc_doc_dir = ( os.path.abspath(os.path.dirname(__file__)) diff --git a/docs/installation.rst b/docs/installation.rst index 4d60229d..3333e65e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -18,6 +18,9 @@ Please get in touch by `opening an issue `_ if you cannot get adcc to work. +Some specialty features of adcc require extra dependencies (e.g., Pandas and +Matplotlib), see :ref:`optional-dependencies` for details. + Installing adcc --------------- @@ -154,6 +157,28 @@ the :ref:`devnotes` will provide you with some useful pointers to get started. +.. _optional-dependencies: + +Optional dependencies for analysis features +------------------------------------------- + +- **Matplotlib**: Plotting spectra with :func:`adcc.ElectronicTransition.plot_spectrum` + + +- **Pandas**: Export to `pandas.DataFrame` via :func:`adcc.ExcitedStates.to_dataframe` + + +Installation of optional packages +................................. + +- Using pip: Either install optional dependencies directly with adcc via + ``pip install adcc[analysis]`` or run manually for each package, e.g., ``pip install matplotlib`` + +- Using conda: Install each package manually, e.g., ``conda install matplotlib``. + +Note that all other core features of adcc still work without +these packages installed. + .. _troubleshooting: diff --git a/examples/hydrogen_fluoride/psi4_631g_sf_adc2.py b/examples/hydrogen_fluoride/psi4_631g_sf_adc2.py index 61aa02de..8ed7e367 100755 --- a/examples/hydrogen_fluoride/psi4_631g_sf_adc2.py +++ b/examples/hydrogen_fluoride/psi4_631g_sf_adc2.py @@ -14,7 +14,7 @@ no_com """) -psi4.set_num_threads(adcc.thread_pool.n_cores) +psi4.set_num_threads(adcc.get_n_threads()) psi4.core.be_quiet() psi4.set_options({'basis': "6-31g", 'e_convergence': 1e-14, diff --git a/examples/hydrogen_fluoride/pyscf_631g_sf_adc2.py b/examples/hydrogen_fluoride/pyscf_631g_sf_adc2.py index 64629754..4ede8463 100755 --- a/examples/hydrogen_fluoride/pyscf_631g_sf_adc2.py +++ b/examples/hydrogen_fluoride/pyscf_631g_sf_adc2.py @@ -14,6 +14,7 @@ scfres = scf.UHF(mol) scfres.conv_tol = 1e-14 scfres.conv_tol_grad = 1e-10 +scfres.max_cycle = 500 scfres.kernel() # Run solver and print results diff --git a/examples/hydrogen_fluoride/pyscf_631g_sf_adc2_dissociation.py b/examples/hydrogen_fluoride/pyscf_631g_sf_adc2_dissociation.py index f27fc049..831c7281 100755 --- a/examples/hydrogen_fluoride/pyscf_631g_sf_adc2_dissociation.py +++ b/examples/hydrogen_fluoride/pyscf_631g_sf_adc2_dissociation.py @@ -26,7 +26,7 @@ def run_spin_flip(distance): states = adcc.adc2(scfres, n_spin_flip=1) ene = scfres.energy_tot() + states.ground_state.energy_correction(2) - return ene + states.eigenvalues[0] + return ene + states.excitation_energy[0] def run_progression(outfile="631g_adc2_dissociation.nptxt"): diff --git a/examples/neon/test_cg_alpha.py b/examples/neon/test_cg_alpha.py index 1910ff20..bd448d9e 100644 --- a/examples/neon/test_cg_alpha.py +++ b/examples/neon/test_cg_alpha.py @@ -5,20 +5,16 @@ from pyscf import gto, scf from adcc.solver.preconditioner import JacobiPreconditioner -from adcc.AmplitudeVector import AmplitudeVector from adcc.solver import IndexSymmetrisation from adcc.solver.conjugate_gradient import conjugate_gradient, default_print -from adcc.modified_transition_moments import compute_modified_transition_moments +from adcc.adc_pp.modified_transition_moments import modified_transition_moments class ShiftedMat(adcc.AdcMatrix): def __init__(self, method, mp_results, omega=0.0): self.omega = omega super().__init__(method, mp_results) - diagonal = AmplitudeVector(*tuple( - self.diagonal(block) for block in self.blocks - )) - self.omegamat = adcc.ones_like(diagonal) * omega + self.omegamat = adcc.ones_like(self.diagonal()) * omega def __matmul__(self, other): return super().__matmul__(other) - self.omegamat * other @@ -39,9 +35,8 @@ def __matmul__(self, other): refstate = adcc.ReferenceState(scfres) matrix = ShiftedMat("adc3", refstate, omega=0.0) -rhs = compute_modified_transition_moments( - matrix, refstate.operators.electric_dipole[0], "adc2" -) +rhs = modified_transition_moments("adc2", matrix.ground_state, + refstate.operators.electric_dipole[0]) preconditioner = JacobiPreconditioner(matrix) freq = 0.0 preconditioner.update_shifts(freq) diff --git a/examples/water/pyscf_ccpvdz_fv_adc3.py b/examples/water/pyscf_ccpvdz_fv_adc3.py index 41cdc86b..79ddd11c 100755 --- a/examples/water/pyscf_ccpvdz_fv_adc3.py +++ b/examples/water/pyscf_ccpvdz_fv_adc3.py @@ -16,15 +16,6 @@ scfres.conv_tol = 1e-13 scfres.kernel() -# -# Some more advanced memory tampering options -# -# Initialise ADC memory (512 MiB) -# Use a tensor block size parameter of 16 and -# a specific allocator (in this case std::allocator) -adcc.memory_pool.initialise(max_memory=512 * 1024 * 1024, - tensor_block_size=12, allocator="standard") - # Run an adc3 calculation: singlets = adcc.adc3(scfres, frozen_virtual=3, n_singlets=3) triplets = adcc.adc3(singlets.matrix, n_triplets=3) diff --git a/examples/water/pyscf_sto3g_adc2.py b/examples/water/pyscf_sto3g_adc2.py index 74045d7b..e5ab8bb2 100755 --- a/examples/water/pyscf_sto3g_adc2.py +++ b/examples/water/pyscf_sto3g_adc2.py @@ -17,12 +17,6 @@ scfres.conv_tol_grad = 1e-10 scfres.kernel() -# Explicitly initialise ADC virtual memory pool to (256 MiB) -# (if this call is missing than only RAM is used. This allows -# to dump data to disk as well such that maximally up to the -# specified amount of data (in bytes) will reside in RAM) -adcc.memory_pool.initialise(max_memory=256 * 1024 * 1024) - # Run an adc2 calculation: singlets = adcc.adc2(scfres, n_singlets=5) triplets = adcc.adc2(singlets.matrix, n_triplets=3) diff --git a/examples/water/pyscf_sto3g_uhf_adc2.py b/examples/water/pyscf_sto3g_uhf_adc2.py index 4bd69312..1c7c44f6 100755 --- a/examples/water/pyscf_sto3g_uhf_adc2.py +++ b/examples/water/pyscf_sto3g_uhf_adc2.py @@ -20,16 +20,7 @@ scfres.verbose = 4 scfres.kernel() -# Explicitly initialise ADC virtual memory pool to (256 MiB) -# (if this call is missing than only RAM is used. This allows -# to dump data to disk as well such that maximally up to the -# specified amount of data (in bytes) will reside in RAM) -adcc.memory_pool.initialise(max_memory=256 * 1024 * 1024) - # Run an adc2 calculation: singlets = adcc.adc2(scfres, n_states=5) -# triplets = adcc.adc2(singlets.matrix, n_triplets=3) print(singlets.describe()) -print() -# print(triplets.describe()) diff --git a/examples/water/tpa.py b/examples/water/tpa.py index bf2a975b..842c2489 100644 --- a/examples/water/tpa.py +++ b/examples/water/tpa.py @@ -8,7 +8,6 @@ from pyscf import gto, scf from adcc.solver.preconditioner import JacobiPreconditioner -from adcc.AmplitudeVector import AmplitudeVector from adcc.solver import IndexSymmetrisation from adcc.solver.conjugate_gradient import conjugate_gradient, default_print from adcc.adc_pp.modified_transition_moments import modified_transition_moments @@ -20,10 +19,7 @@ class ShiftedMat(adcc.AdcMatrix): def __init__(self, method, mp_results, omega=0.0): self.omega = omega super().__init__(method, mp_results) - diagonal = AmplitudeVector(*tuple( - self.diagonal(block) for block in self.blocks - )) - self.omegamat = adcc.ones_like(diagonal) * omega + self.omegamat = adcc.ones_like(self.diagonal()) * omega def __matmul__(self, other): return super().__matmul__(other) - self.omegamat * other diff --git a/setup.py b/setup.py index 74b8d5c8..0e3600ee 100755 --- a/setup.py +++ b/setup.py @@ -534,15 +534,14 @@ def read_readme(): "opt_einsum >= 3.0", "numpy >= 1.14", "scipy >= 1.2", - "matplotlib >= 3.0", "h5py >= 2.9", "tqdm >= 4.30", - "pandas >= 0.25.0", ], - tests_require=["pytest", "pytest-cov", "pyyaml"], + tests_require=["pytest", "pytest-cov", "pyyaml", "pandas >= 0.25.0"], extras_require={ "build_docs": ["sphinx>=2", "breathe", "sphinxcontrib-bibtex", "sphinx-automodapi", "sphinx-rtd-theme"], + "analysis": ["matplotlib >= 3.0", "pandas >= 0.25.0"], }, # cmdclass={"build_ext": build_ext, "pytest": PyTest,