diff --git a/.coveragerc b/.coveragerc index b14e675..fa3fa52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] -source = - diffpy/snmf/ +source = + src [report] omit = */python?.?/* diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2d2cb16 --- /dev/null +++ b/.flake8 @@ -0,0 +1,11 @@ +[flake8] +exclude = + .git, + __pycache__, + build, + dist, + doc/source/conf.py +max-line-length = 115 +# Ignore some style 'errors' produced while formatting by 'black' +# https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#labels-why-pycodestyle-warnings +extend-ignore = E203 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..42fce5f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +diffpy.snmf/_version.py export-subst diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f7f7590 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,59 @@ +name: Build Documentation + +on: + push: + branches: + - main + release: + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: conda-incubator/setup-miniconda@v2 + with: + activate-environment: build + environment-file: ./environment.yml + python-version: 3 + auto-update-conda: true + + - name: install requirements + run: | + conda config --set always_yes yes --set changeps1 no + conda config --add channels conda-forge + conda create -n build python=3.12 + conda activate build + conda install --file requirements/build.txt + conda install --file requirements/run.txt + conda install --file requirements/test.txt + conda install --file requirements/docs.txt + python -m pip install . --no-deps + + - name: build documents + run: make -C doc html + + - name: Run tests and upload coverage + shell: bash -l {0} + run: | + conda activate build + coverage run -m pytest -vv -s + coverage report -m + codecov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc/build/html diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index 22a7cd1..0000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Documentation -on: - push: - branches: [ main ] - workflow_dispatch: - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: conda-incubator/setup-miniconda@v2 - with: - activate-environment: build - auto-update-conda: true - - - name: Install requirements - run: | - conda install -n build -c conda-forge --file requirements/docs.txt --quiet --yes - sudo apt-get install python3-sphinx - - - name: Install the package - run: python -m pip install . - - - name: Build documents - run: | - conda activate build - make -C doc html - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - publish_branch: gh-pages - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./doc/build/html - force_orphan: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 294255e..581821e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - CI pull_request: workflow_dispatch: @@ -19,9 +20,8 @@ jobs: uses: actions/checkout@v3 with: repository: diffpy/diffpy.snmf - # for bookkeeping have diffpy.snmf at the same level as everything else in the - # directory tree path: . + fetch-depth: 0 # avoid shallow clone with no tags - name: initialize miniconda # this uses a marketplace action that sets up miniconda in a way that makes @@ -50,10 +50,6 @@ jobs: shell: bash -l {0} run: | conda activate test - coverage run run_tests.py + coverage run -m pytest -vv -s coverage report -m - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 -# with: -# token: ${{ secrets.CODECOV_TOKEN }} + codecov diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..f2ff7e4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,19 @@ +name: pre-commit + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + pre-commit: + # pull requests are a duplicate of a branch push if within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + - uses: pre-commit/action@v3.0.0 + with: + extra_args: --all-files diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..e0926f4 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +line_length = 115 +multi_line_output = 3 +include_trailing_comma = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c458806 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +default_language_version: + python: python3 +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit hooks + autofix_prs: true + autoupdate_branch: 'pre-commit-autoupdate' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: monthly + skip: [no-commit-to-branch] + submodules: false +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: '\.(rst|txt)$' + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + - repo: https://github.com/kynan/nbstripout + rev: 0.7.1 + hooks: + - id: nbstripout + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: no-commit-to-branch + name: Prevent Commit to Main Branch + args: ["--branch", "main"] + stages: [pre-commit] diff --git a/AUTHORS.rst b/AUTHORS.rst index 0223b1a..6736f65 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,9 +1,10 @@ Authors -==================== +======= -Ran Gu, Adeolu Ajayi, Qiang Du, Simon J.L. Billinge +Billinge Group and community contibutors. Contributors ------------ -For a full list of contributors, visit + +For a list of contributors, visit https://github.com/diffpy/diffpy.snmf/graphs/contributors diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7bc0953..2669451 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,5 @@ -=========== - Change Log -=========== +============= +Release Notes +============= .. current developments - diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 0000000..ff9c356 --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,133 @@ +===================================== + Contributor Covenant Code of Conduct +===================================== + +Our Pledge +---------- + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +Our Standards +------------- + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +Enforcement Responsibilities +---------------------------- + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +Scope +----- + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +sb2896@columbia.edu. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +Enforcement Guidelines +---------------------- + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +1. Correction +**************** + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +2. Warning +************* + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +3. Temporary Ban +****************** + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +4. Permanent Ban +****************** + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant `_. + +Community Impact Guidelines were inspired by `Mozilla's code of conduct enforcement ladder `_. + +For answers to common questions about this code of conduct, see the `FAQ `_. `Translations are available `_ diff --git a/LICENSE b/LICENSE.rst similarity index 84% rename from LICENSE rename to LICENSE.rst index da86896..74fd798 100644 --- a/LICENSE +++ b/LICENSE.rst @@ -1,6 +1,8 @@ BSD 3-Clause License -Copyright (c) 2023, Simon Billinge +Copyright (c) 2023-2024, The Trustees of Columbia University +in the City of New York. +All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -12,9 +14,9 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7371063 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,14 @@ +include AUTHORS.rst +include LICENSE +include README.rst +include requirements.txt + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include docs *.rst conf.py Makefile make.bat + +include diffpy.snmf/version.py + +# If including data files in the package, add them like: +# include path/to/data_file diff --git a/README.md b/README.md deleted file mode 100644 index 86614d5..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# diffpy.snmf - -[![test](https://github.com/diffpy/diffpy.snmf/actions/workflows/main.yml/badge.svg)](https://github.com/diffpy/diffpy.snmf/actions/workflows/main.yml) -[![codecov](https://codecov.io/gh/diffpy/diffpy.snmf/branch/main/graph/badge.svg)](https://codecov.io/gh/diffpy/diffpy.snmf) diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..aa574de --- /dev/null +++ b/README.rst @@ -0,0 +1,133 @@ +|Icon| |title|_ +=============== + +.. |title| replace:: diffpy.snmf +.. _title: https://diffpy.github.io/diffpy.snmf + +.. |Icon| image:: https://avatars.githubusercontent.com/diffpy + :target: https://diffpy.github.io/diffpy.snmf + :height: 100px + +|PyPi| |Forge| |PythonVersion| |PR| + +|CI| |Codecov| |Black| |Tracking| + +.. |Black| image:: https://img.shields.io/badge/code_style-black-black + :target: https://github.com/psf/black + +.. |CI| image:: https://github.com/diffpy/diffpy.snmf/actions/workflows/main.yml/badge.svg + :target: https://github.com/diffpy/diffpy.snmf/actions/workflows/main.yml + +.. |Codecov| image:: https://codecov.io/gh/diffpy/diffpy.snmf/branch/main/graph/badge.svg + :target: https://codecov.io/gh/diffpy/diffpy.snmf + +.. |Forge| image:: https://img.shields.io/conda/vn/conda-forge/diffpy.snmf + :target: https://anaconda.org/conda-forge/diffpy.snmf + +.. |PR| image:: https://img.shields.io/badge/PR-Welcome-29ab47ff + +.. |PyPi| image:: https://img.shields.io/pypi/v/diffpy.snmf + :target: https://pypi.org/project/diffpy.snmf/ + +.. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/diffpy.snmf + :target: https://pypi.org/project/diffpy.snmf/ + +.. |Tracking| image:: https://img.shields.io/badge/issue_tracking-github-blue + :target: https://github.com/diffpy/diffpy.snmf/issues + +A python package implementing the stretched NMF algorithm. + +``diffpy.snmf`` implements the stretched non negative matrix factorization (sNMF) and sparse stretched NMF +(ssNMF) algorithms. + +This algorithm is designed to do an NMF factorization on a set of signals ignoring any uniform stretching of the signal +on the independent variable axis. For example, for powder diffraction data taken from samples containing multiple +chemical phases where the measurements were done at different temperatures and the materials were undergoing thermal +expansion. + +For more information about the diffpy.snmf library, please consult our `online documentation `_. + +Citation +-------- + +If you use this program for a scientific research that leads +to publication, we ask that you acknowledge use of the program +by citing the following paper in your publication: + + Ran Gu, Yevgeny Rakita, Ling Lan, Zach Thatcher, Gabrielle E. Kamm, Daniel O’Nolan, Brennan Mcbride, Allison Wustrow, James R. Neilson, Karena W. Chapman, Qiang Du, and Simon J. L. Billinge, + `Stretched Non-negative Matrix Factorization + `__, + arXiv:2311.15173 [cond-mat.mtrl-sci] (2023). + + +Installation +------------ + +The preferred method is to use `Miniconda Python +`_ +and install from the "conda-forge" channel of Conda packages. + +To add "conda-forge" to the conda channels, run the following in a terminal. :: + + conda config --add channels conda-forge + +We want to install our packages in a suitable conda environment. +The following creates and activates a new environment named ``diffpy.snmf_env`` :: + + conda create -n diffpy.snmf_env python=3 + conda activate diffpy.snmf_env + +Then, to fully install ``diffpy.snmf`` in our active environment, run :: + + conda install diffpy.snmf + +Another option is to use ``pip`` to download and install the latest release from +`Python Package Index `_. +To install using ``pip`` into your ``diffpy.snmf_env`` environment, we will also have to install dependencies :: + + pip install -r https://raw.githubusercontent.com/diffpy/diffpy.snmf/main/requirements/run.txt + +and then install the package :: + + pip install diffpy.snmf + +If you prefer to install from sources, after installing the dependencies, obtain the source archive from +`GitHub `_. Once installed, ``cd`` into your ``diffpy.snmf`` directory +and run the following :: + + pip install . + +Support and Contribute +---------------------- + +`Diffpy user group `_ is the discussion forum for general questions and discussions about the use of diffpy.snmf. Please join the diffpy.snmf users community by joining the Google group. The diffpy.snmf project welcomes your expertise and enthusiasm! + +If you see a bug or want to request a feature, please `report it as an issue `_ and/or `submit a fix as a PR `_. You can also post it to the `Diffpy user group `_. + +Feel free to fork the project and contribute. To install diffpy.snmf +in a development mode, with its sources being directly used by Python +rather than copied to a package directory, use the following in the root +directory :: + + pip install -e . + +To ensure code quality and to prevent accidental commits into the default branch, please set up the use of our pre-commit +hooks. + +1. Install pre-commit in your working environment by running ``conda install pre-commit``. + +2. Initialize pre-commit (one time only) ``pre-commit install``. + +Thereafter your code will be linted by black and isort and checked against flake8 before you can commit. +If it fails by black or isort, just rerun and it should pass (black and isort will modify the files so should +pass after they are modified). If the flake8 test fails please see the error messages and fix them manually before +trying to commit again. + +Improvements and fixes are always appreciated. + +Before contribuing, please read our `Code of Conduct `_. + +Contact +------- + +For more information on diffpy.snmf please visit the project `web-page `_ or email Prof. Simon Billinge at sb2896@columbia.edu. diff --git a/diffpy/__init__.py b/diffpy/__init__.py deleted file mode 100644 index 8fbb313..0000000 --- a/diffpy/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -############################################################################## -# -# diffpy by DANSE Diffraction group -# Simon J. L. Billinge -# (c) 2008 Trustees of the Columbia University -# in the City of New York. All rights reserved. -# -# File coded by: Pavol Juhas -# -# See AUTHORS.txt for a list of people who contributed. -# See LICENSE.txt for license information. -# -############################################################################## - -"""snmf - implements the stretchednmf algorithm - -""" - - -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) - - -# End of file diff --git a/diffpy/snmf/__init__.py b/diffpy/snmf/__init__.py deleted file mode 100644 index 0fd5168..0000000 --- a/diffpy/snmf/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -############################################################################## -# -# pdfmorph by DANSE Diffraction group -# Simon J. L. Billinge -# (c) 2006 trustees of the Michigan State University. -# All rights reserved. -# -# File coded by: Chris Farrow -# -# See AUTHORS.txt for a list of people who contributed. -# See LICENSE.txt for license information. -# -############################################################################## - -"""Tools for manipulating and comparing PDFs. -""" - -# obtain version information -__version__ = '0.0.1' - -# top-level import -#from diffpy.snmf.pdfmorph_api import pdfmorph, morph_default_config, plot_morph - -# End of file diff --git a/diffpy/snmf/stretchednmfapp.py b/diffpy/snmf/stretchednmfapp.py deleted file mode 100644 index 067d527..0000000 --- a/diffpy/snmf/stretchednmfapp.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np -import argparse -from pathlib import Path -from diffpy.snmf.subroutines import lift_data, initialize_components -from diffpy.snmf.containers import ComponentSignal -from diffpy.snmf.io import load_input_signals, initialize_variables - -ALLOWED_DATA_TYPES = ['powder_diffraction', 'pd', 'pair_distribution_function', 'pdf'] - - -def create_parser(): - parser = argparse.ArgumentParser( - prog="stretched_nmf", - description="Stretched Nonnegative Matrix Factorization" - ) - parser.add_argument('-i', '--input-directory', type=str, default=None, - help="Directory containing experimental data. Defaults to current working directory.") - parser.add_argument('-o', '--output-directory', type=str, - help="The directory where the results will be written. Defaults to '/snmf_results'.") - parser.add_argument('t', '--data-type', type=str, default=None, choices=ALLOWED_DATA_TYPES, - help="The type of the experimental data.") - parser.add_argument('-l', '--lift-factor', type=float, default=1, - help="The lifting factor. Data will be lifted by lifted_data = data + abs(min(data) * lift). Default is 1.") - parser.add_argument('number-of-components', type=int, - help="The number of component signals for the NMF decomposition. Must be an integer greater than 0") - parser.add_argument('-v', '--version', action='version', help='Print the software version number') - args = parser.parse_args() - return args - - -def main(): - args = create_parser() - if args.input_directory is None: - args.input_directory = Path.cwd() - grid, input_data = load_input_signals(args.input_directory) - lifted_input_data = lift_data(input_data, args.lift_factor) - variables = initialize_variables(lifted_input_data, args.number_of_components, args.data_type) - components = initialize_components(variables['number_of_components'], variables['number_of_signals'], grid) - return components diff --git a/diffpy/snmf/tests/test_containers.py b/diffpy/snmf/tests/test_containers.py deleted file mode 100644 index a80a876..0000000 --- a/diffpy/snmf/tests/test_containers.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest -import numpy as np -from diffpy.snmf.containers import ComponentSignal - -tas = [([np.arange(10), 3, 0, [6.55, .357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], .25], - [[6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], - [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0]]), - ([np.arange(5), 10, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], 1.25], - [[-11.47, -10.8444, -9.1322, -16.633, -20.6760], [0, -.50048, -3.31904, 40.9824, -112.1792], - [0, .800768, 5.310464, -65.57184, 179.48672]]), - ([np.arange(5), 2, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], .88], - [[-11.47, -10.3344, -13.9164, -11.5136, 0], [0, -3.3484, 55.1265, -169.7572, 0], - [0, 7.609997, -125.2876, 385.81189, 0]]), - ([np.arange(10), 1, 2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], .88], - [[1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], - [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], - [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0]]), - ([np.arange(14), 100, 3, - [-2.9384, -1.4623, -2.0913, 4.6304, -1.2127, 1.4737, -0.3791, 1.7506, -1.5068, -2.7625, .9617, -.3494, -.3862, - 2.7960], .55], [[-2.9384, -1.9769, 0.9121, .6314, .8622, -2.4239, -.2302, 1.9281, 0, 0, 0, 0, 0, 0], - [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], - [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0]]), - ([np.arange(11), 20, 4, [0, .25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], .987], - [[0, .2533, .5066, .7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], - [0, -.2566, -.5132, -.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], - [0, .5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0]]), - ([np.arange(9), 15, 3, [-1, -2, -3, -4, -5, -6, -7, -8, -9], -0.4], - [[-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]]) - ] -@pytest.mark.parametrize('tas', tas) -def test_apply_stretch(tas): - component = ComponentSignal(tas[0][0], tas[0][1], tas[0][2]) - component.iq = tas[0][3] - component.stretching_factors[0] = tas[0][4] - actual = component.apply_stretch(0) - expected = tas[1] - np.testing.assert_allclose(actual, expected, rtol=1e-01) - - -taw = [([np.arange(5), 2, 0, [0, 1, 2, 3, 4], .5], [0, .5, 1, 1.5, 2]), - ([np.arange(5), 20, 2, [0, -1, -2, -3, -4], .25], [0, -.25, -.5, -.75, -1]), - ([np.arange(40), 200, 4, np.arange(0, 10, .25), .3], np.arange(0, 10, .25) * .3), - ([np.arange(1), 10, 2, [10.5, 11.5, -10.5], 0], [0, 0, 0]), - ([[-12, -10, -15], 5, 2, [-.5, -1, -1.2], .9], [-.45, -.9, -1.08]), - ([[-12, -10, -15], 5, 2, [0, 0, 0], .9], [0, 0, 0]) - ] -@pytest.mark.parametrize('taw', taw) -def test_apply_weight(taw): - component = ComponentSignal(taw[0][0], taw[0][1], taw[0][2]) - component.iq = np.array(taw[0][3]) - component.weights[0] = taw[0][4] - actual = component.apply_weight(0) - expected = taw[1] - np.testing.assert_allclose(actual, expected, rtol=1e-01) diff --git a/diffpy/snmf/tests/test_subroutines.py b/diffpy/snmf/tests/test_subroutines.py deleted file mode 100644 index 1a93971..0000000 --- a/diffpy/snmf/tests/test_subroutines.py +++ /dev/null @@ -1,253 +0,0 @@ -import pytest -import numpy as np -from diffpy.snmf.containers import ComponentSignal -from diffpy.snmf.subroutines import objective_function, get_stretched_component, reconstruct_data, get_residual_matrix, \ - update_weights_matrix, initialize_arrays, lift_data, initialize_components, construct_stretching_matrix, \ - construct_component_matrix, construct_weight_matrix, update_weights, reconstruct_signal - -to = [ - ([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 1], 2.574e14), - ([[[11, 2], [31, 4]], [[5, 63], [7, 18]], .001, [[21, 2], [3, 4]], [[11, 22], [3, 40]], 1], 650.4576), - ([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 0], 2.574e14), -] - - -@pytest.mark.parametrize("to", to) -def test_objective_function(to): - actual = objective_function(to[0][0], to[0][1], to[0][2], to[0][3], to[0][4], to[0][5]) - expected = to[1] - assert actual == pytest.approx(expected) - - -tgso = [ - ([.25, [6.55, .357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], 10], ( - [6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], - [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0])), - ([1.25, [-11.47, -10.688, -8.095, -29.44, 14.38], 5], ( - [-11.47, -10.8444, -9.1322, -16.633, -20.6760], [0, -.50048, -3.31904, 40.9824, -112.1792], - [0, .800768, 5.310464, -65.57184, 179.48672])), - ([.88, [-11.47, -10.688, -8.095, -29.44, 14.38], 5], ( - [-11.47, -10.3344, -13.9164, -11.5136, 0], [0, -3.3484, 55.1265, -169.7572, 0], - [0, 7.609997, -125.2876, 385.81189, 0])), - ( - [.88, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10], - ([1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], - [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], - [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0])), - ([.55, - [-2.9384, -1.4623, -2.0913, 4.6304, -1.2127, 1.4737, -0.3791, 1.7506, -1.5068, -2.7625, .9617, -.3494, -.3862, - 2.7960], 14], ([-2.9384, -1.9769, 0.9121, .6314, .8622, -2.4239, -.2302, 1.9281, 0, 0, 0, 0, 0, 0], - [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], - [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0])), - ([.987, [0, .25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], 11], - ([0, .2533, .5066, .7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], - [0, -.2566, -.5132, -.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], - [0, .5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0])), - ([-0.4, [-1, -2, -3, -4, -5, -6, -7, -8, -9], 9], - ([-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0])) -] - - -@pytest.mark.parametrize("tgso", tgso) -def test_get_stretched_component(tgso): - actual = get_stretched_component(tgso[0][0], tgso[0][1], tgso[0][2]) - expected = tgso[1] - np.testing.assert_allclose(actual, expected, rtol=1e-01) - - -tuwm = [([2, 2, [[.5, .6], [.7, .8]], [[1, 2], [4, 8]], [[1.6, 2.8], [5, 8.8]], 2, [[.78, .12], [.5, .5]], None], - [[0, 1], [1, 1]]), - ([2, 3, [[.5], [.5]], [[1, 2.5], [1.5, 3], [2, 3.5]], [[1, 2], [3, 4], [5, 6]], 1, [[.5], [.5]], None], - [[1], [.1892]]), - ([2, 3, [[.5, .6, .7], [.5, .6, .7]], [[1, 2.5], [1.5, 3], [2, 3.5]], [[1, 2, 3], [3, 4, 5], [5, 6, 7]], 3, - [[.5, .45, .4], [.5, .45, .4]], None], [[1, 1, 1], [.1892, .5600, .938]]), - ([3, 3, [[.7, .8, .9], [.71, .72, .73], [.8, .85, .9]], [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -.5]], - [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], 3, [[.9, .4, .5], [1, 0, .4], [0, 0, .98]], None], - [[1, .0651, 0], [.5848, .0381, .1857], [0, 1, 1]]), - ([2, 2, [[.5], [.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[.6], [.4]], 'align'], [[0], [0]]), - ([1, 3, [[.5, .3]], [[1], [1.1], [1.3]], [[1, 2], [2, 3], [3, 2]], 2, [[.6, .4]], None], [[1, 1]]), - ([2, 2, [[.5, .6], [.7, .8]], [[1, 2], [4, 8]], [[1.6, 2.8], [5, 8.8]], 2, [[.78, .12], [.5, .5]], 'align'], - [[0, 0], [1.0466, 1.46]]), - ([2, 3, [[.5], [.5]], [[1, 2.5], [1.5, 3], [2, 3.5]], [[1, 2], [3, 4], [5, 6]], 1, [[.5], [.5]], 'align'], - [[1.4], [0]]), - ([3, 3, [[.7, .8, .9], [.71, .72, .73], [.8, .85, .9]], [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -.5]], - [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], 3, [[.9, .4, .5], [1, 0, .4], [0, 0, .98]], 'align'], - [[1.2605, .0552, 0], [.2723, 0, 0], [0, 1.0538, 1.1696]]), - ([2, 2, [[.5], [.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[.6], [.4]], 'align'], [[0], [0]]), - ([1, 3, [[.5, .3]], [[1], [1.1], [1.3]], [[1, 2], [2, 3], [3, 2]], 2, [[.6, .4]], 'align'], [[1.3383, 2]]) - ] - - -@pytest.mark.parametrize('tuwm', tuwm) -def test_update_weights_matrix(tuwm): - actual = update_weights_matrix(tuwm[0][0], tuwm[0][1], tuwm[0][2], tuwm[0][3], tuwm[0][4], tuwm[0][5], tuwm[0][6], - tuwm[0][7]) - expected = tuwm[1] - np.testing.assert_allclose(actual, expected, rtol=1e-03, atol=.5) - - -tgrm = [ - ([[[1, 2], [3, 4]], [[.25], [.75]], [[.9], [.7]], [[11, 22], [33, 44]], 1, 2, 2], [[-9, -22], [-33, -44]]), - ([[[1, 2], [3, 4]], [[1], [1]], [[1], [1]], [[11, 22], [33, 44]], 1, 2, 2], [[-8, -22], [-26, -44]]), - ([[[1.1, 4.4], [1.2, 4.5], [14, 7.8]], [[.4, .6], [.75, .25]], [[.9, .89], [.98, .88]], - [[10, 20], [-10.5, -20.6], [0.6, .9]], 2, 2, 3], [[-6.26, -18.24], [14.9744, 23.5067], [-.6, -.9]]), - # positive float - ([[[-1.1, -4.4], [-1.2, -4.5], [-14, -7.8]], [[.4, .6], [.75, .25]], [[.9, .89], [.98, .88]], - [[10, 20], [-10.5, -20.6], [0.6, .9]], 2, 2, 3], [[-13.74, -21.76], [6.0256, 17.6933], [-.6, -.9]]), - # negative floats - ([[[0, 0, 0, 0], [0, 0, 0, 0]], [[.4], [.2], [.3], [.3]], [[.9], [.9], [.9], [.9]], [[0, 0, 0, 0], [0, 0, 0, 0]], 1, - 4, 2], [[0, 0, 0, 0], [0, 0, 0, 0]]) -] - - -@pytest.mark.parametrize('tgrm', tgrm) -def test_get_residual_matrix(tgrm): - actual = get_residual_matrix(tgrm[0][0], tgrm[0][1], tgrm[0][2], tgrm[0][3], tgrm[0][4], tgrm[0][5], tgrm[0][6]) - expected = tgrm[1] - np.testing.assert_allclose(actual, expected, rtol=1e-04) - - -trd = [ - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0)]), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2), ComponentSignal([0, .25, .5, .75, 1], 2, 3), - ComponentSignal([0, .25, .5, .75, 1], 2, 4)]), - #([]) # Exception expected -] -@pytest.mark.parametrize('trd', trd) -def test_reconstruct_data(trd): - actual = reconstruct_data(trd) - assert actual.shape == (len(trd[0].iq),len(trd[0].weights)) - print(actual) - - -tld = [(([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], 1]), ([[4, 2, 4], [3, 3, 3], [5, 13, 0]])), - (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], 0]), ([[1, -1, 1], [0, 0, 0], [2, 10, -3]])), - (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], .5]), ([[2.5, .5, 2.5], [1.5, 1.5, 1.5], [3.5, 11.5, -1.5]])), - (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], -1]), ([[4, 2, 4], [3, 3, 3], [5, 13, 0]])), - (([[[0, 0, 0], [0, 0, 0], [0, 0, 0]], 100]), ([[0, 0, 0], [0, 0, 0], [0, 0, 0]])), - (([[[1.5, 2], [10.5, 1], [0.5, 2]], 1]), ([[2, 2.5], [11, 1.5], [1, 2.5]])), - (([[[-10, -10.5], [-12.2, -12.2], [0, 0]], 1]), ([[2.2, 1.7], [0, 0], [12.2, 12.2]])), - ] - - -@pytest.mark.parametrize('tld', tld) -def test_lift_data(tld): - actual = lift_data(tld[0][0], tld[0][1]) - expected = tld[1] - np.testing.assert_allclose(actual, expected) - -tcc = [(2, 3,[0, .5, 1, 1.5]), # Regular usage - #(0, 3,[0, .5, 1, 1.5]), # Zero components raise an exception. Not tested - ] -@pytest.mark.parametrize('tcc', tcc) -def test_initialize_components(tcc): - actual = initialize_components(tcc[0], tcc[1], tcc[2]) - assert len(actual) == tcc[0] - assert len(actual[0].weights) == tcc[1] - assert (actual[0].grid == np.array(tcc[2])).all() - -tcso =[([ComponentSignal([0,.5,1,1.5],20,0)],1,20), - ([ComponentSignal([0,.5,1,1.5],20,0)],4,20), - # ([ComponentSignal([0,.5,1,1.5],20,0)],0,20), # Raises an exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],-2,20), # Raises an exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],1,0), # Raises an Exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],1,-3), # Raises an exception - ([ComponentSignal([0,.5,1,1.5],20,0),ComponentSignal([0,.5,1,1.5],20,1)],2,20), - ([ComponentSignal([0,.5,1,1.5],20,0),ComponentSignal([0,.5,1,21.5],20,1)],2,20), - ([ComponentSignal([0,1,1.5],20,0),ComponentSignal([0,.5,1,21.5],20,1)],2,20), - # ([ComponentSignal([0,.5,1,1.5],20,0),ComponentSignal([0,.5,1,1.5],20,1)],1,-3), # Negative signal length. Raises an exception - #([],1,20), # Empty components. Raises an Exception - #([],-1,20), # Empty components with negative number of components. Raises an exception - #([],0,20), # Empty components with zero number of components. Raises an exception - #([],1,0), # Empty components with zero signal length. Raises an exception. - #([],-1,-2), # Empty components with negative number of components and signal length Raises an exception. - -] -@pytest.mark.parametrize('tcso',tcso) -def test_construct_stretching_matrix(tcso): - actual = construct_stretching_matrix(tcso[0],tcso[1],tcso[2]) - for component in tcso[0]: - np.testing.assert_allclose(actual[component.id,:], component.stretching_factors) - #assert actual[component.id, :] == component.stretching_factors - -tccm = [ - ([ComponentSignal([0,.25,.5,.75,1],20,0)]), - ([ComponentSignal([0,.25,.5,.75,1],0,0)]), - ([ComponentSignal([0,.25,.5,.75,1],20,0),ComponentSignal([0,.25,.5,.75,1],20,1),ComponentSignal([0,.25,.5,.75,1],20,2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, .75, 1], 20, 1), - ComponentSignal([0, .25, .5, .75, 1], 20, 2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, 2.75, 1], 20, 1), - ComponentSignal([0, .25, .5, .75, 1], 20, 2)]), - ([ComponentSignal([.25], 20, 0), ComponentSignal([.25], 20, 1), ComponentSignal([.25], 20, 2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, .75, 1], 20, 1)]), - # ([ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 0), ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 1)]), # iq is multidimensional. Expected to fail - # (ComponentSignal([], 20, 0)), # Expected to fail - # ([]), #Expected to fail - ] -@pytest.mark.parametrize('tccm',tccm) -def test_construct_component_matrix(tccm): - actual = construct_component_matrix(tccm) - for component in tccm: - np.testing.assert_allclose(actual[component.id], component.iq) - - -tcwm = [ - ([ComponentSignal([0,.25,.5,.75,1],20,0)]), - # ([ComponentSignal([0,.25,.5,.75,1],0,0)]), # 0 signal length. Failure expected - ([ComponentSignal([0,.25,.5,.75,1],20,0),ComponentSignal([0,.25,.5,.75,1],20,1),ComponentSignal([0,.25,.5,.75,1],20,2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, .75, 1], 20, 1), - ComponentSignal([0, .25, .5, .75, 1], 20, 2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, 2.75, 1], 20, 1), - ComponentSignal([0, .25, .5, .75, 1], 20, 2)]), - ([ComponentSignal([.25], 20, 0), ComponentSignal([.25], 20, 1), ComponentSignal([.25], 20, 2)]), - ([ComponentSignal([0, .25, .5, .75, 1], 20, 0), ComponentSignal([0, .25, .5, .75, 1], 20, 1)]), - #(ComponentSignal([], 20, 0)), # Expected to fail - #([]), #Expected to fail -] -@pytest.mark.parametrize('tcwm',tcwm) -def test_construct_weight_matrix(tcwm): - actual = construct_weight_matrix(tcwm) - for component in tcwm: - np.testing.assert_allclose(actual[component.id], component.weights) - - -tuw = [([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], None), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], "align"), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], None), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], "align"), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[-.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, .1]], None), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [[-.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, .1]], "align"), - # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), - # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], None), - # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), - # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [], None), - # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [], 170), - ] -@pytest.mark.parametrize('tuw', tuw) -def test_update_weights(tuw): - actual = update_weights(tuw[0], tuw[1], tuw[2]) - assert np.shape(actual) == (len(tuw[0]), len(tuw[0][0].weights)) - -trs = [([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], 1), - ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - ComponentSignal([0, .25, .5, .75, 1], 2, 2)], 0), - ([ComponentSignal([0, .25, .5, .75, 1], 3, 0), ComponentSignal([0, .25, .5, .75, 1], 3, 1), - ComponentSignal([0, .25, .5, .75, 1], 3, 2)], 2), - # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], -1), -] -@pytest.mark.parametrize('trs',trs) -def test_reconstruct_signal(trs): - actual = reconstruct_signal(trs[0], trs[1]) - assert len(actual) == len(trs[0][0].grid) diff --git a/diffpy/snmf/version.py b/diffpy/snmf/version.py deleted file mode 100644 index bf3c080..0000000 --- a/diffpy/snmf/version.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -############################################################################## -# -# Structure by DANSE Diffraction group -# Simon J. L. Billinge -# (c) 2008 trustees of the Michigan State University. -# All rights reserved. -# -# File coded by: Pavol Juhas -# -# See AUTHORS.txt for a list of people who contributed. -# See LICENSE.txt for license information. -# -############################################################################## - -"""Definition of __version__ and __date__ for diffpy.pdfmorph. -""" - - -# obtain version information -from pkg_resources import get_distribution - -__version__ = get_distribution('diffpy.snmf').version - -# we assume that tag_date was used and __version__ ends in YYYYMMDD -__date__ = __version__[-8:-4] + '-' + __version__[-4:-2] + '-' + __version__[-2:] - -# End of file diff --git a/doc/Makefile b/doc/Makefile index d0c3cbf..fdc96a7 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,20 +1,194 @@ -# Minimal makefile for Sphinx documentation +# Makefile for Sphinx documentation # -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = BUILDDIR = build +BASENAME = $(subst .,,$(subst $() $(),,diffpy.snmf)) + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext -# Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/$(BASENAME).qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/$(BASENAME).qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/$(BASENAME)" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/$(BASENAME)" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +# Manual publishing to the gh-pages branch -.PHONY: help Makefile +GITREPOPATH = $(shell cd $(CURDIR) && git rev-parse --git-dir) +GITREMOTE = origin +GITREMOTEURL = $(shell git config --get remote.$(GITREMOTE).url) +GITLASTCOMMIT = $(shell git rev-parse --short HEAD) -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +publish: + @test -d build/html || \ + ( echo >&2 "Run 'make html' first!"; false ) + git show-ref --verify --quiet refs/heads/gh-pages || \ + git branch --track gh-pages $(GITREMOTE)/gh-pages + test -d build/gh-pages || \ + git clone -s -b gh-pages $(GITREPOPATH) build/gh-pages + cd build/gh-pages && \ + git pull $(GITREMOTEURL) gh-pages + rsync -acv --delete --exclude=.git --exclude=.rsync-exclude \ + --exclude-from=build/gh-pages/.rsync-exclude \ + --link-dest=$(CURDIR)/build/html build/html/ build/gh-pages/ + cd build/gh-pages && \ + git add --all . && \ + git diff --cached --quiet || \ + git commit -m "Sync with the source at $(GITLASTCOMMIT)." + cd build/gh-pages && \ + git push origin gh-pages diff --git a/doc/images/morph_ex2.png b/doc/images/morph_ex2.png deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/nacl_example.agr b/doc/images/nacl_example.agr deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/nacl_example.xcf b/doc/images/nacl_example.xcf deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_example.agr b/doc/images/pdfmorph_example.agr deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_example.png b/doc/images/pdfmorph_example.png deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_example.xcf b/doc/images/pdfmorph_example.xcf deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_smear.png b/doc/images/pdfmorph_smear.png deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_smear2.png b/doc/images/pdfmorph_smear2.png deleted file mode 100644 index e69de29..0000000 diff --git a/doc/images/pdfmorph_stretch.png b/doc/images/pdfmorph_stretch.png deleted file mode 100644 index e69de29..0000000 diff --git a/doc/make.bat b/doc/make.bat index 9534b01..2be8306 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -9,6 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=source set BUILDDIR=build +set SPHINXPROJ=PackagingScientificPython if "%1" == "" goto help @@ -25,11 +26,11 @@ if errorlevel 9009 ( exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd diff --git a/doc/images/aspirin_smear.png b/doc/source/_static/.placeholder similarity index 100% rename from doc/images/aspirin_smear.png rename to doc/source/_static/.placeholder diff --git a/doc/source/api/diffpy.snmf.rst b/doc/source/api/diffpy.snmf.rst new file mode 100644 index 0000000..9a808f6 --- /dev/null +++ b/doc/source/api/diffpy.snmf.rst @@ -0,0 +1,68 @@ +:tocdepth: -1 + +diffpy.snmf package +=================== + +.. automodule:: diffpy.snmf + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +diffpy.snmf.subroutines module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.subroutines + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.containers module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.containers + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.io module +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.io + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.polynomials module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.polynomials + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.optimizers module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.optimizers + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.factorizers module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.factorizers + :members: + :undoc-members: + :show-inheritance: + +diffpy.snmf.stretchednmfapp module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.snmf.stretchednmfapp + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/conf.py b/doc/source/conf.py index a6cf4b5..195ee2f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,95 +1,289 @@ -# Configuration file for the Sphinx documentation builder. +#!/usr/bin/env python +# -*- coding: utf-8 -*- # -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# http://www.sphinx-doc.org/en/master/config +# diffpy.snmf documentation build configuration file, created by +# sphinx-quickstart on Thu Jan 30 15:49:41 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. -# -- Path setup -------------------------------------------------------------- +import sys +import time +from importlib.metadata import version +from pathlib import Path # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import sys -import os -sys.path.insert(0, os.path.abspath('../..')) - - -# -- Project information ----------------------------------------------------- +# documentation root, use Path().resolve() to make it absolute, like shown here. +# sys.path.insert(0, str(Path(".").resolve())) +sys.path.insert(0, str(Path("../..").resolve())) +sys.path.insert(0, str(Path("../../src").resolve())) -project = 'snmf' -copyright = '2009-2023, Trustees of Columbia University in the City of New York, all rights reserved.' -author = 'Ran Gu, Adeolu Ajayi, Qiang Du, Simon J.L. Billinge' +# abbreviations +ab_authors = "Billinge Group members and community contributors" -# The full version, including alpha/beta/rc tags -release = '0.1.0' +# -- General configuration ------------------------------------------------ - -# -- General configuration --------------------------------------------------- +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -import sphinx_rtd_theme -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', - 'sphinx.ext.todo', 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', 'm2r'] -napoleon_google_docstring = False -napoleon_use_param = False -napoleon_use_ivar = False +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx_rtd_theme", + "m2r", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = [".rst", ".md"] + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "diffpy.snmf" +copyright = "2023-%Y, The Trustees of Columbia University in the City of New York" -from jinja2 import Template, Environment, FileSystemLoader +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. -source_suffix = '.rst' +fullversion = version(project) +# The short X.Y version. +version = "".join(fullversion.split(".post")[:1]) +# The full version, including alpha/beta/rc tags. +release = fullversion -master_doc = 'index' +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +today = time.strftime("%B %d, %Y", time.localtime()) +year = today.split()[-1] +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' +# substitute YEAR in the copyright string +copyright = copyright.replace("%Y", year) -language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['Thumbs.db', '.DS_Store'] +exclude_patterns = ["build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True -pygments_style = 'sphinx' +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False -todo_include_todos = True +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" -# -- Options for HTML output ------------------------------------------------- +# A list of ignored prefixes for module index sorting. +modindex_common_prefix = ["diffpy.snmf"] + +# Display all warnings for missing links. +nitpicky = True + +# -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# +html_theme = "sphinx_rtd_theme" -html_theme_options = {} +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = { + "navigation_with_keys": "true", +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' -htmlhelp_basename = 'snmfdoc' +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +basename = "diffpy.snmf".replace(" ", "").replace(".", "") +htmlhelp_basename = basename + "doc" + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'snmf.tex', 'Snmf Documentation', - 'author', 'manual'), + ("index", "diffpy.snmf.tex", "diffpy.snmf Documentation", ab_authors, "manual"), ] -man_pages = [ - (master_doc, 'snmf', 'Snmf Documentation', [author], 1) -] +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [("index", "diffpy.snmf", "diffpy.snmf Documentation", ab_authors, 1)] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'snmf', 'Snmf Documentation', author, - 'snmf', 'One line description of project.', 'Miscellaneous'), + ( + "index", + "diffpy.snmf", + "diffpy.snmf Documentation", + ab_authors, + "diffpy.snmf", + "One line description of project.", + "Miscellaneous", + ), ] -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + -epub_exclude_files = ['search.html'] +# Example configuration for intersphinx: refer to the Python standard library. +# intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/index.rst b/doc/source/index.rst index 25e675d..1122b4f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -2,7 +2,8 @@ Welcome to SNMF's Documentation! ==================================== ``SNMF``: This library implements the stretched non negative matrix factorization (sNMF) and sparse stretched NMF -(ssNMF) algorithms described in ... +(ssNMF) algorithms described in the paper "Stretched Non-negative Matrix Factorization" by Ran Gu et al. (2023), +which is referenced under the Citation section below. This algorithm is designed to do an NMF factorization on a set of signals ignoring any uniform stretching of the signal on the independent variable axis. For example, for powder diffraction data taken from samples containing multiple @@ -38,6 +39,19 @@ within the chemical phases through its addition of stretching factors. It is important to note that the user must specify the number of component signals to obtain from the experimental data. Non-physical results may be obtained if the number of anticipated component signals is too high. +Citation +-------- + +If you use this program for a scientific research that leads +to publication, we ask that you acknowledge use of the program +by citing the following paper in your publication: + + Ran Gu, Yevgeny Rakita, Ling Lan, Zach Thatcher, Gabrielle E. Kamm, Daniel O’Nolan, Brennan Mcbride, Allison Wustrow, James R. Neilson, Karena W. Chapman, Qiang Du, and Simon J. L. Billinge, + `Stretched Non-negative Matrix Factorization + `__, + arXiv:2311.15173 [cond-mat.mtrl-sci] (2023). + + Authors ------- @@ -66,6 +80,7 @@ To get started, please go to :ref:`quick_start` license release + Package API .. include:: ../../CHANGELOG.rst diff --git a/doc/source/license.rst b/doc/source/license.rst index 826133d..2dd9469 100644 --- a/doc/source/license.rst +++ b/doc/source/license.rst @@ -1,40 +1,39 @@ -License -======= +:tocdepth: -1 + +.. index:: license -This program is part of the DiffPy open-source project at Columbia -University and is available subject to the conditions and terms laid out -below. +License +####### -Copyright © 2009-2019, Trustees of Columbia University in the City of -New York, all rights reserved. +OPEN SOURCE LICENSE AGREEMENT +============================= +BSD 3-Clause License -For more information please visit the diffpy web-page at -http://diffpy.org or email Prof. Simon Billinge at sb2896@columbia.edu. +Copyright (c) 2023-2024, The Trustees of Columbia University in +the City of New York. +All Rights Reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * The name of COLUMBIA UNIVERSITY nor the names of its contributors - may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 6bf4a72..0eba4eb 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -1,189 +1,22 @@ .. _quick_start: -PDFmorph Tutorial +Tutorial (To be addded) ################# -Welcome! This will be a quick tutorial to accquaint users with PDFmorph -and some of what it can do. +Welcome! This will be a quick tutorial to accquaint users with `snmf`. -As we described in the README and installation instructions, please make -sure that you are familiar with working with your command line terminal -before using this application. +Basic Workflow +======================= -Before you've started this tutorial, please ensure that you've installed -all necessary software and dependencies. + 1. Add a step-by-step guide to the basic workflow of the software. -Basic PDFmorph Workflow ------------------------ - - 1. Open your Terminal or Command Prompt. - - 2. It it's not active already, activate your PDFmorph-equipped - conda environment by typing in :: - - source activate - - on Linux or ``activate `` on Windows. - - * If you need to list your available conda environments, - run the command ``conda info --envs`` or - ``conda env list`` - - * Run the ``pdfmorph --help`` command and read over the - info on that page for a brief overview of some of what we will - explore in this tutorial. - - 3. Using the ``mkdir`` command, create a directory where you'll - store the tutorial PDF files and use the ``cd`` command to change - into that directory. You can download the tutorial files - :download:`here <../../tests/testdata/tutorialData.zip>`. - Then, ``cd`` into the ``tutorialData`` directory. - - * The files in this dataset were collected by Soham Banerjee - at Brookhaven National Laboratory in Upton, New York. - - * The files are PDF data collected on Iridium Telluride with - 20% Rhodium Doping (IrRhTe2) with the first file (01) collected - at 10K and the last (44) at 300K. The samples increase in - temperature as their numbers increase. The "C" in their names - indicates that they have undergone cooling. - - * Note that these files have the ``.gr`` extension, which - indicates that they are measured PDFs. The ``.cgr`` file - extension indicates that a file is a calculated PDF, sich as - those generated by the `PDFgui `_ - program. - - 4. First, we will run the PDFmorph application without any morphing - and only using one PDF. Type the following command into your - Command Line :: - - pdfmorph darkSub_rh20_C_01.gr darkSub_rh20_C_01.gr - - This should produce two PDF curves which are congruent, resulting - in a flat green line underneath them. - - 5. Now, we will see PDFmorph run with two different PDFs and no - morphing. Type the following command into your command line :: - - pdfmorph darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - Without morphing, the difference Rw = 0.407. This indicates that - the two PDFs vary drastically. - - * While running the ``pdfmorph`` command, it is important - to remember that the first PDF file argument you provide - (in this case, ``darkSub_rh20_C_01.gr``) is the PDF which - will get morphed, while the second PDF file argument you - provide (here, ``darkSub_rh20_C_44.gr``) is the PDF which - acts as the model and does not get morphed. Hereinafter, - we will refer to the first PDF argument as the "objective" - and the second as the "reference", as the PDFmorph display - does. - - 6. Now, we will start the morphing process, which requires us to - provide initial guesses for our scaling factor, Gaussian smear, - and stretch, separately. We will start with the scaling factor. - Begin by typing the command :: - - pdfmorph --scale=2 -a darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - Now, the difference Rw = 1.457, a significant increase from our - value previously. We must modify our initial value for the - scaling factor and do so until we see a reduction in the - difference Rw from the unmorphed value. Type :: - - pdfmorph --scale=0.9 -a darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - The difference Rw is now 0.351, lower than our unmorphed - example's value. To see PDFmorph optimize the scale factor, - simply drop ``-a`` from the command and type :: - - pdfmorph --scale=0.9 darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - PDFmorph, given a reasonable initial guess, will use find the - optimal value for each morphing feature. Here, we see that - PDFmorph displays ``scale = 0.799025`` in the command prompt, - meaning that it has found this to be the most optimal value for - the scale factor. The difference Rw = 0.330, indicating a - better fit than our reasonable initial guess. - - * It is the choice of the user whether or not to run values - before removing ``-a`` when analyzing data with PDFmorph. - By including it, you allow the possibility to move towards - convergence before allowing the program to optimize by - removing it; when including it, you may reach a highly - optimized value on the first guess or diverge greatly. - In this tutorial, we will use it every time to check - for convergence. - - 7. Now, we will examine the Gaussian smearing factor. We provide an - initial guess by typing :: - - pdfmorph --scale=0.8 --smear=0.5 -a darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - And viewing the results. We've tailored our scale factor to be - close to the value given by PDFmorph, but see that the difference - Rw has increased substantially due to our smear value. One - approach, as described above, is to remove the ``-a`` from the - above command and run it again. - - * Note: The warnings that the Terminal/Command Prompt - displays are largely numerical in nature and do not - indicate a physically irrelevant guess. These are somewhat - superficial and in most cases can be ignored. - - We see that this has had hardly any effect on our PDF. To see - an effect, we restrict the ``rmin`` and ``rmax`` values to - reflect relevant data range by typing :: - - pdfmorph --scale=0.8 --smear=0.5 --rmin=1.5 --rmax=30 darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - Now, we see that the difference Rw = 0.204 and that the optimized - ``smear=-0.084138``. - - * We restricted the r values because some of the Gaussian - smear effects are only visible in a fixed r range. We - chose this r range by noting where most of our relevant - data was that was not exponentially decayed by - instrumental shortcomings. - - We are getting closer to an acceptably close fit to our data! - - 8. Finally, we will examine the stretch factor. Provide an intial - guess by typing :: - - pdfmorph --scale=0.8 --smear=-0.08 --stretch=0.5 --rmin=1.5 --rmax=30 -a darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - And noting that the difference has increased. Before continuing, - see if you can see which direction (higher or lower) our initial - estimate for the stretch factor needs to go and then removing - the ``-a`` to check optimized value! - - If you cannot, type :: - - pdfmorph --scale=0.8 --smear=-0.08 --stretch=0.005 --rmin=1.5 --rmax=30 -a darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr - - to observe decreased difference and then remove ``-a`` to see - the optimized ``--stretch=0.001762``. We have now reached - the optimal fit for our PDF! - - 9. Now, try it on your own! If you have personally collected or - otherwise readily available PDF data, try this process to see if - you can morph your PDFs to one another. Many of the parameters - provided in this tutorial are unique to it, so be cautious about - your choices and made sure that they remain physically relevant. - -Enjoy the software! - -.. Additional PDFmorph Functionality/Exploration -.. --------------------------------------------- - -.. TODO include undoped PDF example, phase changed PDFs, and nano/non-nano PDFs +Extra Tutorials +=============== +Add extra tutorials here Bug Reports ------------ +=========== Please enjoy using our software! If you come accross any bugs in the -application, please report them to diffpy-dev@googlegroups.com +application, please report them to diffpy-users@googlegroups.com. diff --git a/doc/source/release.rst b/doc/source/release.rst index ca8fa0b..8956c78 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -1,4 +1,3 @@ .. index:: release notes .. include:: /../../CHANGELOG.rst - diff --git a/news/TEMPLATE.rst b/news/TEMPLATE.rst new file mode 100644 index 0000000..790d30b --- /dev/null +++ b/news/TEMPLATE.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/cookie.rst b/news/cookie.rst new file mode 100644 index 0000000..bdf76b4 --- /dev/null +++ b/news/cookie.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**Changed:** + +* Support Python version 3.12 +* Remove support for Python version 3.9 + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Repo structure modified to the new diffpy standard +* Code linting based on .pre-commit-config.yaml + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1250363 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,72 @@ +[build-system] +requires = ["setuptools>=62.0", "setuptools-git-versioning<2"] +build-backend = "setuptools.build_meta" + +[project] +name = "diffpy.snmf" +dynamic=['version'] +authors = [ + { name="Simon J.L. Billinge group", email="simon.billinge@gmail.com" }, +] +maintainers = [ + { name="Simon J.L. Billinge group", email="simon.billinge@gmail.com" }, +] +description = "A python package implementing the stretched NMF algorithm." +keywords = ['diffpy', 'PDF'] +readme = "README.rst" +requires-python = ">=3.10" +classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Topic :: Scientific/Engineering :: Physics', + 'Topic :: Scientific/Engineering :: Chemistry', +] + +[project.urls] +Homepage = "https://github.com/diffpy/diffpy.snmf/" +Issues = "https://github.com/diffpy/diffpy.snmf/issues/" + +[tool.setuptools-git-versioning] +enabled = true +template = "{tag}" +dev_template = "{tag}" +dirty_template = "{tag}" + +[tool.setuptools.packages.find] +where = ["src"] # list of folders that contain the packages (["."] by default) +include = ["*"] # package names should match these glob patterns (["*"] by default) +exclude = ["diffpy.snmf.tests*"] # exclude packages matching these glob patterns (empty by default) +namespaces = false # to disable scanning PEP 420 namespaces (true by default) + +[tool.black] +line-length = 115 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.rst + | \.txt + | _build + | buck-out + | build + | dist + + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data +)/ +''' diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000..f72d870 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,2 @@ +python +setuptools diff --git a/requirements/docs.txt b/requirements/docs.txt index 661d44d..ab17b1c 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,4 +1,4 @@ -make +sphinx sphinx_rtd_theme +doctr m2r - diff --git a/doc/images/aspirin_stretch.png b/requirements/pip.txt similarity index 100% rename from doc/images/aspirin_stretch.png rename to requirements/pip.txt diff --git a/rever.xsh b/rever.xsh deleted file mode 100644 index 5932990..0000000 --- a/rever.xsh +++ /dev/null @@ -1,18 +0,0 @@ -$ACTIVITIES = [ - 'version_bump', # Changes the version number in various source files (setup.py, __init__.py, etc) - 'changelog', # Uses files in the news folder to create a changelog for release - 'tag', # Creates a tag for the new version number - #'pypi', # Sends the package to pypi - #'conda_forge', # Creates a PR into your package's feedstock - 'ghrelease' # Creates a Github release entry for the new tag - ] -$VERSION_BUMP_PATTERNS = [ # These note where/how to find the version numbers - ('snmf/__init__.py', '__version__\s*=.*', "__version__ = '$VERSION'"), - ('setup.py', 'version\s*=.*,', "version='$VERSION',") - ] -$CHANGELOG_FILENAME = 'CHANGELOG.rst' # Filename for the changelog -$CHANGELOG_TEMPLATE = 'TEMPLATE.rst' # Filename for the news template -$TAG_REMOTE = 'https://github.com/diffpy/snmf.git' # Repo to push tags to - -$GITHUB_ORG = 'diffpy' # Github org for Github releases and conda-forge -$GITHUB_REPO = 'diffpy.snmf' # Github repo for Github releases and conda-forge diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 108efd3..0000000 --- a/run_tests.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -import sys -import pytest - -if __name__ == '__main__': - # show output results from every test function - args = ['-v'] - # show the message output for skipped and expected failure tests - if len(sys.argv) > 1: - args.extend(sys.argv[1:]) - print('pytest arguments: {}'.format(args)) - # # compute coverage stats for xpdAcq - # call pytest and exit with the return code from pytest so that - # travis will fail correctly if tests fail - exit_res = pytest.main(args) - sys.exit(exit_res) - diff --git a/setup.py b/setup.py deleted file mode 100644 index 4e022e3..0000000 --- a/setup.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python - -# Installation script for diffpy.snmf - -"""diffpy.snmf - a package implementing the stretched NMF algorithm. - -Packages: diffpy.snmf -""" - -import os -from setuptools import setup, find_packages - - -MYDIR = os.path.dirname(os.path.abspath(__file__)) - -# with open(os.path.join(MYDIR, 'requirements/run.txt')) as fp: -# requirements = [line.strip() for line in fp] - -with open(os.path.join(MYDIR, 'README.md')) as fp: - long_description = fp.read() - - -# define distribution -setup( - name="diffpy.snmf", - version='0.0.1', - packages=find_packages(exclude=['tests', 'applications']), - entry_points={ - # define console_scripts here, see setuptools docs for details. - 'console_scripts': [ - 'snmf = diffpy.snmf.stretchednmfapp:main', - ], - }, - test_suite='tests', - # install_requires=requirements, - author='Ran Gu, Simon J.L. Billinge', - author_email='sb2896@columbia.edu', - maintainer='Simon J.L. Billinge', - maintainer_email='sb2896@columbia.edu', - url='https://github.com/diffpy/diffpy.snmf', - description="A python package implementing the stretched NMF algorithm.", - long_description = long_description, - long_description_content_type = 'text/x-rst', - license='BSD', - keywords="diffpy PDF", - classifiers = [ - # List of possible values at - # http://pypi.python.org/pypi?:action=list_classifiers - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Scientific/Engineering :: Chemistry', - 'Topic :: Scientific/Engineering :: Physics', - ], -) - -# End of file diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py new file mode 100644 index 0000000..2baa1ea --- /dev/null +++ b/src/diffpy/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2024 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Billinge Group members and community contributors. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.snmf/graphs/contributors +# +# See LICENSE.rst for license information. +# +############################################################################## + +"""Blank namespace package for module diffpy.""" + + +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) + +# End of file diff --git a/src/diffpy/snmf/__init__.py b/src/diffpy/snmf/__init__.py new file mode 100644 index 0000000..9c7220b --- /dev/null +++ b/src/diffpy/snmf/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2024 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Billinge Group members and community contributors. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.snmf/graphs/contributors +# +# See LICENSE.rst for license information. +# +############################################################################## + +"""A python package implementing the stretched NMF algorithm.""" + +# package version +from diffpy.snmf.version import __version__ + +# silence the pyflakes syntax checker +assert __version__ or True + +# End of file diff --git a/diffpy/snmf/containers.py b/src/diffpy/snmf/containers.py similarity index 82% rename from diffpy/snmf/containers.py rename to src/diffpy/snmf/containers.py index e25b8d5..1af960a 100644 --- a/diffpy/snmf/containers.py +++ b/src/diffpy/snmf/containers.py @@ -1,5 +1,5 @@ -import numpy as np import numdifftools +import numpy as np class ComponentSignal: @@ -40,17 +40,21 @@ def apply_stretch(self, m): stretching operation, and one vector is the second derivative of the stretching operation. """ normalized_grid = np.arange(len(self.grid)) - func = lambda stretching_factor: np.interp(normalized_grid / stretching_factor, normalized_grid, self.iq, - left=0, right=0) - derivative_func = numdifftools.Derivative(func) + interpolate_intensity = lambda stretching_factor: np.interp( # noqa: E731 + normalized_grid / stretching_factor, normalized_grid, self.iq, left=0, right=0 + ) + derivative_func = numdifftools.Derivative(interpolate_intensity) second_derivative_func = numdifftools.Derivative(derivative_func) - stretched_component = func(self.stretching_factors[m]) + stretched_component = interpolate_intensity(self.stretching_factors[m]) stretched_component_gra = derivative_func(self.stretching_factors[m]) stretched_component_hess = second_derivative_func(self.stretching_factors[m]) - return np.asarray(stretched_component), np.asarray(stretched_component_gra), np.asarray( - stretched_component_hess) + return ( + np.asarray(stretched_component), + np.asarray(stretched_component_gra), + np.asarray(stretched_component_hess), + ) def apply_weight(self, m, stretched_component=None): """Applies as weight factor to a component signal. diff --git a/diffpy/snmf/factorizers.py b/src/diffpy/snmf/factorizers.py similarity index 64% rename from diffpy/snmf/factorizers.py rename to src/diffpy/snmf/factorizers.py index ef6b2c0..b4620fd 100644 --- a/diffpy/snmf/factorizers.py +++ b/src/diffpy/snmf/factorizers.py @@ -5,27 +5,26 @@ def lsqnonneg(stretched_component_matrix, target_signal): """Finds the weights of stretched component signals under one-sided constraint. - Solves ``argmin_x || Ax - b ||_2`` for ``x>=0`` where A is the stretched_component_matrix and b is the target_signal - vector. Finds the weights of component signals given undecomposed signal data and stretched components under a - one-sided constraint on the weights. + Solves ``argmin_x || Ax - b ||_2`` for ``x>=0`` where A is the stretched_component_matrix and b is the + target_signal vector. Finds the weights of component signals given undecomposed signal data and stretched + components under a one-sided constraint on the weights. Parameters ---------- stretched_component_matrix: 2d array like The component matrix where each column contains a stretched component signal. Has dimensions R x C where R is - the length of the signal and C is the number of components. Does not need to be nonnegative. Corresponds with 'A' - from the objective function. + the length of the signal and C is the number of components. Does not need to be nonnegative. Corresponds with + 'A' from the objective function. target_signal: 1d array like - The signal that is used as reference against which weight factors will be determined. Any column from the matrix - of the entire, unfactorized input data could be used. Has length R. Does not need to be nonnegative. Corresponds - with 'b' from the objective function. + The signal that is used as reference against which weight factors will be determined. Any column from the + matrix of the entire, unfactorized input data could be used. Has length R. Does not need to be nonnegative. + Corresponds with 'b' from the objective function. Returns ------- 1d array like The vector containing component signal weights at a moment. Has length C. - """ stretched_component_matrix = np.asarray(stretched_component_matrix) target_signal = np.asarray(target_signal) diff --git a/diffpy/snmf/io.py b/src/diffpy/snmf/io.py similarity index 56% rename from diffpy/snmf/io.py rename to src/diffpy/snmf/io.py index 6063569..12eb1b2 100644 --- a/diffpy/snmf/io.py +++ b/src/diffpy/snmf/io.py @@ -1,6 +1,8 @@ +from pathlib import Path + import numpy as np import scipy.sparse -from pathlib import Path + from diffpy.utils.parsers.loaddata import loadData @@ -10,8 +12,8 @@ def initialize_variables(data_input, number_of_components, data_type, sparsity=1 Parameters ---------- data_input: 2d array like - The observed or simulated PDF or XRD data provided by the user. Has dimensions R x N where R is the signal length - and N is the number of PDF/XRD signals. + The observed or simulated PDF or XRD data provided by the user. Has dimensions R x N where R is the signa + length and N is the number of PDF/XRD signals. number_of_components: int The number of component signals the user would like to decompose 'data_input' into. @@ -20,33 +22,43 @@ def initialize_variables(data_input, number_of_components, data_type, sparsity=1 The type of data the user has passed into the program. Can assume the value of 'PDF' or 'XRD.' sparsity: float, optional - The regularization parameter that behaves as the coefficient of a "sparseness" regularization term that enhances - the ability to decompose signals in the case of sparse data e.g. X-ray Diffraction data. A non-zero value - indicates sparsity in the data; greater magnitudes indicate greater amounts of sparsity. + The regularization parameter that behaves as the coefficient of a "sparseness" regularization term that + enhances the ability to decompose signals in the case of sparse data e.g. X-ray Diffraction data. + A non-zero value indicates sparsity in the data; greater magnitudes indicate greater amounts of sparsity. smoothness: float, optional - The regularization parameter that behaves as the coefficient of a "smoothness" term that ensures that component - signal weightings change smoothly with time. Assumes a default value of 1e18. + The regularization parameter that behaves as the coefficient of a "smoothness" term that ensures that + component signal weightings change smoothly with time. Assumes a default value of 1e18. Returns ------- dictionary - The collection of the names and values of the constants used in the algorithm. Contains the number of observed PDF - /XRD patterns, the length of each pattern, the type of the data, the number of components the user would like to - decompose the data into, an initial guess for the component matrix, and initial guess for the weight factor matrix - ,an initial guess for the stretching factor matrix, a parameter controlling smoothness of the solution, a - parameter controlling sparseness of the solution, the matrix representing the smoothness term, and a matrix used - to construct a hessian matrix. + The collection of the names and values of the constants used in the algorithm. Contains the number of + observed PDF/XRD patterns, the length of each pattern, the type of the data, the number of components + the user would like to decompose the data into, an initial guess for the component matrix, and initial + guess for the weight factor matrix, an initial guess for the stretching factor matrix, a parameter + controlling smoothness of the solution, a parameter controlling sparseness of the solution, the matrix + representing the smoothness term, and a matrix used to construct a hessian matrix. """ signal_length = data_input.shape[0] number_of_signals = data_input.shape[1] - diagonals = [np.ones(number_of_signals - 2), -2 * np.ones(number_of_signals - 2), np.ones(number_of_signals - 2)] - smoothness_term = .25 * scipy.sparse.diags(diagonals, [0, 1, 2], shape=(number_of_signals - 2, number_of_signals)) + diagonals = [ + np.ones(number_of_signals - 2), + -2 * np.ones(number_of_signals - 2), + np.ones(number_of_signals - 2), + ] + smoothness_term = 0.25 * scipy.sparse.diags( + diagonals, [0, 1, 2], shape=(number_of_signals - 2, number_of_signals) + ) hessian_helper_matrix = scipy.sparse.block_diag([smoothness_term.T @ smoothness_term] * number_of_components) - sequence = np.arange(number_of_signals * number_of_components).reshape(number_of_components, number_of_signals).T.flatten() + sequence = ( + np.arange(number_of_signals * number_of_components) + .reshape(number_of_components, number_of_signals) + .T.flatten() + ) hessian_helper_matrix = hessian_helper_matrix[sequence, :][:, sequence] return { @@ -57,29 +69,29 @@ def initialize_variables(data_input, number_of_components, data_type, sparsity=1 "smoothness": smoothness, "sparsity": sparsity, "smoothness_term": smoothness_term, - "hessian_helper_matrix": hessian_helper_matrix + "hessian_helper_matrix": hessian_helper_matrix, } def load_input_signals(file_path=None): """Processes a directory of a series of PDF/XRD patterns into a usable format. - Constructs a 2d array out of a directory of PDF/XRD patterns containing each files dependent variable column in a - new column. Constructs a 1d array containing the grid values. + Constructs a 2d array out of a directory of PDF/XRD patterns containing each files dependent variable + column in a new column. Constructs a 1d array containing the grid values. Parameters ---------- file_path: str or Path object, optional - The path to the directory containing the input XRD/PDF data. If no path is specified, defaults to the current - working directory. Accepts a string or a pathlib.Path object. Input data not on the same grid as the first file - read will be ignored. + The path to the directory containing the input XRD/PDF data. If no path is specified, defaults to the + current working directory. Accepts a string or a pathlib.Path object. Input data not on the same grid + as the first file read will be ignored. Returns ------- tuple - The tuple whose first element is an R x M 2d array made of PDF/XRD patterns as each column; R is the length of the - signal and M is the number of patterns. The tuple contains a 1d array containing the values of the grid points as - its second element; Has length R. + The tuple whose first element is an R x M 2d array made of PDF/XRD patterns as each column; R is the + length of the signal and M is the number of patterns. The tuple contains a 1d array containing the values + of the grid points as its second element; Has length R. """ diff --git a/diffpy/snmf/optimizers.py b/src/diffpy/snmf/optimizers.py similarity index 58% rename from diffpy/snmf/optimizers.py rename to src/diffpy/snmf/optimizers.py index 722c102..4198907 100644 --- a/diffpy/snmf/optimizers.py +++ b/src/diffpy/snmf/optimizers.py @@ -1,37 +1,39 @@ -import numpy as np import cvxpy +import numpy as np def get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound): """Finds the weights of stretched component signals under a two-sided constraint - Solves min J(y) = (linear_coefficient)' * y + (1/2) * y' * (quadratic coefficient) * y where lower_bound <= y <= - upper_bound and stretched_component_gram_matrix is symmetric positive definite. Finds the weightings of stretched - component signals under a two-sided constraint. + Solves min J(y) = (linear_coefficient)' * y + (1/2) * y' * (quadratic coefficient) * y where + lower_bound <= y <= upper_bound and stretched_component_gram_matrix is symmetric positive definite. + Finds the weightings of stretched component signals under a two-sided constraint. Parameters ---------- stretched_component_gram_matrix: 2d array like - The Gram matrix constructed from the stretched component matrix. It is a square positive definite matrix. It has - dimensions C x C where C is the number of component signals. Must be symmetric positive definite. + The Gram matrix constructed from the stretched component matrix. It is a square positive definite matrix. + It has dimensions C x C where C is the number of component signals. Must be symmetric positive definite. linear_coefficient: 1d array like - The vector containing the product of the stretched component matrix and the transpose of the observed data matrix. - Has length C. + The vector containing the product of the stretched component matrix and the transpose of the observed + data matrix. Has length C. lower_bound: 1d array like - The lower bound on the values of the output weights. Has the same dimensions of the function output. Each - element in 'lower_bound' determines the minimum value the corresponding element in the function output may take. + The lower bound on the values of the output weights. Has the same dimensions of the function output. Each + element in 'lower_bound' determines the minimum value the corresponding element in the function output may + take. upper_bound: 1d array like - The upper bound on the values of the output weights. Has the same dimensions of the function output. Each element - in 'upper_bound' determines the maximum value the corresponding element in the function output may take. + The upper bound on the values of the output weights. Has the same dimensions of the function output. Each + element in 'upper_bound' determines the maximum value the corresponding element in the function output may + take. Returns ------- 1d array like - The vector containing the weightings of the components needed to reconstruct a given input signal from the input - set. Has length C + The vector containing the weightings of the components needed to reconstruct a given input signal from the + input set. Has length C """ stretched_component_gram_matrix = np.asarray(stretched_component_gram_matrix) @@ -43,8 +45,9 @@ def get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound solution_variable = cvxpy.Variable(problem_size) objective = cvxpy.Minimize( - linear_coefficient.T @ solution_variable + 0.5 * cvxpy.quad_form(solution_variable, - stretched_component_gram_matrix)) + linear_coefficient.T @ solution_variable + + 0.5 * cvxpy.quad_form(solution_variable, stretched_component_gram_matrix) + ) constraints = [lower_bound <= solution_variable, solution_variable <= upper_bound] cvxpy.Problem(objective, constraints).solve() diff --git a/diffpy/snmf/polynomials.py b/src/diffpy/snmf/polynomials.py similarity index 89% rename from diffpy/snmf/polynomials.py rename to src/diffpy/snmf/polynomials.py index 3c8d5dd..265be05 100644 --- a/diffpy/snmf/polynomials.py +++ b/src/diffpy/snmf/polynomials.py @@ -3,7 +3,8 @@ def rooth(linear_coefficient, constant_term): """ - Returns the largest real root of x^3+(linear_coefficient) * x + constant_term. If there are no real roots return 0. + Returns the largest real root of x^3+(linear_coefficient) * x + constant_term. If there are no real roots + return 0. Parameters ---------- @@ -15,7 +16,8 @@ def rooth(linear_coefficient, constant_term): Returns ------- ndarray of floats - The largest real root of x^3+(linear_coefficient) * x + constant_term if roots are real, else return 0 array + The largest real root of x^3+(linear_coefficient) * x + constant_term if roots are real, else + return 0 array """ diff --git a/src/diffpy/snmf/stretchednmfapp.py b/src/diffpy/snmf/stretchednmfapp.py new file mode 100644 index 0000000..7080e0c --- /dev/null +++ b/src/diffpy/snmf/stretchednmfapp.py @@ -0,0 +1,60 @@ +import argparse +from pathlib import Path + +from diffpy.snmf.io import initialize_variables, load_input_signals +from diffpy.snmf.subroutines import initialize_components, lift_data + +ALLOWED_DATA_TYPES = ["powder_diffraction", "pd", "pair_distribution_function", "pdf"] + + +def create_parser(): + parser = argparse.ArgumentParser( + prog="stretched_nmf", description="Stretched Nonnegative Matrix Factorization" + ) + parser.add_argument( + "-i", + "--input-directory", + type=str, + default=None, + help="Directory containing experimental data. Defaults to current working directory.", + ) + parser.add_argument( + "-o", + "--output-directory", + type=str, + help="The directory where the results will be written. Defaults to '/snmf_results'.", + ) + parser.add_argument( + "t", + "--data-type", + type=str, + default=None, + choices=ALLOWED_DATA_TYPES, + help="The type of the experimental data.", + ) + parser.add_argument( + "-l", + "--lift-factor", + type=float, + default=1, + help="The lifting factor. Data will be lifted by lifted_data = data + abs(min(data) * lift). Default 1.", + ) + parser.add_argument( + "number-of-components", + type=int, + help="The number of component signals for the NMF decomposition. Must be an integer greater than 0", + ) + parser.add_argument("-v", "--version", action="version", help="Print the software version number") + args = parser.parse_args() + return args + + +def main(): + args = create_parser() + if args.input_directory is None: + args.input_directory = Path.cwd() + grid, input_data = load_input_signals(args.input_directory) + lifted_input_data = lift_data(input_data, args.lift_factor) + variables = initialize_variables(lifted_input_data, args.number_of_components, args.data_type) + components = initialize_components(variables["number_of_components"], variables["number_of_signals"], grid) + return components diff --git a/diffpy/snmf/subroutines.py b/src/diffpy/snmf/subroutines.py similarity index 73% rename from diffpy/snmf/subroutines.py rename to src/diffpy/snmf/subroutines.py index c0052bb..e4f6a5f 100644 --- a/diffpy/snmf/subroutines.py +++ b/src/diffpy/snmf/subroutines.py @@ -1,12 +1,13 @@ +import numdifftools import numpy as np -from diffpy.snmf.optimizers import get_weights -from diffpy.snmf.factorizers import lsqnonneg + from diffpy.snmf.containers import ComponentSignal -import numdifftools +from diffpy.snmf.factorizers import lsqnonneg +from diffpy.snmf.optimizers import get_weights def initialize_components(number_of_components, number_of_signals, grid_vector): - """Initializes ComponentSignals for each of the components in the decomposition + """Initializes ComponentSignals for each of the components in the decomposition. Parameters ---------- @@ -31,15 +32,15 @@ def initialize_components(number_of_components, number_of_signals, grid_vector): def lift_data(data_input, lift=1): - """Lifts values of data_input + """Lifts values of data_input. Adds 'lift' * the minimum value in data_input to data_input element-wise. Parameters ---------- data_input: 2d array like - The matrix containing a series of signals to be decomposed. Has dimensions N x M where N is the length of each - signal and M is the number of signals. + The matrix containing a series of signals to be decomposed. Has dimensions N x M where N is the length + of each signal and M is the number of signals. lift: float The factor representing how much to lift 'data_input'. @@ -48,14 +49,13 @@ def lift_data(data_input, lift=1): ------- 2d array like The matrix that contains data_input - (min(data_input) * lift). - """ data_input = np.asarray(data_input) return data_input + np.abs(np.min(data_input) * lift) def construct_stretching_matrix(components, number_of_components, number_of_signals): - """Constructs the stretching factor matrix + """Constructs the stretching factor matrix. Parameters ---------- @@ -67,9 +67,8 @@ def construct_stretching_matrix(components, number_of_components, number_of_sign Returns ------- 2d array - The matrix containing the stretching factors for the component signals for each of the signals in the raw data. - Has dimensions `component_signal` x `number_of_signals` - + The matrix containing the stretching factors for the component signals for each of the signals in the + raw data. Has dimensions `component_signal` x `number_of_signals` """ if (len(components)) == 0: raise ValueError(f"Number of components = {number_of_components}. Number_of_components must be >= 1.") @@ -85,7 +84,7 @@ def construct_stretching_matrix(components, number_of_components, number_of_sign def construct_component_matrix(components): - """Constructs the component matrix + """Constructs the component matrix. Parameters ---------- @@ -96,7 +95,6 @@ def construct_component_matrix(components): ------- 2d array The matrix containing the component signal values. Has dimensions `signal_length` x `number_of_components`. - """ signal_length = len(components[0].iq) number_of_components = len(components) @@ -112,7 +110,7 @@ def construct_component_matrix(components): def construct_weight_matrix(components): - """Constructs the weights matrix + """Constructs the weights matrix. Constructs a Ķ x M matrix where K is the number of components and M is the number of signals. Each element is the stretching factor for a specific @@ -150,16 +148,16 @@ def update_weights(components, data_input, method=None): components: tuple of ComponentSignal objects The tuple containing the component signals. method: str - The string specifying which method should be used to find a new weight matrix: non-negative least squares or a - quadratic program. + The string specifying which method should be used to find a new weight matrix: non-negative least squares + or a quadratic program. data_input: 2d array The 2d array containing the user-provided signals. Returns ------- 2d array - The 2d array containing the weight factors for each component for each signal from `data_input`. Has dimensions - K x M where K is the number of components and M is the number of signals in `data_input.` + The 2d array containing the weight factors for each component for each signal from `data_input`. + Has dimensions K x M where K is the number of components and M is the number of signals in `data_input.` """ data_input = np.asarray(data_input) weight_matrix = construct_weight_matrix(components) @@ -170,11 +168,15 @@ def update_weights(components, data_input, method=None): stretched_components = np.zeros((signal_length, number_of_components)) for i, component in enumerate(components): stretched_components[:, i] = component.apply_stretch(signal)[0] - if method == 'align': + if method == "align": weights = lsqnonneg(stretched_components, data_input[:, signal]) else: - weights = get_weights(stretched_components.T @ stretched_components, - -stretched_components.T @ data_input[:, signal], 0, 1) + weights = get_weights( + stretched_components.T @ stretched_components, + -stretched_components.T @ data_input[:, signal], + 0, + 1, + ) weight_matrix[:, signal] = weights return weight_matrix @@ -196,7 +198,6 @@ def reconstruct_signal(components, signal_idx): ------- 1d array like The reconstruction of a signal from calculated weights, stretching factors, and iq values. - """ signal_length = len(components[0].grid) reconstruction = np.zeros(signal_length) @@ -208,13 +209,14 @@ def reconstruct_signal(components, signal_idx): def initialize_arrays(number_of_components, number_of_moments, signal_length): - """Generates the initial guesses for the weight, stretching, and component matrices + """Generates the initial guesses for the weight, stretching, and component matrices. - Calculates the initial guesses for the component matrix, stretching factor matrix, and weight matrix. The initial - guess for the component matrix is a random (signal_length) x (number_of_components) matrix where each element is - between 0 and 1. The initial stretching factor matrix is a random (number_of_components) x (number_of_moments) - matrix where each element is number slightly perturbed from 1. The initial weight matrix guess is a random - (number_of_components) x (number_of_moments) matrix where each element is between 0 and 1. + Calculates the initial guesses for the component matrix, stretching factor matrix, and weight matrix. The + initial guess for the component matrix is a random (signal_length) x (number_of_components) matrix where + each element is between 0 and 1. The initial stretching factor matrix is a random + (number_of_components) x (number_of_moments) matrix where each element is number slightly perturbed from 1. + The initial weight matrix guess is a random (number_of_components) x (number_of_moments) matrix where + each element is between 0 and 1. Parameters ---------- @@ -232,17 +234,19 @@ def initialize_arrays(number_of_components, number_of_moments, signal_length): tuple of 2d arrays of floats The tuple containing three elements: the initial component matrix guess, the initial stretching factor matrix guess, and the initial weight factor matrix guess in that order. - """ component_matrix_guess = np.random.rand(signal_length, number_of_components) weight_matrix_guess = np.random.rand(number_of_components, number_of_moments) - stretching_matrix_guess = np.ones(number_of_components, number_of_moments) + np.random.randn(number_of_components, - number_of_moments) * 1e-3 + stretching_matrix_guess = ( + np.ones(number_of_components, number_of_moments) + + np.random.randn(number_of_components, number_of_moments) * 1e-3 + ) return component_matrix_guess, weight_matrix_guess, stretching_matrix_guess -def objective_function(residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, - sparsity): +def objective_function( + residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, sparsity +): """Defines the objective function of the algorithm and returns its value. Calculates the value of '(||residual_matrix||_F) ** 2 + smoothness * (||smoothness_term * @@ -251,25 +255,26 @@ def objective_function(residual_matrix, stretching_factor_matrix, smoothness, sm Parameters ---------- residual_matrix: 2d array like - The matrix where each column is the difference between an experimental PDF/XRD pattern and a calculated PDF/XRD - pattern at each grid point. Has dimensions R x M where R is the length of each pattern and M is the amount of - patterns. + The matrix where each column is the difference between an experimental PDF/XRD pattern and a calculated + PDF/XRD pattern at each grid point. Has dimensions R x M where R is the length of each pattern and M is + the amount of patterns. stretching_factor_matrix: 2d array like - The matrix containing the stretching factors of the calculated component signal. Has dimensions K x M where K is - the amount of components and M is the number of experimental PDF/XRD patterns. + The matrix containing the stretching factors of the calculated component signal. Has dimensions K x M where + K is the amount of components and M is the number of experimental PDF/XRD patterns. smoothness: float - The coefficient of the smoothness term which determines the intensity of the smoothness term and its behavior. - It is not very sensitive and is usually adjusted by multiplying it by ten. + The coefficient of the smoothness term which determines the intensity of the smoothness term and its + behavior. It is not very sensitive and is usually adjusted by multiplying it by ten. smoothness_term: 2d array like The regularization term that ensures that smooth changes in the component stretching signals are favored. - Has dimensions (M-2) x M where M is the amount of experimentally obtained PDF/XRD patterns, the moment amount. + Has dimensions (M-2) x M where M is the amount of experimentally obtained PDF/XRD patterns, the moment + amount. component_matrix: 2d array like - The matrix containing the calculated component signals of the experimental PDF/XRD patterns. Has dimensions R x K - where R is the signal length and K is the number of component signals. + The matrix containing the calculated component signals of the experimental PDF/XRD patterns. Has dimensions + R x K where R is the signal length and K is the number of component signals. sparsity: float The parameter determining the intensity of the sparsity regularization term which enables the algorithm to @@ -279,22 +284,24 @@ def objective_function(residual_matrix, stretching_factor_matrix, smoothness, sm ------- float The value of the objective function. - """ residual_matrix = np.asarray(residual_matrix) stretching_factor_matrix = np.asarray(stretching_factor_matrix) component_matrix = np.asarray(component_matrix) - return .5 * np.linalg.norm(residual_matrix, 'fro') ** 2 + .5 * smoothness * np.linalg.norm( - smoothness_term @ stretching_factor_matrix.T, 'fro') ** 2 + sparsity * np.sum(np.sqrt(component_matrix)) + return ( + 0.5 * np.linalg.norm(residual_matrix, "fro") ** 2 + + 0.5 * smoothness * np.linalg.norm(smoothness_term @ stretching_factor_matrix.T, "fro") ** 2 + + sparsity * np.sum(np.sqrt(component_matrix)) + ) def get_stretched_component(stretching_factor, component, signal_length): """Applies a stretching factor to a component signal. - Computes a stretched signal and reinterpolates it onto the original grid of points. Uses a normalized grid of evenly - spaced integers counting from 0 to signal_length (exclusive) to approximate values in between grid nodes. Once this - grid is stretched, values at grid nodes past the unstretched signal's domain are set to zero. Returns the - approximate values of x(r/a) from x(r) where x is a component signal. + Computes a stretched signal and reinterpolates it onto the original grid of points. Uses a normalized grid + of evenly spaced integers counting from 0 to signal_length (exclusive) to approximate values in between grid + nodes. Once this grid is stretched, values at grid nodes past the unstretched signal's domain are set to zero. + Returns the approximate values of x(r/a) from x(r) where x is a component signal. Parameters ---------- @@ -310,7 +317,6 @@ def get_stretched_component(stretching_factor, component, signal_length): tuple of 1d array of floats The calculated component signal with stretching factors applied. Has length N, the length of the unstretched component signal. Also returns the gradient and hessian of the stretching transformation. - """ component = np.asarray(component) normalized_grid = np.arange(signal_length) @@ -325,11 +331,23 @@ def stretched_component_func(stretching_factor): stretched_component_gra = derivative_func(stretching_factor) stretched_component_hess = second_derivative_func(stretching_factor) - return np.asarray(stretched_component), np.asarray(stretched_component_gra), np.asarray(stretched_component_hess) - - -def update_weights_matrix(component_amount, signal_length, stretching_factor_matrix, component_matrix, data_input, - moment_amount, weights_matrix, method): + return ( + np.asarray(stretched_component), + np.asarray(stretched_component_gra), + np.asarray(stretched_component_hess), + ) + + +def update_weights_matrix( + component_amount, + signal_length, + stretching_factor_matrix, + component_matrix, + data_input, + moment_amount, + weights_matrix, + method, +): """Update the weight factors matrix. Parameters @@ -341,23 +359,23 @@ def update_weights_matrix(component_amount, signal_length, stretching_factor_mat The length of the experimental signal patterns stretching_factor_matrix: 2d array like - The matrx containing the stretching factors of the calculated component signals. Has dimensions K x M where K is - the number of component signals and M is the number of XRD/PDF patterns. + The matrx containing the stretching factors of the calculated component signals. Has dimensions K x M + where K is the number of component signals and M is the number of XRD/PDF patterns. - component_matrix: 2d array like - The matrix containing the unstretched calculated component signals. Has dimensions N x K where N is the length of - the signals and K is the number of component signals. + component_matrix: 2d array lik + The matrix containing the unstretched calculated component signals. Has dimensions N x K where N is the + length of the signals and K is the number of component signals. data_input: 2d array like - The experimental series of PDF/XRD patterns. Has dimensions N x M where N is the length of the PDF/XRD signals and - M is the number of PDF/XRD patterns. + The experimental series of PDF/XRD patterns. Has dimensions N x M where N is the length of the PDF/XRD + signals and M is the number of PDF/XRD patterns. moment_amount: int The number of PDF/XRD patterns from the experimental data. weights_matrix: 2d array like - The matrix containing the weights of the stretched component signals. Has dimensions K x M where K is the number - of component signals and M is the number of XRD/PDF patterns. + The matrix containing the weights of the stretched component signals. Has dimensions K x M where K is + the number of component signals and M is the number of XRD/PDF patterns. method: str The string specifying the method for obtaining individual weights. @@ -366,7 +384,6 @@ def update_weights_matrix(component_amount, signal_length, stretching_factor_mat ------- 2d array like The matrix containing the new weight factors of the stretched component signals. - """ stretching_factor_matrix = np.asarray(stretching_factor_matrix) component_matrix = np.asarray(component_matrix) @@ -376,44 +393,48 @@ def update_weights_matrix(component_amount, signal_length, stretching_factor_mat for i in range(moment_amount): stretched_components = np.zeros((signal_length, component_amount)) for n in range(component_amount): - stretched_components[:, n] = get_stretched_component(stretching_factor_matrix[n, i], component_matrix[:, n], - signal_length)[0] - if method == 'align': + stretched_components[:, n] = get_stretched_component( + stretching_factor_matrix[n, i], component_matrix[:, n], signal_length + )[0] + if method == "align": weight = lsqnonneg(stretched_components[0:signal_length, :], data_input[0:signal_length, i]) else: weight = get_weights( stretched_components[0:signal_length, :].T @ stretched_components[0:signal_length, :], -1 * stretched_components[0:signal_length, :].T @ data_input[0:signal_length, i], - 0, 1) + 0, + 1, + ) weights_matrix[:, i] = weight return weights_matrix -def get_residual_matrix(component_matrix, weights_matrix, stretching_matrix, data_input, moment_amount, - component_amount, signal_length): - """Obtains the residual matrix between the experimental data and calculated data +def get_residual_matrix( + component_matrix, weights_matrix, stretching_matrix, data_input, moment_amount, component_amount, signal_length +): + """Obtains the residual matrix between the experimental data and calculated data. - Calculates the difference between the experimental data and the reconstructed experimental data created from the - calculated components, weights, and stretching factors. For each experimental pattern, the stretched and weighted - components making up that pattern are subtracted. + Calculates the difference between the experimental data and the reconstructed experimental data created from + the calculated components, weights, and stretching factors. For each experimental pattern, the stretched and + weighted components making up that pattern are subtracted. Parameters ---------- component_matrix: 2d array like - The matrix containing the calculated component signals. Has dimensions N x K where N is the length of the signal - and K is the number of calculated component signals. + The matrix containing the calculated component signals. Has dimensions N x K where N is the length of the + signal and K is the number of calculated component signals. weights_matrix: 2d array like - The matrix containing the calculated weights of the stretched component signals. Has dimensions K x M where K is - the number of components and M is the number of moments or experimental PDF/XRD patterns. + The matrix containing the calculated weights of the stretched component signals. Has dimensions K x M where + K is the number of components and M is the number of moments or experimental PDF/XRD patterns. stretching_matrix: 2d array like - The matrix containing the calculated stretching factors of the calculated component signals. Has dimensions K x M - where K is the number of components and M is the number of moments or experimental PDF/XRD patterns. + The matrix containing the calculated stretching factors of the calculated component signals. Has dimensions + K x M where K is the number of components and M is the number of moments or experimental PDF/XRD patterns. data_input: 2d array like - The matrix containing the experimental PDF/XRD data. Has dimensions N x M where N is the length of the signals and - M is the number of signal patterns. + The matrix containing the experimental PDF/XRD data. Has dimensions N x M where N is the length of the + signals and M is the number of signal patterns. moment_amount: int The number of patterns in the experimental data. Represents the number of moments in time in the data series @@ -428,11 +449,10 @@ def get_residual_matrix(component_matrix, weights_matrix, stretching_matrix, dat Returns ------- 2d array like - The matrix containing the residual between the experimental data and reconstructed data from calculated values. - Has dimensions N x M where N is the signal length and M is the number of moments. Each column contains the - difference between an experimental signal and a reconstruction of that signal from the calculated weights, - components, and stretching factors. - + The matrix containing the residual between the experimental data and reconstructed data from calculated + values. Has dimensions N x M where N is the signal length and M is the number of moments. Each column + contains the difference between an experimental signal and a reconstruction of that signal from the + calculated weights, components, and stretching factors. """ component_matrix = np.asarray(component_matrix) weights_matrix = np.asarray(weights_matrix) @@ -442,15 +462,17 @@ def get_residual_matrix(component_matrix, weights_matrix, stretching_matrix, dat for m in range(moment_amount): residual = residual_matrx[:, m] for k in range(component_amount): - residual = residual + weights_matrix[k, m] * get_stretched_component(stretching_matrix[k, m], - component_matrix[:, k], signal_length)[ - 0] + residual = ( + residual + + weights_matrix[k, m] + * get_stretched_component(stretching_matrix[k, m], component_matrix[:, k], signal_length)[0] + ) residual_matrx[:, m] = residual return residual_matrx def reconstruct_data(components): - """Reconstructs the `input_data` matrix + """Reconstructs the `input_data` matrix. Reconstructs the `input_data` matrix from calculated component signals, weights, and stretching factors. @@ -463,7 +485,6 @@ def reconstruct_data(components): ------- 2d array The 2d array containing the reconstruction of input_data. - """ signal_length = len(components[0].iq) number_of_signals = len(components[0].weights) diff --git a/doc/images/morph_ex1.png b/src/diffpy/snmf/tests/__init__.py similarity index 100% rename from doc/images/morph_ex1.png rename to src/diffpy/snmf/tests/__init__.py diff --git a/src/diffpy/snmf/tests/conftest.py b/src/diffpy/snmf/tests/conftest.py new file mode 100644 index 0000000..e3b6313 --- /dev/null +++ b/src/diffpy/snmf/tests/conftest.py @@ -0,0 +1,19 @@ +import json +from pathlib import Path + +import pytest + + +@pytest.fixture +def user_filesystem(tmp_path): + base_dir = Path(tmp_path) + home_dir = base_dir / "home_dir" + home_dir.mkdir(parents=True, exist_ok=True) + cwd_dir = base_dir / "cwd_dir" + cwd_dir.mkdir(parents=True, exist_ok=True) + + home_config_data = {"username": "home_username", "email": "home@email.com"} + with open(home_dir / "diffpyconfig.json", "w") as f: + json.dump(home_config_data, f) + + yield tmp_path diff --git a/src/diffpy/snmf/tests/debug.py b/src/diffpy/snmf/tests/debug.py new file mode 100644 index 0000000..2b585b2 --- /dev/null +++ b/src/diffpy/snmf/tests/debug.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2024 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Billinge Group members and community contributors. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.snmf/graphs/contributors +# +# See LICENSE.rst for license information. +# +############################################################################## + +""" +Convenience module for debugging the unit tests using + +python -m diffpy.snmf.tests.debug + +Exceptions raised by failed tests or other errors are not caught. +""" + + +if __name__ == "__main__": + import sys + + from diffpy.snmf.tests import testsuite + + pattern = sys.argv[1] if len(sys.argv) > 1 else "" + suite = testsuite(pattern) + suite.debug() + + +# End of file diff --git a/src/diffpy/snmf/tests/test_containers.py b/src/diffpy/snmf/tests/test_containers.py new file mode 100644 index 0000000..1c78be0 --- /dev/null +++ b/src/diffpy/snmf/tests/test_containers.py @@ -0,0 +1,110 @@ +import numpy as np +import pytest + +from diffpy.snmf.containers import ComponentSignal + +tas = [ + ( + [np.arange(10), 3, 0, [6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], 0.25], + [ + [6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], + [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], + [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0], + ], + ), + ( + [np.arange(5), 10, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], 1.25], + [ + [-11.47, -10.8444, -9.1322, -16.633, -20.6760], + [0, -0.50048, -3.31904, 40.9824, -112.1792], + [0, 0.800768, 5.310464, -65.57184, 179.48672], + ], + ), + ( + [np.arange(5), 2, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], 0.88], + [ + [-11.47, -10.3344, -13.9164, -11.5136, 0], + [0, -3.3484, 55.1265, -169.7572, 0], + [0, 7.609997, -125.2876, 385.81189, 0], + ], + ), + ( + [np.arange(10), 1, 2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.88], + [ + [1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], + [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], + [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0], + ], + ), + ( + [ + np.arange(14), + 100, + 3, + [ + -2.9384, + -1.4623, + -2.0913, + 4.6304, + -1.2127, + 1.4737, + -0.3791, + 1.7506, + -1.5068, + -2.7625, + 0.9617, + -0.3494, + -0.3862, + 2.7960, + ], + 0.55, + ], + [ + [-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0], + [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], + [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0], + ], + ), + ( + [np.arange(11), 20, 4, [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], 0.987], + [ + [0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], + [0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], + [0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0], + ], + ), + ( + [np.arange(9), 15, 3, [-1, -2, -3, -4, -5, -6, -7, -8, -9], -0.4], + [[-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], + ), +] + + +@pytest.mark.parametrize("tas", tas) +def test_apply_stretch(tas): + component = ComponentSignal(tas[0][0], tas[0][1], tas[0][2]) + component.iq = tas[0][3] + component.stretching_factors[0] = tas[0][4] + actual = component.apply_stretch(0) + expected = tas[1] + np.testing.assert_allclose(actual, expected, rtol=1e-01) + + +taw = [ + ([np.arange(5), 2, 0, [0, 1, 2, 3, 4], 0.5], [0, 0.5, 1, 1.5, 2]), + ([np.arange(5), 20, 2, [0, -1, -2, -3, -4], 0.25], [0, -0.25, -0.5, -0.75, -1]), + ([np.arange(40), 200, 4, np.arange(0, 10, 0.25), 0.3], np.arange(0, 10, 0.25) * 0.3), + ([np.arange(1), 10, 2, [10.5, 11.5, -10.5], 0], [0, 0, 0]), + ([[-12, -10, -15], 5, 2, [-0.5, -1, -1.2], 0.9], [-0.45, -0.9, -1.08]), + ([[-12, -10, -15], 5, 2, [0, 0, 0], 0.9], [0, 0, 0]), +] + + +@pytest.mark.parametrize("taw", taw) +def test_apply_weight(taw): + component = ComponentSignal(taw[0][0], taw[0][1], taw[0][2]) + component.iq = np.array(taw[0][3]) + component.weights[0] = taw[0][4] + actual = component.apply_weight(0) + expected = taw[1] + np.testing.assert_allclose(actual, expected, rtol=1e-01) diff --git a/diffpy/snmf/tests/test_factorizers.py b/src/diffpy/snmf/tests/test_factorizers.py similarity index 51% rename from diffpy/snmf/tests/test_factorizers.py rename to src/diffpy/snmf/tests/test_factorizers.py index 65aa0ac..c20d192 100644 --- a/diffpy/snmf/tests/test_factorizers.py +++ b/src/diffpy/snmf/tests/test_factorizers.py @@ -1,20 +1,20 @@ import numpy as np -import scipy import pytest + from diffpy.snmf.factorizers import lsqnonneg tl = [ - ([[[1, 0], [1, 0], [0, 1]], [2, 1, 1]], [1.5, 1.]), + ([[[1, 0], [1, 0], [0, 1]], [2, 1, 1]], [1.5, 1.0]), ([[[2, 3], [1, 2], [0, 0]], [7, 7, 2]], [0, 2.6923]), - ([[[3, 2, 4, 1]], [3.2]], [0, 0, .8, 0]), - ([[[-.4, 0], [0, 0], [-9, -18]], [-2, -3, -4.9]], [.5532, 0]), - ([[[-.1, -.2], [-.8, -.9]], [0, 0]], [0, 0]), + ([[[3, 2, 4, 1]], [3.2]], [0, 0, 0.8, 0]), + ([[[-0.4, 0], [0, 0], [-9, -18]], [-2, -3, -4.9]], [0.5532, 0]), + ([[[-0.1, -0.2], [-0.8, -0.9]], [0, 0]], [0, 0]), ([[[0, 0], [0, 0]], [10, 10]], [0, 0]), - ([[[2], [1], [-4], [-.3]], [6, 4, .33, -5]], 0.767188240872451), + ([[[2], [1], [-4], [-0.3]], [6, 4, 0.33, -5]], 0.767188240872451), ] -@pytest.mark.parametrize('tl', tl) +@pytest.mark.parametrize("tl", tl) def test_lsqnonneg(tl): actual = lsqnonneg(tl[0][0], tl[0][1]) expected = tl[1] diff --git a/diffpy/snmf/tests/test_optimizers.py b/src/diffpy/snmf/tests/test_optimizers.py similarity index 71% rename from diffpy/snmf/tests/test_optimizers.py rename to src/diffpy/snmf/tests/test_optimizers.py index e686250..ab7a71f 100644 --- a/diffpy/snmf/tests/test_optimizers.py +++ b/src/diffpy/snmf/tests/test_optimizers.py @@ -1,19 +1,20 @@ import pytest + from diffpy.snmf.optimizers import get_weights tm = [ ([[[1, 0], [0, 1]], [1, 1], [0, 0], [1, 1]], [0, 0]), ([[[1, 0], [0, 1]], [1, 1], -1, 1], [-1, -1]), ([[[1.75, 0], [0, 1.5]], [1, 1.2], -1, 1], [-0.571428571428571, -0.8]), - ([[[.75, .2], [.2, .75]], [-.1, -.2], -1, 1], [0.066985645933014, 0.248803827751196]), + ([[[0.75, 0.2], [0.2, 0.75]], [-0.1, -0.2], -1, 1], [0.066985645933014, 0.248803827751196]), ([[[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, 1, 1], -10, 12], [-1.5, -2, -1.5]), ([[[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, -1, -1], -10, 12], [0, 1, 1]), - ([[[4, 0, 0, 0], [0, 3, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]], [-2, -3, -4, -1], 0, 1000], [.5, 1, 2, 1]), + ([[[4, 0, 0, 0], [0, 3, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]], [-2, -3, -4, -1], 0, 1000], [0.5, 1, 2, 1]), ] -@pytest.mark.parametrize('tm', tm) +@pytest.mark.parametrize("tm", tm) def test_get_weights(tm): expected = tm[1] actual = get_weights(tm[0][0], tm[0][1], tm[0][2], tm[0][3]) - assert (actual == pytest.approx(expected, rel=1e-4, abs=1e-6)) + assert actual == pytest.approx(expected, rel=1e-4, abs=1e-6) diff --git a/diffpy/snmf/tests/test_polynomials.py b/src/diffpy/snmf/tests/test_polynomials.py similarity index 98% rename from diffpy/snmf/tests/test_polynomials.py rename to src/diffpy/snmf/tests/test_polynomials.py index a75baca..d935834 100644 --- a/diffpy/snmf/tests/test_polynomials.py +++ b/src/diffpy/snmf/tests/test_polynomials.py @@ -1,5 +1,6 @@ -import pytest import numpy as np +import pytest + from diffpy.snmf.polynomials import rooth tr = [ @@ -15,7 +16,7 @@ ([-9, 3], 2.8169), ([[2, 2], 2], [0, 0]), ([[[2, 2], [2, 2]], 2], [[0, 0], [0, 0]]), - ([[[[3, 2], [-2, -2], [100, 0]]], 2], [[[0, 0], [0, 0], [0, 0]]]) + ([[[[3, 2], [-2, -2], [100, 0]]], 2], [[[0, 0], [0, 0], [0, 0]]]), ] diff --git a/src/diffpy/snmf/tests/test_subroutines.py b/src/diffpy/snmf/tests/test_subroutines.py new file mode 100644 index 0000000..66029d2 --- /dev/null +++ b/src/diffpy/snmf/tests/test_subroutines.py @@ -0,0 +1,539 @@ +import numpy as np +import pytest + +from diffpy.snmf.containers import ComponentSignal +from diffpy.snmf.subroutines import ( + construct_component_matrix, + construct_stretching_matrix, + construct_weight_matrix, + get_residual_matrix, + get_stretched_component, + initialize_components, + lift_data, + objective_function, + reconstruct_data, + reconstruct_signal, + update_weights, + update_weights_matrix, +) + +to = [ + ([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 1], 2.574e14), + ([[[11, 2], [31, 4]], [[5, 63], [7, 18]], 0.001, [[21, 2], [3, 4]], [[11, 22], [3, 40]], 1], 650.4576), + ([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 0], 2.574e14), +] + + +@pytest.mark.parametrize("to", to) +def test_objective_function(to): + actual = objective_function(to[0][0], to[0][1], to[0][2], to[0][3], to[0][4], to[0][5]) + expected = to[1] + assert actual == pytest.approx(expected) + + +tgso = [ + ( + [0.25, [6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], 10], + ( + [6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], + [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], + [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0], + ), + ), + ( + [1.25, [-11.47, -10.688, -8.095, -29.44, 14.38], 5], + ( + [-11.47, -10.8444, -9.1322, -16.633, -20.6760], + [0, -0.50048, -3.31904, 40.9824, -112.1792], + [0, 0.800768, 5.310464, -65.57184, 179.48672], + ), + ), + ( + [0.88, [-11.47, -10.688, -8.095, -29.44, 14.38], 5], + ( + [-11.47, -10.3344, -13.9164, -11.5136, 0], + [0, -3.3484, 55.1265, -169.7572, 0], + [0, 7.609997, -125.2876, 385.81189, 0], + ), + ), + ( + [0.88, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10], + ( + [1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], + [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], + [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0], + ), + ), + ( + [ + 0.55, + [ + -2.9384, + -1.4623, + -2.0913, + 4.6304, + -1.2127, + 1.4737, + -0.3791, + 1.7506, + -1.5068, + -2.7625, + 0.9617, + -0.3494, + -0.3862, + 2.7960, + ], + 14, + ], + ( + [-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0], + [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], + [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0], + ), + ), + ( + [0.987, [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], 11], + ( + [0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], + [0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], + [0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0], + ), + ), + ( + [-0.4, [-1, -2, -3, -4, -5, -6, -7, -8, -9], 9], + ([-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]), + ), +] + + +@pytest.mark.parametrize("tgso", tgso) +def test_get_stretched_component(tgso): + actual = get_stretched_component(tgso[0][0], tgso[0][1], tgso[0][2]) + expected = tgso[1] + np.testing.assert_allclose(actual, expected, rtol=1e-01) + + +tuwm = [ + ( + [ + 2, + 2, + [[0.5, 0.6], [0.7, 0.8]], + [[1, 2], [4, 8]], + [[1.6, 2.8], [5, 8.8]], + 2, + [[0.78, 0.12], [0.5, 0.5]], + None, + ], + [[0, 1], [1, 1]], + ), + ( + [2, 3, [[0.5], [0.5]], [[1, 2.5], [1.5, 3], [2, 3.5]], [[1, 2], [3, 4], [5, 6]], 1, [[0.5], [0.5]], None], + [[1], [0.1892]], + ), + ( + [ + 2, + 3, + [[0.5, 0.6, 0.7], [0.5, 0.6, 0.7]], + [[1, 2.5], [1.5, 3], [2, 3.5]], + [[1, 2, 3], [3, 4, 5], [5, 6, 7]], + 3, + [[0.5, 0.45, 0.4], [0.5, 0.45, 0.4]], + None, + ], + [[1, 1, 1], [0.1892, 0.5600, 0.938]], + ), + ( + [ + 3, + 3, + [[0.7, 0.8, 0.9], [0.71, 0.72, 0.73], [0.8, 0.85, 0.9]], + [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -0.5]], + [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], + 3, + [[0.9, 0.4, 0.5], [1, 0, 0.4], [0, 0, 0.98]], + None, + ], + [[1, 0.0651, 0], [0.5848, 0.0381, 0.1857], [0, 1, 1]], + ), + ([2, 2, [[0.5], [0.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[0.6], [0.4]], "align"], [[0], [0]]), + ([1, 3, [[0.5, 0.3]], [[1], [1.1], [1.3]], [[1, 2], [2, 3], [3, 2]], 2, [[0.6, 0.4]], None], [[1, 1]]), + ( + [ + 2, + 2, + [[0.5, 0.6], [0.7, 0.8]], + [[1, 2], [4, 8]], + [[1.6, 2.8], [5, 8.8]], + 2, + [[0.78, 0.12], [0.5, 0.5]], + "align", + ], + [[0, 0], [1.0466, 1.46]], + ), + ( + [ + 2, + 3, + [[0.5], [0.5]], + [[1, 2.5], [1.5, 3], [2, 3.5]], + [[1, 2], [3, 4], [5, 6]], + 1, + [[0.5], [0.5]], + "align", + ], + [[1.4], [0]], + ), + ( + [ + 3, + 3, + [[0.7, 0.8, 0.9], [0.71, 0.72, 0.73], [0.8, 0.85, 0.9]], + [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -0.5]], + [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], + 3, + [[0.9, 0.4, 0.5], [1, 0, 0.4], [0, 0, 0.98]], + "align", + ], + [[1.2605, 0.0552, 0], [0.2723, 0, 0], [0, 1.0538, 1.1696]], + ), + ([2, 2, [[0.5], [0.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[0.6], [0.4]], "align"], [[0], [0]]), + ([1, 3, [[0.5, 0.3]], [[1], [1.1], [1.3]], [[1, 2], [2, 3], [3, 2]], 2, [[0.6, 0.4]], "align"], [[1.3383, 2]]), +] + + +@pytest.mark.parametrize("tuwm", tuwm) +def test_update_weights_matrix(tuwm): + actual = update_weights_matrix( + tuwm[0][0], tuwm[0][1], tuwm[0][2], tuwm[0][3], tuwm[0][4], tuwm[0][5], tuwm[0][6], tuwm[0][7] + ) + expected = tuwm[1] + np.testing.assert_allclose(actual, expected, rtol=1e-03, atol=0.5) + + +tgrm = [ + ([[[1, 2], [3, 4]], [[0.25], [0.75]], [[0.9], [0.7]], [[11, 22], [33, 44]], 1, 2, 2], [[-9, -22], [-33, -44]]), + ([[[1, 2], [3, 4]], [[1], [1]], [[1], [1]], [[11, 22], [33, 44]], 1, 2, 2], [[-8, -22], [-26, -44]]), + ( + [ + [[1.1, 4.4], [1.2, 4.5], [14, 7.8]], + [[0.4, 0.6], [0.75, 0.25]], + [[0.9, 0.89], [0.98, 0.88]], + [[10, 20], [-10.5, -20.6], [0.6, 0.9]], + 2, + 2, + 3, + ], + [[-6.26, -18.24], [14.9744, 23.5067], [-0.6, -0.9]], + ), + # positive float + ( + [ + [[-1.1, -4.4], [-1.2, -4.5], [-14, -7.8]], + [[0.4, 0.6], [0.75, 0.25]], + [[0.9, 0.89], [0.98, 0.88]], + [[10, 20], [-10.5, -20.6], [0.6, 0.9]], + 2, + 2, + 3, + ], + [[-13.74, -21.76], [6.0256, 17.6933], [-0.6, -0.9]], + ), + # negative floats + ( + [ + [[0, 0, 0, 0], [0, 0, 0, 0]], + [[0.4], [0.2], [0.3], [0.3]], + [[0.9], [0.9], [0.9], [0.9]], + [[0, 0, 0, 0], [0, 0, 0, 0]], + 1, + 4, + 2, + ], + [[0, 0, 0, 0], [0, 0, 0, 0]], + ), +] + + +@pytest.mark.parametrize("tgrm", tgrm) +def test_get_residual_matrix(tgrm): + actual = get_residual_matrix( + tgrm[0][0], tgrm[0][1], tgrm[0][2], tgrm[0][3], tgrm[0][4], tgrm[0][5], tgrm[0][6] + ) + expected = tgrm[1] + np.testing.assert_allclose(actual, expected, rtol=1e-04) + + +trd = [ + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ] + ), + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0)]), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 3), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 4), + ] + ), + # ([]) # Exception expected +] + + +@pytest.mark.parametrize("trd", trd) +def test_reconstruct_data(trd): + actual = reconstruct_data(trd) + assert actual.shape == (len(trd[0].iq), len(trd[0].weights)) + print(actual) + + +tld = [ + (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], 1]), ([[4, 2, 4], [3, 3, 3], [5, 13, 0]])), + (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], 0]), ([[1, -1, 1], [0, 0, 0], [2, 10, -3]])), + (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], 0.5]), ([[2.5, 0.5, 2.5], [1.5, 1.5, 1.5], [3.5, 11.5, -1.5]])), + (([[[1, -1, 1], [0, 0, 0], [2, 10, -3]], -1]), ([[4, 2, 4], [3, 3, 3], [5, 13, 0]])), + (([[[0, 0, 0], [0, 0, 0], [0, 0, 0]], 100]), ([[0, 0, 0], [0, 0, 0], [0, 0, 0]])), + (([[[1.5, 2], [10.5, 1], [0.5, 2]], 1]), ([[2, 2.5], [11, 1.5], [1, 2.5]])), + (([[[-10, -10.5], [-12.2, -12.2], [0, 0]], 1]), ([[2.2, 1.7], [0, 0], [12.2, 12.2]])), +] + + +@pytest.mark.parametrize("tld", tld) +def test_lift_data(tld): + actual = lift_data(tld[0][0], tld[0][1]) + expected = tld[1] + np.testing.assert_allclose(actual, expected) + + +tcc = [ + (2, 3, [0, 0.5, 1, 1.5]), # Regular usage + # (0, 3,[0, .5, 1, 1.5]), # Zero components raise an exception. Not tested +] + + +@pytest.mark.parametrize("tcc", tcc) +def test_initialize_components(tcc): + actual = initialize_components(tcc[0], tcc[1], tcc[2]) + assert len(actual) == tcc[0] + assert len(actual[0].weights) == tcc[1] + assert (actual[0].grid == np.array(tcc[2])).all() + + +tcso = [ + ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0)], 1, 20), + ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0)], 4, 20), + # ([ComponentSignal([0,.5,1,1.5],20,0)],0,20), # Raises an exception + # ([ComponentSignal([0,.5,1,1.5],20,0)],-2,20), # Raises an exception + # ([ComponentSignal([0,.5,1,1.5],20,0)],1,0), # Raises an Exception + # ([ComponentSignal([0,.5,1,1.5],20,0)],1,-3), # Raises an exception + ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 1.5], 20, 1)], 2, 20), + ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 21.5], 20, 1)], 2, 20), + ([ComponentSignal([0, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 21.5], 20, 1)], 2, 20), + # ([ComponentSignal([0,.5,1,1.5],20,0),ComponentSignal([0,.5,1,1.5],20,1)],1,-3), + # Negative signal length. Raises an exception + # ([],1,20), # Empty components. Raises an Exception + # ([],-1,20), # Empty components with negative number of components. Raises an exception + # ([],0,20), # Empty components with zero number of components. Raises an exception + # ([],1,0), # Empty components with zero signal length. Raises an exception. + # ([],-1,-2), # Empty components with negative number of components and signal length Raises an exception. +] + + +@pytest.mark.parametrize("tcso", tcso) +def test_construct_stretching_matrix(tcso): + actual = construct_stretching_matrix(tcso[0], tcso[1], tcso[2]) + for component in tcso[0]: + np.testing.assert_allclose(actual[component.id, :], component.stretching_factors) + # assert actual[component.id, :] == component.stretching_factors + + +tccm = [ + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0)]), + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 0, 0)]), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 2.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ([ComponentSignal([0.25], 20, 0), ComponentSignal([0.25], 20, 1), ComponentSignal([0.25], 20, 2)]), + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1)]), + # ([ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 0), + # ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 1)]), + # iq is multidimensional. Expected to fail + # (ComponentSignal([], 20, 0)), # Expected to fail + # ([]), #Expected to fail +] + + +@pytest.mark.parametrize("tccm", tccm) +def test_construct_component_matrix(tccm): + actual = construct_component_matrix(tccm) + for component in tccm: + np.testing.assert_allclose(actual[component.id], component.iq) + + +tcwm = [ + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0)]), + # ([ComponentSignal([0,.25,.5,.75,1],0,0)]), # 0 signal length. Failure expected + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), + ComponentSignal([0, 0.25, 0.5, 2.75, 1], 20, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), + ] + ), + ([ComponentSignal([0.25], 20, 0), ComponentSignal([0.25], 20, 1), ComponentSignal([0.25], 20, 2)]), + ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1)]), + # (ComponentSignal([], 20, 0)), # Expected to fail + # ([]), #Expected to fail +] + + +@pytest.mark.parametrize("tcwm", tcwm) +def test_construct_weight_matrix(tcwm): + actual = construct_weight_matrix(tcwm) + for component in tcwm: + np.testing.assert_allclose(actual[component.id], component.weights) + + +tuw = [ + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], + None, + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], + "align", + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + None, + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "align", + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[-0.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, 0.1]], + None, + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + [[-0.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, 0.1]], + "align", + ), + # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), + # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], None), + # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), + # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [], None), + # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), + # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [], 170), +] + + +@pytest.mark.parametrize("tuw", tuw) +def test_update_weights(tuw): + actual = update_weights(tuw[0], tuw[1], tuw[2]) + assert np.shape(actual) == (len(tuw[0]), len(tuw[0][0].weights)) + + +trs = [ + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + 1, + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), + ], + 0, + ), + ( + [ + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 0), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 1), + ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 2), + ], + 2, + ), + # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), + # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], -1), +] + + +@pytest.mark.parametrize("trs", trs) +def test_reconstruct_signal(trs): + actual = reconstruct_signal(trs[0], trs[1]) + assert len(actual) == len(trs[0][0].grid) diff --git a/src/diffpy/snmf/version.py b/src/diffpy/snmf/version.py new file mode 100644 index 0000000..202bd54 --- /dev/null +++ b/src/diffpy/snmf/version.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2024 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Billinge Group members and community contributors. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.snmf/graphs/contributors +# +# See LICENSE.rst for license information. +# +############################################################################## + +"""Definition of __version__.""" + +# We do not use the other three variables, but can be added back if needed. +# __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"] + +# obtain version information +from importlib.metadata import version + +__version__ = version("diffpy.snmf") + +# End of file