diff --git a/.github/workflows/deploy-conda.yml b/.github/workflows/deploy-conda.yml index 5031c33..f152a90 100644 --- a/.github/workflows/deploy-conda.yml +++ b/.github/workflows/deploy-conda.yml @@ -50,6 +50,6 @@ jobs: conda config --set anaconda_upload yes export PATH=$(conda info --root):$PATH export PATH=$(conda info --root)/bin:$PATH - conda-build --output-folder . . + conda build --output-folder conda-bld . env: ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }} diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml index fc278d6..792c081 100644 --- a/.github/workflows/deploy-pypi.yml +++ b/.github/workflows/deploy-pypi.yml @@ -33,7 +33,7 @@ jobs: python setup.py bdist_wheel twine check dist/* - - name: Publish to PyPi + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true diff --git a/.gitignore b/.gitignore index efc9eee..824da3f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,15 @@ # cache __pycache__/ *.py[cod] +.ipynb_checkpoints/ +.pytest_cache/ -# C extentions +# C extensions *.so +*.o + +# Build +.mesonpy-* # Distribution bin/ @@ -23,6 +29,7 @@ develop-eggs/ dist/ eggs/ sdist/ +tmp/ .eggs/ *.egg-info/ .installed.cfg @@ -36,4 +43,10 @@ nosetests.xml coverage.xml # Sphinx documentation -docs/_build/ +docs/build/ + +# Lint +.ruff_cache/ + +# Conda +conda-bld/ diff --git a/MANIFEST.in b/MANIFEST.in index bed30ab..76f806e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,50 +3,28 @@ global-exclude *.so *.dll *.dylib global-exclude *.o global-exclude *.swp -recursive-include docs *.bat -recursive-include docs *.css -recursive-include docs *.py -recursive-include docs *.rst -recursive-include docs *.svg -recursive-include docs *.png -recursive-include docs *.ico -recursive-include docs *.js -recursive-include docs *.ttf -recursive-include docs Makefile -recursive-include docs *.bib -recursive-include docs *.in -recursive-include docs *.txt -recursive-exclude docs *.html -recursive-exclude docs *.pdf -recursive-include docs/source *.html - recursive-include ortho *.rst -recursive-include examples *.py -recursive-include examples *.rst -recursive-include notebooks *.ipynb -recursive-include tests *.py -recursive-include tests *.rst -recursive-exclude tests *.svg -recursive-include tests *.txt - include CHANGELOG.rst include README.rst include AUTHORS.txt include LICENSE.txt include requirements.txt include pyproject.toml -include .coveragerc -include tox.ini -include environment.yml +exclude tox.ini +exclude environment.yml exclude TODO.rst exclude .coverage +exclude .coveragerc exclude .gitattributes +exclude .tokeignore -prune docs/build -prune .git +prune docs prune tmp -prune .tox -prune .github prune conda-recipe -prune benchmark +prune notebooks +prune examples +prune tests +prune .git +prune .github +prune .tox diff --git a/README.rst b/README.rst index 32a4a3a..90bfde6 100644 --- a/README.rst +++ b/README.rst @@ -20,9 +20,9 @@ ortho A python package to generate a set of orthogonal functions. -* `Documentation `_ -* `API Reference `_ -* `Github `_ +* `Documentation `__ +* `API Reference `__ +* `Github `__ ----------- Description @@ -100,7 +100,7 @@ Install using the package available on `PyPi `__ |conda-version| |conda-platform| -Install using `Anaconda cloud `_ by +Install using `Anaconda cloud `__ by :: diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index f1ac15c..28ceb69 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -26,7 +26,6 @@ requirements: - numpy - sympy - matplotlib - - seaborn test: imports: diff --git a/ortho/__version__.py b/ortho/__version__.py index 8879c6c..6a9beea 100644 --- a/ortho/__version__.py +++ b/ortho/__version__.py @@ -1 +1 @@ -__version__ = "0.3.7" +__version__ = "0.4.0" diff --git a/ortho/_orthogonal_functions/display_utilities.py b/ortho/_orthogonal_functions/display_utilities.py new file mode 100644 index 0000000..2a4ccfd --- /dev/null +++ b/ortho/_orthogonal_functions/display_utilities.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright 2021, Siavash Ameli +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileType: SOURCE +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the license found in the LICENSE.txt file in the root +# directory of this source tree. + + +# =========== +# is notebook +# =========== + +def is_notebook(): + """ + Returns ``True`` if this script is running in a jupyter notebook. Returns + ``False`` otherwise, including both ipython and python. + """ + + try: + shell = get_ipython().__class__.__name__ + if shell == 'ZMQInteractiveShell': + return True # Jupyter notebook or qtconsole + elif shell == 'TerminalInteractiveShell': + return False # Terminal running IPython + else: + return False # Other type + except NameError: + return False # Probably standard Python interpreter diff --git a/ortho/_orthogonal_functions/orthogonal_functions.py b/ortho/_orthogonal_functions/orthogonal_functions.py index b171d96..9c75e58 100644 --- a/ortho/_orthogonal_functions/orthogonal_functions.py +++ b/ortho/_orthogonal_functions/orthogonal_functions.py @@ -16,7 +16,7 @@ from .orthogonalization_utilities import check_mutual_orthonormality from .orthogonalization_utilities import get_symbolic_coeffs from .orthogonalization_utilities import get_numeric_coeffs -from .plot_utilities import plot_functions +from .plot_functions import plot_functions # ==================== diff --git a/ortho/_orthogonal_functions/plot_functions.py b/ortho/_orthogonal_functions/plot_functions.py new file mode 100644 index 0000000..b8bc39c --- /dev/null +++ b/ortho/_orthogonal_functions/plot_functions.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright 2021, Siavash Ameli +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileType: SOURCE +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the license found in the LICENSE.txt file in the root directory +# of this source tree. + + +# ======= +# Imports +# ======= + +import numpy +import sympy +import os +from .plot_utilities import plt, matplotlib, get_custom_theme, save_plot +from .declarations import t + +__all__ = ['plot_functions'] + + +# ============== +# Plot Functions +# ============== + +@matplotlib.rc_context(get_custom_theme(font_scale=1.2)) +def plot_functions(phi_orthonormalized_list, start_index, interval): + """ + Plots the generated functions, also saves the plots as both ``svg`` and + ``pdf`` format. + + :param phi_orthonormalized_list: The list of generated functions. Each + entry is a ``sympy`` object. + :param: list + + :param start_index: The indet of the first function. + :type start_index: int + + :param Interval: The right side of the interval of the domain of the + functions. + :param Interval: float + """ + + # Axis + t_array = numpy.logspace(-7, numpy.log10(interval[1]), 1000) + + # Evaluate functions + num_functions = len(phi_orthonormalized_list) + + f = numpy.zeros((num_functions, t_array.size), dtype=float) + for j in range(num_functions): + f_lambdify = sympy.lambdify(t, phi_orthonormalized_list[j], 'numpy') + f[j, :] = f_lambdify(t_array) + + # Plot + fig, ax = plt.subplots(figsize=(7, 4.8)) + for j in range(num_functions): + ax.semilogx(t_array, f[j, :], + label=r'$i = %d$' % (j+start_index)) + + ax.legend(ncol=3, loc='lower left', borderpad=0.5, frameon=False) + ax.set_xlim([t_array[0], t_array[-1]]) + ax.set_ylim([-1, 1]) + ax.set_yticks([-1, 0, 1]) + ax.set_xlabel(r'$t$') + ax.set_ylabel(r'$\phi_i^{\perp}(t)$') + ax.set_title('Orthogonalized inverse-monomial functions') + ax.grid(axis='y') + + # Get the root directory of the package (parent directory of this script) + file_dir = os.path.dirname(os.path.realpath(__file__)) + parent_dir = os.path.dirname(file_dir) + second_parent_dir = os.path.dirname(parent_dir) + + # Try to save in the docs/images directory. Check if exists and writable + save_dir = os.path.join(second_parent_dir, 'docs', 'images') + if (not os.path.isdir(save_dir)) or (not os.access(save_dir, os.W_OK)): + + # Write in the current working directory + save_dir = os.getcwd() + + # Save plot in both svg and pdf format + if os.access(save_dir, os.W_OK): + save_filename = 'orthogonal_functions' + save_plot(plt, save_filename, save_dir=save_dir, + transparent_background=True) + else: + print('Cannot save plot to %s. Directory is not writable.' % save_dir) + + # If no display backend is enabled, do not plot in the interactive mode + if matplotlib.get_backend() != 'agg': + plt.show() diff --git a/ortho/_orthogonal_functions/plot_utilities.py b/ortho/_orthogonal_functions/plot_utilities.py index 6b58a09..46ef482 100644 --- a/ortho/_orthogonal_functions/plot_utilities.py +++ b/ortho/_orthogonal_functions/plot_utilities.py @@ -2,9 +2,9 @@ # SPDX-License-Identifier: BSD-3-Clause # SPDX-FileType: SOURCE # -# This program is free software: you can redistribute it and/or modify it under -# the terms of the license found in the LICENSE.txt file in the root directory -# of this source tree. +# This program is free software: you can redistribute it and/or modify it +# under the terms of the license found in the LICENSE.txt file in the root +# directory of this source tree. # ======= @@ -12,117 +12,368 @@ # ======= import os -import sympy -import numpy -import shutil +import platform import matplotlib -import matplotlib.pyplot as plt -from .declarations import t +import matplotlib.ticker +from matplotlib.ticker import PercentFormatter # noqa: F401 +from matplotlib.ticker import ScalarFormatter, NullFormatter # noqa: F401 +from matplotlib.ticker import FormatStrFormatter, FuncFormatter # noqa: F401 + +import shutil +from .display_utilities import is_notebook +import logging +import warnings + +# Check DISPLAY +if ((not bool(os.environ.get('DISPLAY', None))) or + (bool(os.environ.get('GLEARN_NO_DISPLAY', None)))) and \ + (not is_notebook()): + + # No display found (used on servers). Using non-interactive backend + if platform.system() == 'Darwin': + # For MacOS, first, use macos backend, "then" import pyplot + matplotlib.use('agg') + import matplotlib.pyplot as plt + else: + # For Linux and Windows, "first" import pyplot, then use Agg backend. + import matplotlib.pyplot as plt + plt.switch_backend('agg') +else: + # Display exists. Import pyplot without changing any backend. + import matplotlib.pyplot as plt + +# Remove plt.tight_layout() warning +logging.captureWarnings(True) +warnings.filterwarnings( + action='ignore', + module='matplotlib', + category=UserWarning, + message=('This figure includes Axes that are not compatible with ' + + 'tight_layout, so results might be incorrect.')) + +__all__ = ['get_custom_theme', 'set_custom_theme', 'save_plot', + 'show_or_save_plot'] + + +# ===================== +# customize theme style +# ===================== + +def _customize_theme_style(): + """ + Get the parameters that control the general style of the plots. + + The style parameters control properties like the color of the background + and whether a grid is enabled by default. This is accomplished using the + matplotlib rcParams system. + """ + + # Define colors here + dark_gray = ".15" + light_gray = ".8" + + # Common parameters + style_dict = { + + "figure.facecolor": "white", + "axes.labelcolor": dark_gray, + + "xtick.direction": "out", + "ytick.direction": "out", + "xtick.color": dark_gray, + "ytick.color": dark_gray, + + "axes.axisbelow": True, + "grid.linestyle": "-", + + "text.color": dark_gray, + "font.family": ["sans-serif"], + "font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", + "Bitstream Vera Sans", "sans-serif"], + + "lines.solid_capstyle": "round", + "patch.edgecolor": "w", + "patch.force_edgecolor": True, + "xtick.top": False, + "ytick.right": False, + } -# ============= -# Plot Settings -# ============= + # Set grid + style_dict.update({ + "axes.grid": False, + }) -def plot_settings(): + # Set the color of the background, spines, and grids + style_dict.update({ + + "axes.facecolor": "white", + "axes.edgecolor": dark_gray, + "grid.color": light_gray, + + "axes.spines.left": True, + "axes.spines.bottom": True, + "axes.spines.right": True, + "axes.spines.top": True, + + }) + + # Show the axes ticks + style_dict.update({ + "xtick.bottom": True, + "ytick.left": True, + }) + + return style_dict + + +# ======================= +# customize theme context +# ======================= + +def _customize_theme_context(context="notebook", font_scale=1): """ - General settings for the plot. + Get the parameters that control the scaling of plot elements. + + These parameters correspond to label size, line thickness, etc. For more + information, see the :doc:`aesthetics tutorial <../tutorial/aesthetics>`. + + The base context is "notebook", and the other contexts are "paper", "talk", + and "poster", which are version of the notebook parameters scaled by + different values. Font elements can also be scaled independently of (but + relative to) the other values. + + Parameters + ---------- + + context : None, dict, or one of {paper, notebook, talk, poster} + A dictionary of parameters or the name of a preconfigured set. + + font_scale : float, optional + Separate scaling factor to independently scale the size of the + font elements. """ - # Color palette - import seaborn as sns - # sns.set() + contexts = ["paper", "notebook", "talk", "poster"] + if context not in contexts: + raise ValueError(f"context must be in {', '.join(contexts)}") + + # Set up dictionary of default parameters + texts_base_context = { + + "font.size": 12, + "axes.labelsize": 12, + "axes.titlesize": 12, + "xtick.labelsize": 11, + "ytick.labelsize": 11, + "legend.fontsize": 11, + "legend.title_fontsize": 12, + + } + + base_context = { + + "axes.linewidth": 1.25, + "grid.linewidth": 1, + "lines.linewidth": 1.5, + "lines.markersize": 6, + "patch.linewidth": 1, + + "xtick.major.width": 1.25, + "ytick.major.width": 1.25, + "xtick.minor.width": 1, + "ytick.minor.width": 1, + + "xtick.major.size": 6, + "ytick.major.size": 6, + "xtick.minor.size": 4, + "ytick.minor.size": 4, + + } - # Axes font size - sns.set(font_scale=1.2) + base_context.update(texts_base_context) + + # Scale all the parameters by the same factor depending on the context + scaling = dict(paper=.8, notebook=1, talk=1.5, poster=2)[context] + context_dict = {k: v * scaling for k, v in base_context.items()} + + # Now independently scale the fonts + font_keys = texts_base_context.keys() + font_dict = {k: context_dict[k] * font_scale for k in font_keys} + context_dict.update(font_dict) + + return context_dict + + +# ==================== +# customize theme text +# ==================== + +def _customize_theme_text(): + """ + Returns a dictionary of settings that primarily sets LaTeX, if exists. + """ + + text_dict = {} # LaTeX if shutil.which('latex'): - plt.rc('text', usetex=True) - - # Style sheet - sns.set_style("white") - sns.set_style("ticks") + text_dict['text.usetex'] = True + text_dict['text.latex.preamble'] = r'\usepackage{amsmath}' # Font (Note: this should be AFTER the plt.style.use) - plt.rc('font', family='serif') - plt.rcParams['svg.fonttype'] = 'none' # text in svg file is text not path. + text_dict['font.family'] = 'serif' + text_dict['svg.fonttype'] = 'none' # text in svg will be text not path + + return text_dict -# ============== -# Plot Functions -# ============== +# ================ +# get custom theme +# ================ -def plot_functions(phi_orthonormalized_list, start_index, interval): +def get_custom_theme( + context="notebook", + font_scale=1, + use_latex=True, + **kwargs): """ - Plots the generated functions, also saves the plots as both ``svg`` and - ``pdf`` format. + Returns a dictionary that can be used to update plt.rcParams. - :param phi_orthonormalized_list: The list of generated functions. Each - entry is a ``sympy`` object. - :param: list + Usage: + Before a function, add this line: - :param start_index: The indet of the first function. - :type start_index: int + @matplotlib.rc_context(get_custom_theme(font_scale=1.2)) + def some_plotting_function(): + ... - :param Interval: The right side of the interval of the domain of the - functions. - :param Interval: float + plot.show() + + Note that the plot.show() must be within the "context" (meaning the scope) + of the above rc_context declaration. That is, if plt.show() is postponed + to be a global plt.show() outside of the above function, the matplotlib + parameter settings will be set back to their defaults. Hence, make sure to + plot within the scope of the intended function where the rcParams context + is customized. + + By setting font_scale=1, a pre-set of axes tick sizes are applied to the + plot which are different than the default matplotlib sizes. To disable + these pre-set sizes, set font_scale=None. """ - # Run plot settings - plot_settings() - - # Axis - t_array = numpy.logspace(-7, numpy.log10(interval[1]), 1000) - - # Evaluate functions - num_functions = len(phi_orthonormalized_list) - - f = numpy.zeros((num_functions, t_array.size), dtype=float) - for j in range(num_functions): - f_lambdify = sympy.lambdify(t, phi_orthonormalized_list[j], 'numpy') - f[j, :] = f_lambdify(t_array) - - # Plot - fig, ax = plt.subplots(figsize=(7, 4.8)) - for j in range(num_functions): - ax.semilogx(t_array, f[j, :], - label=r'$i = %d$' % (j+start_index)) - - ax.legend(ncol=3, loc='lower left', borderpad=0.5, frameon=False) - ax.set_xlim([t_array[0], t_array[-1]]) - ax.set_ylim([-1, 1]) - ax.set_yticks([-1, 0, 1]) - ax.set_xlabel(r'$t$') - ax.set_ylabel(r'$\phi_i^{\perp}(t)$') - ax.set_title('Orthogonalized inverse-monomial functions') - ax.grid(axis='y') - - # Get the root directory of the package (parent directory of this script) - file_dir = os.path.dirname(os.path.realpath(__file__)) - parent_dir = os.path.dirname(file_dir) - second_parent_dir = os.path.dirname(parent_dir) - - # Try to save in the docs/images directory. Check if exists and writable - save_dir = os.path.join(second_parent_dir, 'docs', 'images') - if (not os.path.isdir(save_dir)) or (not os.access(save_dir, os.W_OK)): - - # Write in the current working directory + plt_rc_params = {} + + # Set the style (such as the which background, ticks) + plt_rc_params.update(_customize_theme_style()) + + # Set the context (such as scaling font sizes) + if font_scale is not None: + plt_rc_params.update(_customize_theme_context( + context=context, font_scale=font_scale)) + + # Set text rendering and font (such as using LaTeX) + if use_latex is True: + plt_rc_params.update(_customize_theme_text()) + + # Add extra arguments + plt_rc_params.update(kwargs) + + return plt_rc_params + + +# ================ +# set custom theme +# ================ + +def set_custom_theme(context="notebook", font_scale=1, use_latex=True): + """ + Sets a customized theme for plotting. + """ + + plt_rc_params = get_custom_theme(context=context, font_scale=font_scale, + use_latex=use_latex) + matplotlib.rcParams.update(plt_rc_params) + + +# ========= +# save plot +# ========= + +def save_plot( + plt, + filename, + save_dir=None, + transparent_background=True, + pdf=True, + bbox_extra_artists=None, + dpi=200, + verbose=False): + """ + Saves plot as svg format in the current working directory. + + :param plt: matplotlib.pyplot object for the plots. + :type plt: matplotlib.pyplot + + :param filename: Name of the file without extension or directory name. + :type filename: string + + :param transparent_background: Sets the background of svg file to be + transparent. + :type transparent_background: bool + """ + + # If no directory specified, write in the current working directory + if save_dir is None: save_dir = os.getcwd() # Save plot in both svg and pdf format + filename_svg = filename + '.svg' + filename_pdf = filename + '.pdf' if os.access(save_dir, os.W_OK): - save_fullename_svg = os.path.join(save_dir, 'orthogonal_functions.svg') - save_fullename_pdf = os.path.join(save_dir, 'orthogonal_functions.pdf') - plt.savefig(save_fullename_svg, transparent=True, bbox_inches='tight') - plt.savefig(save_fullename_pdf, transparent=True, bbox_inches='tight') - print('') - print('Plot saved to "%s".' % (save_fullename_svg)) - print('Plot saved to "%s".' % (save_fullename_pdf)) + save_fullname_svg = os.path.join(save_dir, filename_svg) + save_fullname_pdf = os.path.join(save_dir, filename_pdf) + + plt.savefig( + save_fullname_svg, + transparent=transparent_background, + bbox_inches='tight') + if verbose: + print('Plot saved to "%s".' % (save_fullname_svg)) + + if pdf: + plt.savefig( + save_fullname_pdf, dpi=dpi, + transparent=transparent_background, + bbox_extra_artists=bbox_extra_artists, bbox_inches='tight') + plt.close() + if verbose: + print('Plot saved to "%s".' % (save_fullname_pdf)) else: print('Cannot save plot to %s. Directory is not writable.' % save_dir) - # If no display backend is enabled, do not plot in the interactive mode - if matplotlib.get_backend() != 'agg': + +# ================= +# show or save plot +# ================= + +def show_or_save_plot( + plt, + filename, + transparent_background=True, + pdf=True, + bbox_extra_artists=None, + dpi=200, + verbose=False): + """ + Shows the plot. If no graphical beckend exists, saves the plot. + """ + + # Check if the graphical back-end exists + if matplotlib.get_backend() != 'agg' or is_notebook(): plt.show() + else: + # write the plot as SVG file in the current working directory + save_plot(plt, filename, transparent_background=transparent_background, + pdf=pdf, bbox_extra_artists=bbox_extra_artists, dpi=dpi, + verbose=verbose) + plt.close() diff --git a/requirements.txt b/requirements.txt index 8160e45..b84497c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ numpy sympy matplotlib -seaborn diff --git a/setup.py b/setup.py index e7ae3fa..bac0182 100644 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ def main(argv): version = version_dummy['__version__'] del version_dummy - # author + # Author author = open(os.path.join(directory, 'AUTHORS.txt'), 'r').read().rstrip() # Requirements @@ -153,13 +153,14 @@ def main(argv): 'tests.*', 'tests', 'examples.*', - 'examples'] + 'examples', + 'docs.*', + 'docs'] ), install_requires=requirements, python_requires='>=3.9', setup_requires=[ - 'setuptools', - 'pytest-runner'], + 'setuptools'], tests_require=[ 'pytest', 'pytest-cov'], diff --git a/tests/test_orthogonal_functions.py b/tests/test_orthogonal_functions.py index cb25b99..3dd9a28 100755 --- a/tests/test_orthogonal_functions.py +++ b/tests/test_orthogonal_functions.py @@ -14,11 +14,29 @@ # ======= # matplotlib without display +import os import matplotlib matplotlib.use('Agg') from ortho import OrthogonalFunctions # noqa: E402 +import warnings +warnings.resetwarnings() +warnings.filterwarnings("error") + + +# =========== +# remove file +# =========== + +def remove_file(filename): + """ + Remove file. + """ + + if os.path.exists(filename): + os.remove(filename) + # ========================= # Test Orthogonal Functions @@ -44,6 +62,10 @@ def test_orthogonal_functions(): # Plot the results OF.plot() + # Remove saved plots + remove_file('orthogonal_functions.svg') + remove_file('orthogonal_functions.pdf') + # =========== # Script Main diff --git a/tests/test_parse_arguments.py b/tests/test_parse_arguments.py index 1a001f0..3b87d2a 100755 --- a/tests/test_parse_arguments.py +++ b/tests/test_parse_arguments.py @@ -15,6 +15,10 @@ from ortho._parse_arguments import parse_arguments +import warnings +warnings.resetwarnings() +warnings.filterwarnings("error") + # ==================== # Test Parse arguments