diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 000000000..0d6284bbf --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,50 @@ +name: Deploy documentation with GitHub Pages dependencies preinstalled + +on: + push: + branches: ["develop"] + workflow_dispatch: # enable manual workflow execution + +# Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + # prevent this action from running on forks + if: github.repository == 'abinit/abipy' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..0f8620287 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,81 @@ +# Runs the complete test suite. +name: Tests + +on: + push: + branches: [develop] + paths-ignore: ["**/*.md", docs/**] + pull_request: + branches: [develop] + paths-ignore: ["**/*.md", docs/**] + workflow_dispatch: + workflow_call: # make this workflow reusable by release.yml + +permissions: + contents: read + +jobs: + test: + # prevent this action from running on forks + if: github.repository == 'abinit/abipy' + defaults: + run: + shell: bash -l {0} # enables conda/mamba env activation by reading bash profile + strategy: + fail-fast: false + matrix: + # maximize CI coverage of different platforms and python versions while minimizing the + # total number of jobs. We run all pytest splits with the oldest supported python + # version (currently 3.9) on windows (seems most likely to surface errors) and with + # newest version (currently 3.12) on ubuntu (to get complete coverage on unix). + config: + - os: windows-latest + python: "3.9" + resolution: highest + extras: ci,optional + - os: ubuntu-latest + python: '>3.9' + resolution: lowest-direct + extras: ci,optional + - os: macos-latest + python: '3.10' + resolution: lowest-direct + extras: ci # test with only required dependencies installed + + # pytest-split automatically distributes work load so parallel jobs finish in similar time + # update durations file with `pytest --store-durations --durations-path tests/files/.pytest-split-durations` + split: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + runs-on: ${{ matrix.config.os }} + + #env: + # PMG_MAPI_KEY: ${{ secrets.PMG_MAPI_KEY }} + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Set up micromamba + uses: mamba-org/setup-micromamba@main + + - name: Create mamba environment + run: | + micromamba create -n abipy python=${{ matrix.config.python }} --yes + + - name: Install uv + run: micromamba run -n abipy pip install uv + + #- name: Install ubuntu-only conda dependencies + # if: matrix.config.os == 'ubuntu-latest' + # run: | + # micromamba install -n abipy -c conda-forge enumlib packmol bader openbabel openff-toolkit --yes + + - name: Install pymatgen and dependencies + run: | + micromamba activate abipy + uv pip install --editable '.[${{ matrix.config.extras }}]' --resolution=${{ matrix.config.resolution }} + + - name: pytest split ${{ matrix.split }} + run: | + micromamba activate abipy + pytest --splits 10 --group ${{ matrix.split }} --durations-path tests/files/.pytest-split-durations tests diff --git a/abipy/eph/varpeq.py b/abipy/eph/varpeq.py index 945533772..628d79934 100644 --- a/abipy/eph/varpeq.py +++ b/abipy/eph/varpeq.py @@ -159,9 +159,6 @@ def to_string(self, verbose=0) -> str: app(self.structure.to_string(verbose=verbose, title="Structure")) app("") app(self.ebands.to_string(with_structure=False, verbose=verbose, title="Electronic Bands")) - #if verbose > 1: - # app("") - # app(self.hdr.to_string(verbose=verbose, title="Abinit Header")) app("") app("VARPEQ parameters:") diff --git a/abipy/ppcodes/oncv_parser.py b/abipy/ppcodes/oncv_parser.py index ca906dd7c..218a8cc22 100644 --- a/abipy/ppcodes/oncv_parser.py +++ b/abipy/ppcodes/oncv_parser.py @@ -531,7 +531,7 @@ def kin_densities(self) -> dict[str, RadialFunction]: @lazy_property def vtaus(self) -> dict[str, RadialFunction]: """ - Dictionary with Vtau pototentials on the radial mesh. + Dictionary with Vtau ptotentials on the radial mesh. """ if not self.is_metapsp: raise ValueEror("kin_densities are only available in pseudos generated with metapsp") diff --git a/abipy/ppcodes/oncv_plotter.py b/abipy/ppcodes/oncv_plotter.py index fb1bcb85f..609559323 100644 --- a/abipy/ppcodes/oncv_plotter.py +++ b/abipy/ppcodes/oncv_plotter.py @@ -327,11 +327,12 @@ def plotly_vtau(self, *args, **kwargs): from plotly.tools import mpl_to_plotly return mpl_to_plotly(self.plot_vtau(*args, show=False, **kwargs)) - def plot_vtau(self, ax=None, fontsize: int = 8, **kwargs) -> Figure: + def plot_vtau(self, xscale="log", ax=None, fontsize: int = 8, **kwargs) -> Figure: """ Plot v_tau and v_tau(model+pseudo) potentials on axis ax. Args: + xscale: "log" to plot vtau in log scale or "linear". For other options see matplotlib. ax: |matplotlib-Axes| or None if a new figure should be created. """ ax, fig, plt = get_ax_fig_plt(ax) @@ -347,23 +348,26 @@ def plot_vtau(self, ax=None, fontsize: int = 8, **kwargs) -> Figure: fontsize=fontsize, ) self._add_rc_vlines_ax(ax, with_lloc=True) + ax.set_xscale(xscale) + + return fig def plotly_tau(self, *args, **kwargs): """Generate plotly figure from matplotly.""" from plotly.tools import mpl_to_plotly return mpl_to_plotly(self.plot_tau(*args, show=False, **kwargs)) - def plot_tau(self, ax=None, fontsize: int = 8, **kwargs) -> Figure: + def plot_tau(self, ax=None, yscale="log", fontsize: int = 8, **kwargs) -> Figure: """ Plot kinetic energy densities tauPS and tau(M+PS) on axis ax. Args: + yscale: "log" to plot tau in log scale or "linear". For other options see matplotlib. ax: |matplotlib-Axes| or None if a new figure should be created. """ ax, fig, plt = get_ax_fig_plt(ax) for key, den in self.parser.kin_densities.items(): - #mode = "ae" if key == "tau_ps" else "ps" ax.plot(den.rmesh, den.values, label=den.name) #**self._mpl_opts_laeps(0, mode)) @@ -373,6 +377,7 @@ def plot_tau(self, ax=None, fontsize: int = 8, **kwargs) -> Figure: fontsize=fontsize, ) self._add_rc_vlines_ax(ax, with_lloc=True) + ax.set_yscale(yscale) return fig diff --git a/abipy/ppcodes/tests/test_oncvpsp.py b/abipy/ppcodes/tests/test_oncvpsp.py index b93d4ac85..ddb0992a7 100644 --- a/abipy/ppcodes/tests/test_oncvpsp.py +++ b/abipy/ppcodes/tests/test_oncvpsp.py @@ -474,6 +474,10 @@ def _call_plotter_methods(self, plotter): assert plotter.plot_atanlogder_econv(show=False) assert plotter.plot_den_formfact(ecut=20, show=False) + if isinstance(plotter, OncvParser) and plotter.parser.is_metapsp: + assert plotter.plot_vtau(show=False) + assert plotter.plot_tau(show=False) + #if self.has_plotly(): def test_psp8_get_densities(self): diff --git a/abipy/tools/plotting.py b/abipy/tools/plotting.py index 6ef678248..dd7be2b04 100644 --- a/abipy/tools/plotting.py +++ b/abipy/tools/plotting.py @@ -19,12 +19,12 @@ from collections import namedtuple, OrderedDict from typing import Any, Callable, Iterator from monty.string import list_strings -from pymatgen.util.plotting import add_fig_kwargs +#from pymatgen.util.plotting import add_fig_kwargs from abipy.tools import duck from abipy.tools.iotools import dataframe_from_filepath from abipy.tools.typing import Figure, Axes, VectorLike from abipy.tools.numtools import data_from_cplx_mode -from plotly.tools import mpl_to_plotly + __all__ = [ "set_axlims", @@ -66,6 +66,115 @@ ) + +def add_fig_kwargs(func): + """Decorator that adds keyword arguments for functions returning matplotlib + figures. + + The function should return either a matplotlib figure or None to signal + some sort of error/unexpected event. + See doc string below for the list of supported options. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # pop the kwds used by the decorator. + title = kwargs.pop("title", None) + size_kwargs = kwargs.pop("size_kwargs", None) + show = kwargs.pop("show", True) + savefig = kwargs.pop("savefig", None) + tight_layout = kwargs.pop("tight_layout", False) + ax_grid = kwargs.pop("ax_grid", None) + ax_annotate = kwargs.pop("ax_annotate", None) + fig_close = kwargs.pop("fig_close", False) + plotly = kwargs.pop("fig_close", False) + + # Call func and return immediately if None is returned. + fig = func(*args, **kwargs) + if fig is None: + return fig + + # Operate on matplotlib figure. + if title is not None: + fig.suptitle(title) + + if size_kwargs is not None: + fig.set_size_inches(size_kwargs.pop("w"), size_kwargs.pop("h"), **size_kwargs) + + if ax_grid is not None: + for ax in fig.axes: + ax.grid(bool(ax_grid)) + + if ax_annotate: + tags = ascii_letters + if len(fig.axes) > len(tags): + tags = (1 + len(ascii_letters) // len(fig.axes)) * ascii_letters + for ax, tag in zip(fig.axes, tags): + ax.annotate(f"({tag})", xy=(0.05, 0.95), xycoords="axes fraction") + + if tight_layout: + try: + fig.tight_layout() + except Exception as exc: + # For some unknown reason, this problem shows up only on travis. + # https://stackoverflow.com/questions/22708888/valueerror-when-using-matplotlib-tight-layout + print("Ignoring Exception raised by fig.tight_layout\n", str(exc)) + + if savefig: + fig.savefig(savefig) + + if plotly: + try: + plotly_fig = mpl_to_ply(fig, latex=False) + if show: plotly_fig.show() + return plotly_fig + except Exception as exc: + raise + #print(str(exc)) + pass + + import matplotlib.pyplot as plt + if show: + plt.show() + + if fig_close: + plt.close(fig=fig) + + return fig + + # Add docstring to the decorated method. + doc_str = """\n\n + Keyword arguments controlling the display of the figure: + + ================ ==================================================== + kwargs Meaning + ================ ==================================================== + title Title of the plot (Default: None). + show True to show the figure (default: True). + savefig "abc.png" or "abc.eps" to save the figure to a file. + size_kwargs Dictionary with options passed to fig.set_size_inches + e.g. size_kwargs=dict(w=3, h=4) + tight_layout True to call fig.tight_layout (default: False) + ax_grid True (False) to add (remove) grid from all axes in fig. + Default: None i.e. fig is left unchanged. + ax_annotate Add labels to subplots e.g. (a), (b). + Default: False + fig_close Close figure. Default: False. + plotly Try to convert mpl figure to plotly. + ================ ==================================================== + +""" + + if wrapper.__doc__ is not None: + # Add s at the end of the docstring. + wrapper.__doc__ += f"\n{doc_str}" + else: + # Use s + wrapper.__doc__ = doc_str + + return wrapper + + class FilesPlotter: """ Use matplotlib to plot multiple png files on a grid. @@ -1743,9 +1852,7 @@ def add_plotly_fig_kwargs(func: Callable) -> Callable: sort of error/unexpected event. See doc string below for the list of supported options. """ - from functools import wraps - - @wraps(func) + @functools.wraps(func) def wrapper(*args, **kwargs): # pop the kwds used by the decorator. title = kwargs.pop("title", None) @@ -2647,6 +2754,8 @@ def add_colorscale_dropwdowns(fig): return fig +#TODO: Add plotly option to add_fig_kwargs + def mpl_to_ply(fig: Figure, latex: bool= False): """ Nasty workaround for plotly latex rendering in legend/breaking exception @@ -2699,6 +2808,7 @@ def parse_latex(label): text.set_text(new_label) # Convert to plotly figure + from plotly.tools import mpl_to_plotly plotly_fig = mpl_to_plotly(fig) plotly_fig.update_layout(template = "plotly_white", title = { @@ -2726,7 +2836,6 @@ def parse_latex(label): for trace in plotly_fig.data: # Retrieve the current label and remove any $ signs new_label = trace.name.replace("$", "") - # Update the trace's name (which is used for the legend label) trace.name = new_label @@ -2784,3 +2893,5 @@ def plot(self, deg_list: list[int], # # def extrapolate_to_zero(self, deg: int): + +