From 428e874cbd05f34743b8adb1d7b96d45a8b313bf Mon Sep 17 00:00:00 2001 From: Adib Kabir Date: Tue, 29 Sep 2026 16:47:36 -0700 Subject: [PATCH 1/5] skpkg: updated labpdfproc with scikit-package level 5 --- .../workflows/build-wheel-release-upload.yml | 64 +++- .github/workflows/check-news-item.yml | 2 +- .pre-commit-config.yaml | 4 +- CHANGELOG.rst | 109 ------- LICENSE.rst | 5 +- MANIFEST.in | 1 - README.rst | 34 +- cookiecutter.json | 2 +- docs/make.bat | 72 ++-- docs/source/api/diffpy.labpdfproc.rst | 32 +- docs/source/conf.py | 12 +- docs/source/img/.placeholder | 0 docs/source/index.rst | 17 +- docs/source/license.rst | 5 +- news/scikit-package.rst | 23 ++ pyproject.toml | 9 +- requirements/build.txt | 2 - requirements/conda.txt | 5 - requirements/pip.txt | 4 - requirements/tests.txt | 1 - src/diffpy/__init__.py | 8 +- src/diffpy/labpdfproc/__init__.py | 10 +- src/diffpy/labpdfproc/functions.py | 307 ------------------ src/diffpy/labpdfproc/labpdfproc_app.py | 35 ++ src/diffpy/labpdfproc/version.py | 11 +- tests/conftest.py | 78 +---- tests/test_functions.py | 222 ------------- tests/test_version.py | 2 +- 28 files changed, 206 insertions(+), 870 deletions(-) create mode 100644 docs/source/img/.placeholder create mode 100644 news/scikit-package.rst delete mode 100644 src/diffpy/labpdfproc/functions.py create mode 100644 src/diffpy/labpdfproc/labpdfproc_app.py delete mode 100644 tests/test_functions.py diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index 7f42d6d..cb88f6c 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -1,18 +1,76 @@ -name: Build Wheel, Release on GitHub/PyPI, and Deploy Docs +name: Build Wheel and Release +# Trigger on tag push or manual dispatch. +# Tag and release privilege are verified inside the reusable workflow. on: workflow_dispatch: push: tags: - - "*" # Trigger on all tags initially, but tag and release privilege are verified in _build-wheel-release-upload.yml + - "*" + +# ── Release modality ────────────────────────────────────────────────────────── +# Three options are provided below. Only ONE job should be active at a time. +# To switch: comment out the active job and uncomment your preferred option, +# then commit the change to main before tagging a release. +# ───────────────────────────────────────────────────────────────────────────── jobs: + # Option 1 (default): Release to GitHub, publish to PyPI, and deploy docs. + # + # The wheel is uploaded to PyPI so users can install with `pip install`. + # A GitHub release is created with the changelog as the release body, and + # the Sphinx documentation is rebuilt and deployed to GitHub Pages. + # + # Choose this for open-source packages distributed via PyPI and/or + # conda-forge where broad public availability is the goal. build-release: uses: scikit-package/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.labpdfproc c_extension: false - maintainer_GITHUB_username: sbillinge + maintainer_github_username: sbillinge secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + + # Option 2: Release to GitHub and deploy docs, without publishing to PyPI. + # + # A GitHub release is created and the Sphinx docs are deployed, but the + # wheel is not uploaded to PyPI. The source code remains publicly visible + # on GitHub and can be installed directly from there. + # + # Choose this when the package is public but you prefer to keep it off the + # default pip index — for example, if you distribute via conda-forge only, + # or if the package is not yet ready for a permanent PyPI presence. + # + # To use: comment out Option 1 above and uncomment the lines below. + # build-release-no-pypi: + # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-no-pypi.yml@v0 + # with: + # project: diffpy.labpdfproc + # c_extension: false + # maintainer_github_username: sbillinge + # secrets: + # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + + # Option 3: Release to GitHub with wheel, license, and instructions bundled + # as a downloadable zip attached to the GitHub release asset. + # + # The wheel is built and packaged together with INSTRUCTIONS.txt and the + # LICENSE file into a zip that is attached directly to the GitHub release. + # Users with access to the (private) repo download the zip, follow the + # instructions inside, and install locally with pip. No PyPI or conda-forge + # upload occurs, and no docs are deployed. + # + # Choose this for private or restricted packages where distribution must be + # controlled: only users with repo access can download the release asset, + # making the GitHub release itself the distribution channel. + # + # To use: comment out Option 1 above and uncomment the lines below. + # build-release-private: + # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-private-pure.yml@v0 + # with: + # project: diffpy.labpdfproc + # maintainer_github_username: sbillinge + # secrets: + # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/check-news-item.yml b/.github/workflows/check-news-item.yml index 1f2a0cc..a172da8 100644 --- a/.github/workflows/check-news-item.yml +++ b/.github/workflows/check-news-item.yml @@ -3,7 +3,7 @@ name: Check for News on: pull_request_target: branches: - - main + - main # GitHub does not evaluate expressions in trigger filters; edit this value if your base branch is not main jobs: check-news-item: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9573476..7554bee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3 + python: python3.14 ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit hooks @@ -52,7 +52,7 @@ repos: - tomli # prettier - multi formatter for .json, .yml, and .md files - repo: https://github.com/pre-commit/mirrors-prettier - rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 + rev: v4.0.0-alpha.8 hooks: - id: prettier additional_dependencies: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e10213..f29d3b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,112 +3,3 @@ Release notes ============= .. current developments - -0.3.1 -===== - -**Added:** - -* Add back support for Python 3.10 and 3.11. - - -0.3.0 -===== - -**Added:** - -* Functionalities to estimate mu*D theoretically. -* Added published reference to ``README.rst``. -* Fast calculation supports values up to muD = 7 -* Recookiecut with updated ``scikit-package`` to enable docs preview in PRs. -* Updated package standards to scikit-package 0.3.0. -* Added ``cookiecutter.json`` file for the ``package update`` command. -* Functionality to read wavelength and anode type directly from a diffpy configuration file. -* Utility and example documentation for ``tools`` module. -* Gooey support so that the app can be run with GUI -* Coverage report in each PR -* doi in Readme for papers. -* Support for independent variables other than two-theta. -* new subcommand ``applymud`` to run the original absorption correction process through CLI. -* Utility and example documentation for the main module. -* Added documentation for new CLI updates. -* Documentation for functions module. -* Spelling check via Codespell in pre-commit -* Python 3.13 support -* Functionality in `load_user_info` to enable user to enter an ORCID. - -**Changed:** - -* Default to brute-force computation when muD < 0.5 or > 7. -* Print a warning message instead of error, explicitly stating the input muD value -* Functions that use DiffractionObject` in `diffpy.utils` to follow the new API. -* Workflow for loading wavelength - raise an error when both wavelength and anode type are specified. -* Readme: muD now requires the ``--mud`` flag instead of a required argument. -* Made muD an optional argument and provided different options (manually entry / z-scan file path) for users to specify muD -* Increased the number of significant figures for wavelength and separated values for Ka1 and Ka2. -* hyphens / underscores format according to new scikit-package group standard. -* GitHub workflows for renamed test file. -* Return a ``ValueError`` if no wavelength is found on config file or if its not specified. -* Compartmentalize commands into the subcommands ``mud``, ``zscan``, and ``sample``. See documentation for more info. -* Changed ``doc`` to ``docs`` and ``CODE_OF_CONDUCT.rst`` to ``CODE-OF-CONDUCT.rst`` to comply with scikit-package standards. -* All function docstrings and tests to be more informative, incorporating new ORCID function and improving overall clarity. - -**Fixed:** - -* duplicated wavelength information in output files - -**Removed:** - -* Remove the import of extend_path from pkgutil in diffpy/__init__.py since we are not strictly following the Python namespace package convention. - - -0.2.0 -===== - -**Added:** - -* Support for Python 3.13 - -**Removed:** - -* Support for Python 3.10 - - -0.1.3 -===== - -**Added:** - -* generate package API doc -* redo cookiecutter to add issue templates and update readme - - -0.1.2 -===== - -**Added:** - -* polynomial interpolation as the default method for cve computation. - -**Fixed:** - -* add PyPI packages under pip.txt - - - -0.1.1 -===== - - - -0.1.1 -===== - - - -0.1.0 -===== - - - -Initial release of labPDFproc. Please see README and documentation for details diff --git a/LICENSE.rst b/LICENSE.rst index d5f6da9..43d13fc 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,9 +1,6 @@ BSD 3-Clause License -Copyright (c) 2025-2026, The Trustees of Columbia University in the City of New York. -All rights reserved. - -Copyright (c) 2026-present, diffpy.labpdfproc developers and contributors. +Copyright (c) 2026, 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 diff --git a/MANIFEST.in b/MANIFEST.in index ae36d2f..f1a78ee 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,6 @@ graft tests graft requirements include AUTHORS.rst LICENSE*.rst README.rst -include src/diffpy/labpdfproc/data * # Exclude all bytecode files and __pycache__ directories global-exclude *.py[cod] # Exclude all .pyc, .pyo, and .pyd files. diff --git a/README.rst b/README.rst index 48b6c98..0c4a9fc 100644 --- a/README.rst +++ b/README.rst @@ -38,30 +38,7 @@ Tools for processing x-ray powder diffraction data from laboratory sources. -PDFgetX3 has revolutionized how PDF methods can be applied to solve nanostructure problems. -However, the program was designed for use with Rapid Acquisition PDF (RAPDF) data from synchrotron sources. -A key approximation inherent in the use of PDFgetX3 for RAPDF data is that absorption effects are negligible. -This is typically not the case for laboratory x-ray diffractometers, where absorption effects can be significant. - -This app is designed to preprocess data from laboratory x-ray diffractometers before using PDFgetX3 to obtain PDFs. -The app currently carries out an absorption correction assuming a parallel beam capillary geometry -which is the most common geometry for lab PDF measurements. - -The theory is described in the following paper: - - Chen, Y., Schertenleib, T., Yang, A., Schouwink, P., Queen, W. L., and Billinge, S. J. L., - *Absorption Correction for Reliable Pair Distribution Functions from Low Energy X-ray Sources*. - Crystal Growth & Design, 2026, 26 (3), 1036–1047. - https://doi.org/10.1021/acs.cgd.5c00551 - - -The related experimental data acquisition protocols are described in the following paper: - - Schertenleib, T., Schmuckler, D., Chen, Y., Jin, G. B., Queen, W. L., and Billinge, S. J. L. (2025). - *Testing Protocols for Obtaining Reliable Pair Distribution Functions from Laboratory X-Ray Sources Using PDFgetX3*. - Chem. Methods, 2500001. - https://doi.org/10.1002/cmtd.202500001 - +* LONGER DESCRIPTION HERE For more information about the diffpy.labpdfproc library, please consult our `online documentation `_. @@ -70,10 +47,7 @@ Citation If you use diffpy.labpdfproc in a scientific publication, we would like you to cite this package as - Chen, Y., Schertenleib, T., Yang, A., Schouwink, P., Queen, W. L., and Billinge, S. J. L., - *Absorption Correction for Reliable Pair Distribution Functions from Low Energy X-ray Sources*. - Crystal Growth & Design, 2026, 26 (3), 1036–1047. - https://doi.org/10.1021/acs.cgd.5c00551 + diffpy.labpdfproc Package, https://github.com/diffpy/diffpy.labpdfproc Installation ------------ @@ -108,7 +82,7 @@ and run the following :: This package also provides command-line utilities. To check the software has been installed correctly, type :: - labpdfproc --version + diffpy.labpdfproc --version You can also type the following command to verify the installation. :: @@ -117,7 +91,7 @@ You can also type the following command to verify the installation. :: To view the basic usage and available commands, type :: - labpdfproc -h + diffpy.labpdfproc -h Getting Started --------------- diff --git a/cookiecutter.json b/cookiecutter.json index 7c013d4..4e72ae9 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,8 +5,8 @@ "maintainer_emails": "sbillinge@ucsb.edu", "maintainer_github_usernames": "sbillinge", "contributors": "Yucong Chen, Till Schertenleib, Caden Myers, Billinge Group members", - "license_holders": "The Trustees of Columbia University in the City of New York", "project_name": "diffpy.labpdfproc", + "license_holders": "The Trustees of Columbia University in the City of New York", "github_username_or_orgname": "diffpy", "github_repo_name": "diffpy.labpdfproc", "conda_pypi_package_dist_name": "diffpy.labpdfproc", diff --git a/docs/make.bat b/docs/make.bat index 2be8306..ac53d5b 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=PackagingScientificPython - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=PackagingScientificPython + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/api/diffpy.labpdfproc.rst b/docs/source/api/diffpy.labpdfproc.rst index a3f53e1..1bcc98d 100644 --- a/docs/source/api/diffpy.labpdfproc.rst +++ b/docs/source/api/diffpy.labpdfproc.rst @@ -1,7 +1,9 @@ :tocdepth: -1 -diffpy.labpdfproc package -========================= +|title| +======= + +.. |title| replace:: diffpy.labpdfproc package .. automodule:: diffpy.labpdfproc :members: @@ -12,33 +14,17 @@ Subpackages ----------- .. toctree:: - :titlesonly: - - diffpy.labpdfproc.data + diffpy.labpdfproc.example_package Submodules ---------- -diffpy.labpdfproc.functions module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: diffpy.labpdfproc.functions - :members: - :undoc-members: - :show-inheritance: - -diffpy.labpdfproc.tools module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: diffpy.labpdfproc.tools - :members: - :undoc-members: - :show-inheritance: +|module| +-------- -diffpy.labpdfproc.labpdfprocapp module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. |module| replace:: diffpy.labpdfproc.example_submodule module -.. automodule:: diffpy.labpdfproc.labpdfprocapp +.. automodule:: diffpy.labpdfproc.example_submodule :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index 4ef7771..e759486 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# diffpy.labpdfproc documentation build configuration file, created by +# diffpy.labpdfproc documentation build configuration file, created by # noqa: E501 # sphinx-quickstart on Thu Jan 30 15:49:41 2014. # # This file is execfile()d with the current directory set to its @@ -22,14 +22,11 @@ try: fullversion = version("diffpy.labpdfproc") except Exception: - fullversion = ( - "No version found. " - "The correct version will appear in the released version." - ) + fullversion = "No version found. The correct version will appear in the released version." # noqa: E501 # 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 Path().resolve() to make it absolute, like shown here +# documentation root, use Path().resolve() to make it absolute, like shown here. # noqa: E501 # sys.path.insert(0, str(Path(".").resolve())) sys.path.insert(0, str(Path("../..").resolve())) sys.path.insert(0, str(Path("../../src").resolve())) @@ -108,9 +105,6 @@ # directories to ignore when looking for source files. exclude_patterns = ["build"] -# Mock imports so API docs render without installing dependencies. -autodoc_mock_imports = ["diffpy.utils"] - # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None diff --git a/docs/source/img/.placeholder b/docs/source/img/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/index.rst b/docs/source/index.rst index 1bf6d0f..64de3f9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,6 +9,14 @@ | Software version |release| | Last updated |today|. +=============== +Getting started +=============== + +Welcome to the ``diffpy.labpdfproc`` documentation! + +To get started, please visit the :ref:`Getting started ` page. + ======= Authors ======= @@ -33,13 +41,12 @@ Acknowledgements Table of contents ================= .. toctree:: - :titlesonly: + :maxdepth: 2 - license - release - Utilities - Examples + getting-started Package API + release + license ======= Indices diff --git a/docs/source/license.rst b/docs/source/license.rst index 2abd4ff..7428e28 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -9,10 +9,7 @@ OPEN SOURCE LICENSE AGREEMENT ============================= BSD 3-Clause License -Copyright (c) 2024-2025, The Trustees of Columbia University in the City of New York. -All Rights Reserved. - -Copyright (c) 2026-present, diffpy.labpdfproc developers and contributors. +Copyright (c) 2026, 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 diff --git a/news/scikit-package.rst b/news/scikit-package.rst new file mode 100644 index 0000000..6a911d7 --- /dev/null +++ b/news/scikit-package.rst @@ -0,0 +1,23 @@ +**Added:** + +* migrated `labpdfproc` to a latest scikit-package level + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index b7ac149..2350565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,17 +45,14 @@ template = "{tag}" dev_template = "{tag}" dirty_template = "{tag}" -[project.scripts] -labpdfproc = "diffpy.labpdfproc.labpdfprocapp:main" - [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.labpdfproc.tests*"] # exclude packages matching these glob patterns (empty by default) +exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) -[tool.setuptools.package-data] -"diffpy.labpdfproc" = ["data/*"] +[project.scripts] +diffpy-labpdfproc = "diffpy.labpdfproc.app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} diff --git a/requirements/build.txt b/requirements/build.txt index f72d870..e69de29 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -1,2 +0,0 @@ -python -setuptools diff --git a/requirements/conda.txt b/requirements/conda.txt index e687e1c..24ce15a 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1,6 +1 @@ numpy -diffpy.utils -pandas -scipy -wxpython -gooey diff --git a/requirements/pip.txt b/requirements/pip.txt index 091d553..24ce15a 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,5 +1 @@ numpy -diffpy.utils -pandas -scipy -gooey diff --git a/requirements/tests.txt b/requirements/tests.txt index d9d0c8c..a727786 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -4,4 +4,3 @@ codecov coverage pytest-cov pytest-env -pytest-mock diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 263df6a..4b01c7f 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -1,14 +1,10 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2024-2025, The Trustees of Columbia University in the City of New York. +# (c) 2026 The Trustees of Columbia University in the City of New York. # All rights reserved. # -# (c) 2026-present, diffpy.labpdfproc developers and contributors. -# All rights reserved. -# -# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, -# Billinge Group members. +# File coded by: Billinge Group members and community contributors. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors diff --git a/src/diffpy/labpdfproc/__init__.py b/src/diffpy/labpdfproc/__init__.py index 694c534..601af7f 100644 --- a/src/diffpy/labpdfproc/__init__.py +++ b/src/diffpy/labpdfproc/__init__.py @@ -1,13 +1,11 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2024-2025, The Trustees of Columbia University in the City of New York. +# (c) 2026 The Trustees of Columbia University in the City of New York. # All rights reserved. # -# (c) 2026-present, diffpy.labpdfproc developers and contributors. -# All rights reserved. -# -# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, +# File coded by: Yucong Chen, Till +# Schertenleib, Caden Myers, # Billinge Group members. # # See GitHub contributions for a more detailed list of contributors. @@ -20,7 +18,7 @@ sources.""" # package version -from diffpy.labpdfproc.version import __version__ +from diffpy.labpdfproc.version import __version__ # noqa # silence the pyflakes syntax checker assert __version__ or True diff --git a/src/diffpy/labpdfproc/functions.py b/src/diffpy/labpdfproc/functions.py deleted file mode 100644 index 8d7a76a..0000000 --- a/src/diffpy/labpdfproc/functions.py +++ /dev/null @@ -1,307 +0,0 @@ -import math -import warnings -from importlib.resources import files - -import numpy as np -import pandas as pd -from scipy.interpolate import interp1d - -from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject - -RADIUS_MM = 1 -N_POINTS_ON_DIAMETER = 300 -TTH_GRID = np.arange(1, 180.1, 0.1) -# Round down the last element if it's slightly above 180.00 -# due to floating point precision -TTH_GRID[-1] = 180.00 -CVE_METHODS = ["brute_force", "polynomial_interpolation"] - -# Pre-computed datasets for polynomial interpolation (fast calculation) -data_dir = files("diffpy.labpdfproc") / "data" -MUD_LIST = np.array([0.5, 1, 2, 3, 4, 5, 6, 7]) -MULS = np.loadtxt(data_dir / "inverse_cve.xy") -COEFFICIENT_LIST = np.array( - pd.read_csv(data_dir / "coefficient_list.csv", header=None) -) -INTERPOLATION_FUNCTIONS = [ - interp1d(MUD_LIST, coeffs, kind="quadratic") for coeffs in COEFFICIENT_LIST -] - - -class Gridded_circle: - def __init__( - self, radius=1, n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=None - ): - self.radius = radius - self.npoints = n_points_on_diameter - self.mu = mu - self.distances = [] - self.muls = [] - self._get_grid_points() - - def _get_grid_points(self): - """Given a radius and a grid size, return a grid of points to - uniformly sample that circle.""" - xs = np.linspace(-self.radius, self.radius, self.npoints) - ys = np.linspace(-self.radius, self.radius, self.npoints) - self.grid = { - (x, y) for x in xs for y in ys if x**2 + y**2 <= self.radius**2 - } - self.total_points_in_grid = len(self.grid) - - def _get_entry_exit_coordinates(self, coordinate, angle): - """Get the coordinates where the beam enters and leaves the - circle for a given angle and grid point. - - It is calculated in the following way: - For the entry coordinate, - the y-component will be the y of the grid point - and the x-component will be minus - the value of x on the circle at the height of this y. - - For the exit coordinate: - Find the line y = ax + b that passes through grid_point at angle. - The circle is x^2 + y^2 = r^2. - The exit point is where these are simultaneous equations - x^2 + y^2 = r^2 & y = ax + b - x^2 + (ax+b)^2 = r^2 - => x^2 + a^2x^2 + 2abx + b^2 - r^2 = 0 - => (1+a^2) x^2 + 2abx + (b^2 - r^2) = 0 - to find x_exit we find the roots of these equations - and pick the root that is above y-grid - then we get y_exit from y_exit = a*x_exit + b. - - Parameters - ---------- - coordinate : tuple of floats - The coordinates of the grid point. - angle : float - The angle in degrees. - - Returns - ------- - (entry_point, exit_point): tuple of floats - (1) The coordinate of the entry point and - (2) of the exit point of a beam entering horizontally - impinging on a coordinate point that lies in the circle - and then exiting at some angle, angle. - """ - epsilon = 1e-7 # precision close to 90 - angle = math.radians(angle) - xgrid = coordinate[0] - ygrid = coordinate[1] - entry_point = (-math.sqrt(self.radius**2 - ygrid**2), ygrid) - if not math.isclose(angle, math.pi / 2, abs_tol=epsilon): - b = ygrid - xgrid * math.tan(angle) - a = math.tan(angle) - xexit_root1, xexit_root2 = np.roots( - (1 + a**2, 2 * a * b, b**2 - self.radius**2) - ) - yexit_root1 = a * xexit_root1 + b - yexit_root2 = a * xexit_root2 + b - if yexit_root2 >= yexit_root1: # We pick the point above - exit_point = (xexit_root2, yexit_root2) - else: - exit_point = (xexit_root1, yexit_root1) - else: - exit_point = (xgrid, math.sqrt(self.radius**2 - xgrid**2)) - return entry_point, exit_point - - def _get_path_length(self, grid_point, angle): - """Return the path length of a horizontal line entering the - circle at the same height to the grid point then exiting at - angle. - - Parameters - ---------- - grid_point : double of floats - The coordinate inside the circle. - angle : float - The angle of the output beam in degrees. - - Returns - ------- - (total distance, primary distance, secondary distance): tuple of floats - The tuple containing three floats, - which are the total distance, entry distance and exit distance. - """ - # move angle a tad above zero if it is zero - # to avoid it having the wrong sign due to some rounding error - angle_delta = 0.000001 - if angle == float(0): - angle = angle + angle_delta - entry, exit = self._get_entry_exit_coordinates(grid_point, angle) - primary_distance = math.dist(grid_point, entry) - secondary_distance = math.dist(grid_point, exit) - total_distance = primary_distance + secondary_distance - return total_distance, primary_distance, secondary_distance - - def set_distances_at_angle(self, angle): - """Given an angle, set the distances from the grid points to the - entry and exit coordinates. - - Parameters - ---------- - angle : float - The angle of the output beam in degrees. - """ - self.primary_distances = [] - self.secondary_distances = [] - self.distances = [] - for coord in self.grid: - distance, primary, secondary = self._get_path_length(coord, angle) - self.distances.append(distance) - self.primary_distances.append(primary) - self.secondary_distances.append(secondary) - - def set_muls_at_angle(self, angle): - """Compute muls = exp(-mu*distance) for a given angle. - - Parameters - ---------- - angle : float - The angle of the output beam in degrees. - """ - mu = self.mu - self.muls = [] - if len(self.distances) == 0: - self.set_distances_at_angle(angle) - for distance in self.distances: - self.muls.append(np.exp(-mu * distance)) - - -def _cve_brute_force(input_pattern, mud): - """Compute cve for the given mud on a global grid using the brute- - force method. - - Assume mu=mud/2, given that the same mu*D yields the same cve and - D/2=1. - """ - mu_sample_invmm = mud / 2 - abs_correction = Gridded_circle( - n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=mu_sample_invmm - ) - distances, muls = [], [] - for angle in TTH_GRID: - abs_correction.set_distances_at_angle(angle) - abs_correction.set_muls_at_angle(angle) - distances.append(sum(abs_correction.distances)) - muls.append(sum(abs_correction.muls)) - distances = np.array(distances) / abs_correction.total_points_in_grid - muls = np.array(muls) / abs_correction.total_points_in_grid - cve = 1 / muls - cve_do = DiffractionObject( - xarray=TTH_GRID, - yarray=cve, - xtype="tth", - wavelength=input_pattern.wavelength, - scat_quantity="cve", - name=f"absorption correction, cve, for {input_pattern.name}", - metadata=input_pattern.metadata, - ) - return cve_do - - -def _cve_polynomial_interpolation(input_pattern, mud): - """Compute cve using polynomial interpolation method, default to - brute- force computation if mu*D is out of the range (0.5 to 7).""" - if mud > 7 or mud < 0.5: - warnings.warn( - f"Input mu*D = {mud} is out of the acceptable range " - f"({np.min(MUD_LIST)} to {np.max(MUD_LIST)}) " - f"for polynomial interpolation. " - f"Proceeding with brute-force computation. " - ) - return _cve_brute_force(input_pattern, mud) - - coeffs = np.array([f(mud) for f in INTERPOLATION_FUNCTIONS]) - muls = np.polyval(coeffs, MULS) - cve = 1 / muls - cve_do = DiffractionObject( - xarray=TTH_GRID, - yarray=cve, - xtype="tth", - wavelength=input_pattern.wavelength, - scat_quantity="cve", - name=f"absorption correction, cve, for {input_pattern.name}", - metadata=input_pattern.metadata, - ) - return cve_do - - -def _cve_method(method): - """Retrieve the cve computation function for the given method.""" - methods = { - "brute_force": _cve_brute_force, - "polynomial_interpolation": _cve_polynomial_interpolation, - } - if method not in CVE_METHODS: - raise ValueError( - f"Unknown method: {method}. " - f"Allowed methods are {*CVE_METHODS, }." - ) - return methods[method] - - -def compute_cve( - input_pattern, mud, method="polynomial_interpolation", xtype="tth" -): - f"""Compute and interpolate the cylindrical volume effect (cve) - for the given input diffraction data and mu*D - using the selected method. - - Parameters - ---------- - input_pattern : DiffractionObject - The input diffraction object to which the cve will be applied. - mud : float - The mu*D value of the diffraction object, - where D is the diameter of the circle. - xtype : str - The quantity on the independent variable axis, - allowed values are {*XQUANTITIES, }. - method : str - The method used to calculate cve, must be one of {*CVE_METHODS, }. - - Returns - ------- - cve_do: DiffractionObject - The diffraction object that contains the cve to be applied. - """ - cve_function = _cve_method(method) - cve_do_on_global_grid = cve_function(input_pattern, mud) - orig_grid = input_pattern.on_xtype(xtype)[0] - global_xtype = cve_do_on_global_grid.on_xtype(xtype)[0] - cve_on_global_xtype = cve_do_on_global_grid.on_xtype(xtype)[1] - newcve = np.interp(orig_grid, global_xtype, cve_on_global_xtype) - cve_do = DiffractionObject( - xarray=orig_grid, - yarray=newcve, - xtype=xtype, - wavelength=input_pattern.wavelength, - scat_quantity="cve", - name=f"absorption correction, cve, for {input_pattern.name}", - metadata=input_pattern.metadata, - ) - return cve_do - - -def apply_corr(input_pattern, absorption_correction): - """Apply absorption correction to the given diffraction object with - the correction diffraction object. - - Parameters - ---------- - input_pattern : DiffractionObject - The input diffraction object to which the cve will be applied. - absorption_correction : DiffractionObject - The diffraction object that contains the cve to be applied. - - Returns - ------- - corrected_pattern: DiffractionObject - The corrected diffraction object - with the correction applied through multiplication. - """ - corrected_pattern = input_pattern * absorption_correction - return corrected_pattern diff --git a/src/diffpy/labpdfproc/labpdfproc_app.py b/src/diffpy/labpdfproc/labpdfproc_app.py new file mode 100644 index 0000000..33ac0fb --- /dev/null +++ b/src/diffpy/labpdfproc/labpdfproc_app.py @@ -0,0 +1,35 @@ +import argparse + +from diffpy.labpdfproc.version import __version__ # noqa + + +def main(): + parser = argparse.ArgumentParser( + prog="diffpy.labpdfproc", + description=( + "Tools for processing x-ray " + "powder diffraction data " + " from laboratory sources.\n\n" + "For more information, visit: " + "https://github.com/diffpy/diffpy.labpdfproc/" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", + action="store_true", + help="Show the program's version number and exit", + ) + + args = parser.parse_args() + + if args.version: + print(f"diffpy.labpdfproc {__version__}") + else: + # Default behavior when no arguments are given + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/src/diffpy/labpdfproc/version.py b/src/diffpy/labpdfproc/version.py index 26fc43b..44bbcfc 100644 --- a/src/diffpy/labpdfproc/version.py +++ b/src/diffpy/labpdfproc/version.py @@ -1,17 +1,14 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2024-2025, The Trustees of Columbia University in the City of New York. +# (c) 2026 The Trustees of Columbia University in the City of New York. # All rights reserved. # -# (c) 2026-present, diffpy.labpdfproc developers and contributors. -# All rights reserved. -# -# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, -# Billinge Group members. +# File coded by: Yucong Chen, Till Schertenleib, +# Caden Myers, Billinge Group members. # # See GitHub contributions for a more detailed list of contributors. -# https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors +# https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors # noqa: E501 # # See LICENSE.rst for license information. # diff --git a/tests/conftest.py b/tests/conftest.py index cda2974..e3b6313 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,85 +7,13 @@ @pytest.fixture def user_filesystem(tmp_path): base_dir = Path(tmp_path) - input_dir = base_dir / "input_dir" - input_dir.mkdir(parents=True, exist_ok=True) home_dir = base_dir / "home_dir" home_dir.mkdir(parents=True, exist_ok=True) - test_dir = base_dir / "test_dir" - test_dir.mkdir(parents=True, exist_ok=True) - output_dir = base_dir / "output_dir" - output_dir.mkdir(parents=True, exist_ok=True) - chi_data = ( - "dataformat = twotheta\n mode = " - "xray\n # chi_Q chi_I\n 1 2\n 3 4\n 5 6\n 7 8\n" - ) - xy_data = "1 2\n 3 4\n 5 6\n 7 8" - unreadable_data = ( - "This is a file with no data " "that is non-readable by LoadData" - ) - binary_data = b"\x00\x01\x02\x03\x04" + cwd_dir = base_dir / "cwd_dir" + cwd_dir.mkdir(parents=True, exist_ok=True) - with open(base_dir / "good_data.chi", "w") as f: - f.write(chi_data) - with open(base_dir / "good_data.xy", "w") as f: - f.write(xy_data) - with open(base_dir / "good_data.txt", "w") as f: - f.write(chi_data) - with open(base_dir / "unreadable_file.txt", "w") as f: - f.write(unreadable_data) - with open(base_dir / "binary.pkl", "wb") as f: - f.write(binary_data) - - with open(input_dir / "good_data.chi", "w") as f: - f.write(chi_data) - with open(input_dir / "good_data.xy", "w") as f: - f.write(xy_data) - with open(input_dir / "good_data.txt", "w") as f: - f.write(chi_data) - with open(input_dir / "unreadable_file.txt", "w") as f: - f.write(unreadable_data) - with open(input_dir / "binary.pkl", "wb") as f: - f.write(binary_data) - - with open(input_dir / "file_list.txt", "w") as f: - f.write( - "good_data.chi" - " \n good_data.xy" - " \n good_data.txt" - " \n missing_file.txt" - ) - with open(input_dir / "file_list_example2.txt", "w") as f: - f.write("input_dir/*.txt \n") - f.write("input_dir/good_data.chi \n") - f.write("good_data.xy \n") - f.write(f"{str(input_dir.resolve() / 'good_data.txt')}\n") - - with open(output_dir / "good_data-mud-corrected.chi", "w") as f: - f.write(chi_data) - with open(output_dir / "good_data-cve.chi", "w") as f: - f.write(chi_data) - home_config_data = { - "wavelength": 0.3, - "owner_name": "home_username", - "owner_email": "home@email.com", - "owner_orcid": "home_orcid", - } + 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) - z_scan_data = """ - -1.00000000 100000.00000000 - -0.77777778 100000.00000000 - -0.55555556 100000.00000000 - -0.33333333 10687.79256604 - -0.11111111 5366.53289631 - 0.11111111 5366.53289631 - 0.33333333 10687.79256604 - 0.55555556 100000.00000000 - 0.77777778 100000.00000000 - 1.00000000 100000.00000000 - """ - with open(test_dir / "testfile.xy", "w") as f: - f.write(z_scan_data) - yield tmp_path diff --git a/tests/test_functions.py b/tests/test_functions.py deleted file mode 100644 index 5671025..0000000 --- a/tests/test_functions.py +++ /dev/null @@ -1,222 +0,0 @@ -import re - -import numpy as np -import pytest - -from diffpy.labpdfproc.functions import ( - CVE_METHODS, - Gridded_circle, - apply_corr, - compute_cve, -) -from diffpy.utils.diffraction_objects import DiffractionObject - - -@pytest.mark.parametrize( - "inputs, expected_grid", - [ - ( - {"radius": 0.5, "n_points_on_diameter": 3, "mu": 1}, - {(0.0, -0.5), (0.0, 0.0), (0.5, 0.0), (-0.5, 0.0), (0.0, 0.5)}, - ), - ( - {"radius": 1, "n_points_on_diameter": 4, "mu": 1}, - { - (-0.333333, -0.333333), - (0.333333, -0.333333), - (-0.333333, 0.333333), - (0.333333, 0.333333), - }, - ), - ], -) -def test_get_grid_points(inputs, expected_grid): - actual_gs = Gridded_circle( - radius=inputs["radius"], - n_points_on_diameter=inputs["n_points_on_diameter"], - mu=inputs["mu"], - ) - actual_grid_sorted = sorted(actual_gs.grid) - expected_grid_sorted = sorted(expected_grid) - for actual_pt, expected_pt in zip( - actual_grid_sorted, expected_grid_sorted - ): - assert actual_pt == pytest.approx(expected_pt, rel=1e-4, abs=1e-6) - - -@pytest.mark.parametrize( - "inputs, expected_distances", - [ - ( - {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 45}, - [0, 1.4142135, 1.4142135, 2, 2], - ), - ( - {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 90}, - [0, 0, 2, 2, 2], - ), - ( - {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 120}, - [0, 0, 2, 3, 1.73205], - ), - ( - {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 30}, - [2.057347, 2.044451, 1.621801, 1.813330], - ), - ( - {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 90}, - [1.885618, 1.885618, 2.552285, 1.218951], - ), - ( - {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 140}, - [1.139021, 2.200102, 2.744909, 1.451264], - ), - ], -) -def test_set_distances_at_angle(inputs, expected_distances): - actual_gs = Gridded_circle( - radius=inputs["radius"], - n_points_on_diameter=inputs["n_points_on_diameter"], - mu=inputs["mu"], - ) - actual_gs.set_distances_at_angle(inputs["angle"]) - actual_distances_sorted = sorted(actual_gs.distances) - expected_distances_sorted = sorted(expected_distances) - assert actual_distances_sorted == pytest.approx( - expected_distances_sorted, rel=1e-4, abs=1e-6 - ) - - -@pytest.mark.parametrize( - "input_mu, expected_muls", - [ - (1, [1, 1, 0.135335, 0.049787, 0.176921]), - (2, [1, 1, 0.018316, 0.002479, 0.031301]), - ], -) -def test_set_muls_at_angle(input_mu, expected_muls): - actual_gs = Gridded_circle(radius=1, n_points_on_diameter=3, mu=input_mu) - actual_gs.set_muls_at_angle(120) - actual_muls_sorted = sorted(actual_gs.muls) - expected_muls_sorted = sorted(expected_muls) - assert actual_muls_sorted == pytest.approx( - expected_muls_sorted, rel=1e-4, abs=1e-6 - ) - - -@pytest.mark.parametrize( - "input_diffraction_data, input_cve_params", - [ # Test that cve diffraction object contains the expected info - # Note that all cve values are interpolated to 0.5 - # cve do should contain the same input xarray, xtype, - # wavelength, and metadata - ( # C1: User did not specify method, default to fast calculation - { - "xarray": np.array([90, 90.1, 90.2]), - "yarray": np.array([2, 2, 2]), - }, - {"mud": 1, "xtype": "tth"}, - ), - ( # C2: User specified brute-force computation method - { - "xarray": np.array([5.1, 5.2, 5.3]), - "yarray": np.array([2, 2, 2]), - }, - {"mud": 1, "method": "brute_force", "xtype": "q"}, - ), - ( # C3: User specified mu*D outside the fast calculation range, - # default to brute-force computation - { - "xarray": np.array([5.1, 5.2, 5.3]), - "yarray": np.array([2, 2, 2]), - }, - {"mud": 20, "xtype": "q"}, - ), - ], -) -def test_compute_cve(mocker, input_diffraction_data, input_cve_params): - expected_xarray = input_diffraction_data["xarray"] - expected_cve = np.array([0.5, 0.5, 0.5]) - expected_xtype = input_cve_params["xtype"] - mocker.patch("diffpy.labpdfproc.functions.N_POINTS_ON_DIAMETER", 4) - mocker.patch("numpy.interp", return_value=expected_cve) - input_pattern = DiffractionObject( - xarray=input_diffraction_data["xarray"], - yarray=input_diffraction_data["yarray"], - xtype=input_cve_params["xtype"], - wavelength=1.54, - scat_quantity="x-ray", - name="test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - actual_cve_do = compute_cve(input_pattern, **input_cve_params) - expected_cve_do = DiffractionObject( - xarray=expected_xarray, - yarray=expected_cve, - xtype=expected_xtype, - wavelength=1.54, - scat_quantity="cve", - name="absorption correction, cve, for test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - assert actual_cve_do == expected_cve_do - - -def test_compute_cve_bad(mocker): - xarray, yarray = np.array([90, 90.1, 90.2]), np.array([2, 2, 2]) - expected_cve = np.array([0.5, 0.5, 0.5]) - mocker.patch("numpy.interp", return_value=expected_cve) - input_pattern = DiffractionObject( - xarray=xarray, - yarray=yarray, - xtype="tth", - wavelength=1.54, - scat_quantity="x-ray", - name="test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - # Test that the function raises a ValueError - # when an invalid method is provided - with pytest.raises( - ValueError, - match=re.escape( - f"Unknown method: invalid_method. " - f"Allowed methods are {*CVE_METHODS, }." - ), - ): - compute_cve(input_pattern, mud=1, method="invalid_method") - - -def test_apply_corr(mocker): - xarray, yarray = np.array([90, 90.1, 90.2]), np.array([2, 2, 2]) - expected_cve = np.array([0.5, 0.5, 0.5]) - mocker.patch("numpy.interp", return_value=expected_cve) - input_pattern = DiffractionObject( - xarray=xarray, - yarray=yarray, - xtype="tth", - wavelength=1.54, - scat_quantity="x-ray", - name="test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - absorption_correction = DiffractionObject( - xarray=xarray, - yarray=expected_cve, - xtype="tth", - wavelength=1.54, - scat_quantity="cve", - name="absorption correction, cve, for test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - actual_corr = apply_corr(input_pattern, absorption_correction) - expected_corr = DiffractionObject( - xarray=xarray, - yarray=np.array([1, 1, 1]), - xtype="tth", - wavelength=1.54, - scat_quantity="x-ray", - name="test", - metadata={"thing1": 1, "thing2": "thing2"}, - ) - assert actual_corr == expected_corr diff --git a/tests/test_version.py b/tests/test_version.py index 994c681..139eb32 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,6 +1,6 @@ """Unit tests for __version__.py.""" -import diffpy.labpdfproc +import diffpy.labpdfproc # noqa def test_package_version(): From b9f9b3550f103ce1b9ac3b35498a2e72864ad7c0 Mon Sep 17 00:00:00 2001 From: Adib Kabir Date: Tue, 29 Sep 2026 17:23:28 -0700 Subject: [PATCH 2/5] skpkg: update labpdfproc to latest skpkg standard --- .../workflows/build-wheel-release-upload.yml | 64 +--- .github/workflows/check-news-item.yml | 2 +- .pre-commit-config.yaml | 4 +- CHANGELOG.rst | 109 +++++++ LICENSE.rst | 5 +- MANIFEST.in | 1 + README.rst | 34 +- cookiecutter.json | 2 +- docs/make.bat | 72 ++-- docs/source/api/diffpy.labpdfproc.rst | 32 +- docs/source/conf.py | 12 +- docs/source/index.rst | 17 +- docs/source/license.rst | 5 +- pyproject.toml | 9 +- requirements/build.txt | 2 + requirements/conda.txt | 5 + requirements/pip.txt | 4 + requirements/tests.txt | 1 + src/diffpy/__init__.py | 8 +- src/diffpy/labpdfproc/__init__.py | 10 +- src/diffpy/labpdfproc/functions.py | 307 ++++++++++++++++++ src/diffpy/labpdfproc/version.py | 11 +- tests/conftest.py | 78 ++++- tests/test_functions.py | 222 +++++++++++++ tests/test_version.py | 2 +- 25 files changed, 870 insertions(+), 148 deletions(-) create mode 100644 src/diffpy/labpdfproc/functions.py create mode 100644 tests/test_functions.py diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index cb88f6c..7f42d6d 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -1,76 +1,18 @@ -name: Build Wheel and Release +name: Build Wheel, Release on GitHub/PyPI, and Deploy Docs -# Trigger on tag push or manual dispatch. -# Tag and release privilege are verified inside the reusable workflow. on: workflow_dispatch: push: tags: - - "*" - -# ── Release modality ────────────────────────────────────────────────────────── -# Three options are provided below. Only ONE job should be active at a time. -# To switch: comment out the active job and uncomment your preferred option, -# then commit the change to main before tagging a release. -# ───────────────────────────────────────────────────────────────────────────── + - "*" # Trigger on all tags initially, but tag and release privilege are verified in _build-wheel-release-upload.yml jobs: - # Option 1 (default): Release to GitHub, publish to PyPI, and deploy docs. - # - # The wheel is uploaded to PyPI so users can install with `pip install`. - # A GitHub release is created with the changelog as the release body, and - # the Sphinx documentation is rebuilt and deployed to GitHub Pages. - # - # Choose this for open-source packages distributed via PyPI and/or - # conda-forge where broad public availability is the goal. build-release: uses: scikit-package/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.labpdfproc c_extension: false - maintainer_github_username: sbillinge + maintainer_GITHUB_username: sbillinge secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} - - # Option 2: Release to GitHub and deploy docs, without publishing to PyPI. - # - # A GitHub release is created and the Sphinx docs are deployed, but the - # wheel is not uploaded to PyPI. The source code remains publicly visible - # on GitHub and can be installed directly from there. - # - # Choose this when the package is public but you prefer to keep it off the - # default pip index — for example, if you distribute via conda-forge only, - # or if the package is not yet ready for a permanent PyPI presence. - # - # To use: comment out Option 1 above and uncomment the lines below. - # build-release-no-pypi: - # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-no-pypi.yml@v0 - # with: - # project: diffpy.labpdfproc - # c_extension: false - # maintainer_github_username: sbillinge - # secrets: - # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} - - # Option 3: Release to GitHub with wheel, license, and instructions bundled - # as a downloadable zip attached to the GitHub release asset. - # - # The wheel is built and packaged together with INSTRUCTIONS.txt and the - # LICENSE file into a zip that is attached directly to the GitHub release. - # Users with access to the (private) repo download the zip, follow the - # instructions inside, and install locally with pip. No PyPI or conda-forge - # upload occurs, and no docs are deployed. - # - # Choose this for private or restricted packages where distribution must be - # controlled: only users with repo access can download the release asset, - # making the GitHub release itself the distribution channel. - # - # To use: comment out Option 1 above and uncomment the lines below. - # build-release-private: - # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-private-pure.yml@v0 - # with: - # project: diffpy.labpdfproc - # maintainer_github_username: sbillinge - # secrets: - # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/check-news-item.yml b/.github/workflows/check-news-item.yml index a172da8..1f2a0cc 100644 --- a/.github/workflows/check-news-item.yml +++ b/.github/workflows/check-news-item.yml @@ -3,7 +3,7 @@ name: Check for News on: pull_request_target: branches: - - main # GitHub does not evaluate expressions in trigger filters; edit this value if your base branch is not main + - main jobs: check-news-item: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7554bee..9573476 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.14 + python: python3 ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit hooks @@ -52,7 +52,7 @@ repos: - tomli # prettier - multi formatter for .json, .yml, and .md files - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 + rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 hooks: - id: prettier additional_dependencies: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f29d3b5..3e10213 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,3 +3,112 @@ Release notes ============= .. current developments + +0.3.1 +===== + +**Added:** + +* Add back support for Python 3.10 and 3.11. + + +0.3.0 +===== + +**Added:** + +* Functionalities to estimate mu*D theoretically. +* Added published reference to ``README.rst``. +* Fast calculation supports values up to muD = 7 +* Recookiecut with updated ``scikit-package`` to enable docs preview in PRs. +* Updated package standards to scikit-package 0.3.0. +* Added ``cookiecutter.json`` file for the ``package update`` command. +* Functionality to read wavelength and anode type directly from a diffpy configuration file. +* Utility and example documentation for ``tools`` module. +* Gooey support so that the app can be run with GUI +* Coverage report in each PR +* doi in Readme for papers. +* Support for independent variables other than two-theta. +* new subcommand ``applymud`` to run the original absorption correction process through CLI. +* Utility and example documentation for the main module. +* Added documentation for new CLI updates. +* Documentation for functions module. +* Spelling check via Codespell in pre-commit +* Python 3.13 support +* Functionality in `load_user_info` to enable user to enter an ORCID. + +**Changed:** + +* Default to brute-force computation when muD < 0.5 or > 7. +* Print a warning message instead of error, explicitly stating the input muD value +* Functions that use DiffractionObject` in `diffpy.utils` to follow the new API. +* Workflow for loading wavelength - raise an error when both wavelength and anode type are specified. +* Readme: muD now requires the ``--mud`` flag instead of a required argument. +* Made muD an optional argument and provided different options (manually entry / z-scan file path) for users to specify muD +* Increased the number of significant figures for wavelength and separated values for Ka1 and Ka2. +* hyphens / underscores format according to new scikit-package group standard. +* GitHub workflows for renamed test file. +* Return a ``ValueError`` if no wavelength is found on config file or if its not specified. +* Compartmentalize commands into the subcommands ``mud``, ``zscan``, and ``sample``. See documentation for more info. +* Changed ``doc`` to ``docs`` and ``CODE_OF_CONDUCT.rst`` to ``CODE-OF-CONDUCT.rst`` to comply with scikit-package standards. +* All function docstrings and tests to be more informative, incorporating new ORCID function and improving overall clarity. + +**Fixed:** + +* duplicated wavelength information in output files + +**Removed:** + +* Remove the import of extend_path from pkgutil in diffpy/__init__.py since we are not strictly following the Python namespace package convention. + + +0.2.0 +===== + +**Added:** + +* Support for Python 3.13 + +**Removed:** + +* Support for Python 3.10 + + +0.1.3 +===== + +**Added:** + +* generate package API doc +* redo cookiecutter to add issue templates and update readme + + +0.1.2 +===== + +**Added:** + +* polynomial interpolation as the default method for cve computation. + +**Fixed:** + +* add PyPI packages under pip.txt + + + +0.1.1 +===== + + + +0.1.1 +===== + + + +0.1.0 +===== + + + +Initial release of labPDFproc. Please see README and documentation for details diff --git a/LICENSE.rst b/LICENSE.rst index 43d13fc..d5f6da9 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,6 +1,9 @@ BSD 3-Clause License -Copyright (c) 2026, The Trustees of Columbia University in the City of New York. +Copyright (c) 2025-2026, The Trustees of Columbia University in the City of New York. +All rights reserved. + +Copyright (c) 2026-present, diffpy.labpdfproc developers and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in index f1a78ee..ae36d2f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ graft tests graft requirements include AUTHORS.rst LICENSE*.rst README.rst +include src/diffpy/labpdfproc/data * # Exclude all bytecode files and __pycache__ directories global-exclude *.py[cod] # Exclude all .pyc, .pyo, and .pyd files. diff --git a/README.rst b/README.rst index 0c4a9fc..48b6c98 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,30 @@ Tools for processing x-ray powder diffraction data from laboratory sources. -* LONGER DESCRIPTION HERE +PDFgetX3 has revolutionized how PDF methods can be applied to solve nanostructure problems. +However, the program was designed for use with Rapid Acquisition PDF (RAPDF) data from synchrotron sources. +A key approximation inherent in the use of PDFgetX3 for RAPDF data is that absorption effects are negligible. +This is typically not the case for laboratory x-ray diffractometers, where absorption effects can be significant. + +This app is designed to preprocess data from laboratory x-ray diffractometers before using PDFgetX3 to obtain PDFs. +The app currently carries out an absorption correction assuming a parallel beam capillary geometry +which is the most common geometry for lab PDF measurements. + +The theory is described in the following paper: + + Chen, Y., Schertenleib, T., Yang, A., Schouwink, P., Queen, W. L., and Billinge, S. J. L., + *Absorption Correction for Reliable Pair Distribution Functions from Low Energy X-ray Sources*. + Crystal Growth & Design, 2026, 26 (3), 1036–1047. + https://doi.org/10.1021/acs.cgd.5c00551 + + +The related experimental data acquisition protocols are described in the following paper: + + Schertenleib, T., Schmuckler, D., Chen, Y., Jin, G. B., Queen, W. L., and Billinge, S. J. L. (2025). + *Testing Protocols for Obtaining Reliable Pair Distribution Functions from Laboratory X-Ray Sources Using PDFgetX3*. + Chem. Methods, 2500001. + https://doi.org/10.1002/cmtd.202500001 + For more information about the diffpy.labpdfproc library, please consult our `online documentation `_. @@ -47,7 +70,10 @@ Citation If you use diffpy.labpdfproc in a scientific publication, we would like you to cite this package as - diffpy.labpdfproc Package, https://github.com/diffpy/diffpy.labpdfproc + Chen, Y., Schertenleib, T., Yang, A., Schouwink, P., Queen, W. L., and Billinge, S. J. L., + *Absorption Correction for Reliable Pair Distribution Functions from Low Energy X-ray Sources*. + Crystal Growth & Design, 2026, 26 (3), 1036–1047. + https://doi.org/10.1021/acs.cgd.5c00551 Installation ------------ @@ -82,7 +108,7 @@ and run the following :: This package also provides command-line utilities. To check the software has been installed correctly, type :: - diffpy.labpdfproc --version + labpdfproc --version You can also type the following command to verify the installation. :: @@ -91,7 +117,7 @@ You can also type the following command to verify the installation. :: To view the basic usage and available commands, type :: - diffpy.labpdfproc -h + labpdfproc -h Getting Started --------------- diff --git a/cookiecutter.json b/cookiecutter.json index 4e72ae9..7c013d4 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,8 +5,8 @@ "maintainer_emails": "sbillinge@ucsb.edu", "maintainer_github_usernames": "sbillinge", "contributors": "Yucong Chen, Till Schertenleib, Caden Myers, Billinge Group members", - "project_name": "diffpy.labpdfproc", "license_holders": "The Trustees of Columbia University in the City of New York", + "project_name": "diffpy.labpdfproc", "github_username_or_orgname": "diffpy", "github_repo_name": "diffpy.labpdfproc", "conda_pypi_package_dist_name": "diffpy.labpdfproc", diff --git a/docs/make.bat b/docs/make.bat index ac53d5b..2be8306 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=PackagingScientificPython - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=PackagingScientificPython + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/api/diffpy.labpdfproc.rst b/docs/source/api/diffpy.labpdfproc.rst index 1bcc98d..a3f53e1 100644 --- a/docs/source/api/diffpy.labpdfproc.rst +++ b/docs/source/api/diffpy.labpdfproc.rst @@ -1,9 +1,7 @@ :tocdepth: -1 -|title| -======= - -.. |title| replace:: diffpy.labpdfproc package +diffpy.labpdfproc package +========================= .. automodule:: diffpy.labpdfproc :members: @@ -14,17 +12,33 @@ Subpackages ----------- .. toctree:: - diffpy.labpdfproc.example_package + :titlesonly: + + diffpy.labpdfproc.data Submodules ---------- -|module| --------- +diffpy.labpdfproc.functions module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.labpdfproc.functions + :members: + :undoc-members: + :show-inheritance: + +diffpy.labpdfproc.tools module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.labpdfproc.tools + :members: + :undoc-members: + :show-inheritance: -.. |module| replace:: diffpy.labpdfproc.example_submodule module +diffpy.labpdfproc.labpdfprocapp module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: diffpy.labpdfproc.example_submodule +.. automodule:: diffpy.labpdfproc.labpdfprocapp :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index e759486..4ef7771 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# diffpy.labpdfproc documentation build configuration file, created by # noqa: E501 +# diffpy.labpdfproc 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 @@ -22,11 +22,14 @@ try: fullversion = version("diffpy.labpdfproc") except Exception: - fullversion = "No version found. The correct version will appear in the released version." # noqa: E501 + fullversion = ( + "No version found. " + "The correct version will appear in the released version." + ) # 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 Path().resolve() to make it absolute, like shown here. # noqa: E501 +# 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())) @@ -105,6 +108,9 @@ # directories to ignore when looking for source files. exclude_patterns = ["build"] +# Mock imports so API docs render without installing dependencies. +autodoc_mock_imports = ["diffpy.utils"] + # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None diff --git a/docs/source/index.rst b/docs/source/index.rst index 64de3f9..1bf6d0f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,14 +9,6 @@ | Software version |release| | Last updated |today|. -=============== -Getting started -=============== - -Welcome to the ``diffpy.labpdfproc`` documentation! - -To get started, please visit the :ref:`Getting started ` page. - ======= Authors ======= @@ -41,12 +33,13 @@ Acknowledgements Table of contents ================= .. toctree:: - :maxdepth: 2 + :titlesonly: - getting-started - Package API - release license + release + Utilities + Examples + Package API ======= Indices diff --git a/docs/source/license.rst b/docs/source/license.rst index 7428e28..2abd4ff 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -9,7 +9,10 @@ OPEN SOURCE LICENSE AGREEMENT ============================= BSD 3-Clause License -Copyright (c) 2026, The Trustees of Columbia University in the City of New York. +Copyright (c) 2024-2025, The Trustees of Columbia University in the City of New York. +All Rights Reserved. + +Copyright (c) 2026-present, diffpy.labpdfproc developers and contributors. All Rights Reserved. Redistribution and use in source and binary forms, with or without diff --git a/pyproject.toml b/pyproject.toml index 2350565..b7ac149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,14 +45,17 @@ template = "{tag}" dev_template = "{tag}" dirty_template = "{tag}" +[project.scripts] +labpdfproc = "diffpy.labpdfproc.labpdfprocapp:main" + [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 = [] # exclude packages matching these glob patterns (empty by default) +exclude = ["diffpy.labpdfproc.tests*"] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) -[project.scripts] -diffpy-labpdfproc = "diffpy.labpdfproc.app:main" +[tool.setuptools.package-data] +"diffpy.labpdfproc" = ["data/*"] [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} diff --git a/requirements/build.txt b/requirements/build.txt index e69de29..f72d870 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -0,0 +1,2 @@ +python +setuptools diff --git a/requirements/conda.txt b/requirements/conda.txt index 24ce15a..e687e1c 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1 +1,6 @@ numpy +diffpy.utils +pandas +scipy +wxpython +gooey diff --git a/requirements/pip.txt b/requirements/pip.txt index 24ce15a..091d553 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1 +1,5 @@ numpy +diffpy.utils +pandas +scipy +gooey diff --git a/requirements/tests.txt b/requirements/tests.txt index a727786..d9d0c8c 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -4,3 +4,4 @@ codecov coverage pytest-cov pytest-env +pytest-mock diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 4b01c7f..263df6a 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -1,10 +1,14 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2026 The Trustees of Columbia University in the City of New York. +# (c) 2024-2025, The Trustees of Columbia University in the City of New York. # All rights reserved. # -# File coded by: Billinge Group members and community contributors. +# (c) 2026-present, diffpy.labpdfproc developers and contributors. +# All rights reserved. +# +# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, +# Billinge Group members. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors diff --git a/src/diffpy/labpdfproc/__init__.py b/src/diffpy/labpdfproc/__init__.py index 601af7f..694c534 100644 --- a/src/diffpy/labpdfproc/__init__.py +++ b/src/diffpy/labpdfproc/__init__.py @@ -1,11 +1,13 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2026 The Trustees of Columbia University in the City of New York. +# (c) 2024-2025, The Trustees of Columbia University in the City of New York. # All rights reserved. # -# File coded by: Yucong Chen, Till -# Schertenleib, Caden Myers, +# (c) 2026-present, diffpy.labpdfproc developers and contributors. +# All rights reserved. +# +# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, # Billinge Group members. # # See GitHub contributions for a more detailed list of contributors. @@ -18,7 +20,7 @@ sources.""" # package version -from diffpy.labpdfproc.version import __version__ # noqa +from diffpy.labpdfproc.version import __version__ # silence the pyflakes syntax checker assert __version__ or True diff --git a/src/diffpy/labpdfproc/functions.py b/src/diffpy/labpdfproc/functions.py new file mode 100644 index 0000000..8d7a76a --- /dev/null +++ b/src/diffpy/labpdfproc/functions.py @@ -0,0 +1,307 @@ +import math +import warnings +from importlib.resources import files + +import numpy as np +import pandas as pd +from scipy.interpolate import interp1d + +from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject + +RADIUS_MM = 1 +N_POINTS_ON_DIAMETER = 300 +TTH_GRID = np.arange(1, 180.1, 0.1) +# Round down the last element if it's slightly above 180.00 +# due to floating point precision +TTH_GRID[-1] = 180.00 +CVE_METHODS = ["brute_force", "polynomial_interpolation"] + +# Pre-computed datasets for polynomial interpolation (fast calculation) +data_dir = files("diffpy.labpdfproc") / "data" +MUD_LIST = np.array([0.5, 1, 2, 3, 4, 5, 6, 7]) +MULS = np.loadtxt(data_dir / "inverse_cve.xy") +COEFFICIENT_LIST = np.array( + pd.read_csv(data_dir / "coefficient_list.csv", header=None) +) +INTERPOLATION_FUNCTIONS = [ + interp1d(MUD_LIST, coeffs, kind="quadratic") for coeffs in COEFFICIENT_LIST +] + + +class Gridded_circle: + def __init__( + self, radius=1, n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=None + ): + self.radius = radius + self.npoints = n_points_on_diameter + self.mu = mu + self.distances = [] + self.muls = [] + self._get_grid_points() + + def _get_grid_points(self): + """Given a radius and a grid size, return a grid of points to + uniformly sample that circle.""" + xs = np.linspace(-self.radius, self.radius, self.npoints) + ys = np.linspace(-self.radius, self.radius, self.npoints) + self.grid = { + (x, y) for x in xs for y in ys if x**2 + y**2 <= self.radius**2 + } + self.total_points_in_grid = len(self.grid) + + def _get_entry_exit_coordinates(self, coordinate, angle): + """Get the coordinates where the beam enters and leaves the + circle for a given angle and grid point. + + It is calculated in the following way: + For the entry coordinate, + the y-component will be the y of the grid point + and the x-component will be minus + the value of x on the circle at the height of this y. + + For the exit coordinate: + Find the line y = ax + b that passes through grid_point at angle. + The circle is x^2 + y^2 = r^2. + The exit point is where these are simultaneous equations + x^2 + y^2 = r^2 & y = ax + b + x^2 + (ax+b)^2 = r^2 + => x^2 + a^2x^2 + 2abx + b^2 - r^2 = 0 + => (1+a^2) x^2 + 2abx + (b^2 - r^2) = 0 + to find x_exit we find the roots of these equations + and pick the root that is above y-grid + then we get y_exit from y_exit = a*x_exit + b. + + Parameters + ---------- + coordinate : tuple of floats + The coordinates of the grid point. + angle : float + The angle in degrees. + + Returns + ------- + (entry_point, exit_point): tuple of floats + (1) The coordinate of the entry point and + (2) of the exit point of a beam entering horizontally + impinging on a coordinate point that lies in the circle + and then exiting at some angle, angle. + """ + epsilon = 1e-7 # precision close to 90 + angle = math.radians(angle) + xgrid = coordinate[0] + ygrid = coordinate[1] + entry_point = (-math.sqrt(self.radius**2 - ygrid**2), ygrid) + if not math.isclose(angle, math.pi / 2, abs_tol=epsilon): + b = ygrid - xgrid * math.tan(angle) + a = math.tan(angle) + xexit_root1, xexit_root2 = np.roots( + (1 + a**2, 2 * a * b, b**2 - self.radius**2) + ) + yexit_root1 = a * xexit_root1 + b + yexit_root2 = a * xexit_root2 + b + if yexit_root2 >= yexit_root1: # We pick the point above + exit_point = (xexit_root2, yexit_root2) + else: + exit_point = (xexit_root1, yexit_root1) + else: + exit_point = (xgrid, math.sqrt(self.radius**2 - xgrid**2)) + return entry_point, exit_point + + def _get_path_length(self, grid_point, angle): + """Return the path length of a horizontal line entering the + circle at the same height to the grid point then exiting at + angle. + + Parameters + ---------- + grid_point : double of floats + The coordinate inside the circle. + angle : float + The angle of the output beam in degrees. + + Returns + ------- + (total distance, primary distance, secondary distance): tuple of floats + The tuple containing three floats, + which are the total distance, entry distance and exit distance. + """ + # move angle a tad above zero if it is zero + # to avoid it having the wrong sign due to some rounding error + angle_delta = 0.000001 + if angle == float(0): + angle = angle + angle_delta + entry, exit = self._get_entry_exit_coordinates(grid_point, angle) + primary_distance = math.dist(grid_point, entry) + secondary_distance = math.dist(grid_point, exit) + total_distance = primary_distance + secondary_distance + return total_distance, primary_distance, secondary_distance + + def set_distances_at_angle(self, angle): + """Given an angle, set the distances from the grid points to the + entry and exit coordinates. + + Parameters + ---------- + angle : float + The angle of the output beam in degrees. + """ + self.primary_distances = [] + self.secondary_distances = [] + self.distances = [] + for coord in self.grid: + distance, primary, secondary = self._get_path_length(coord, angle) + self.distances.append(distance) + self.primary_distances.append(primary) + self.secondary_distances.append(secondary) + + def set_muls_at_angle(self, angle): + """Compute muls = exp(-mu*distance) for a given angle. + + Parameters + ---------- + angle : float + The angle of the output beam in degrees. + """ + mu = self.mu + self.muls = [] + if len(self.distances) == 0: + self.set_distances_at_angle(angle) + for distance in self.distances: + self.muls.append(np.exp(-mu * distance)) + + +def _cve_brute_force(input_pattern, mud): + """Compute cve for the given mud on a global grid using the brute- + force method. + + Assume mu=mud/2, given that the same mu*D yields the same cve and + D/2=1. + """ + mu_sample_invmm = mud / 2 + abs_correction = Gridded_circle( + n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=mu_sample_invmm + ) + distances, muls = [], [] + for angle in TTH_GRID: + abs_correction.set_distances_at_angle(angle) + abs_correction.set_muls_at_angle(angle) + distances.append(sum(abs_correction.distances)) + muls.append(sum(abs_correction.muls)) + distances = np.array(distances) / abs_correction.total_points_in_grid + muls = np.array(muls) / abs_correction.total_points_in_grid + cve = 1 / muls + cve_do = DiffractionObject( + xarray=TTH_GRID, + yarray=cve, + xtype="tth", + wavelength=input_pattern.wavelength, + scat_quantity="cve", + name=f"absorption correction, cve, for {input_pattern.name}", + metadata=input_pattern.metadata, + ) + return cve_do + + +def _cve_polynomial_interpolation(input_pattern, mud): + """Compute cve using polynomial interpolation method, default to + brute- force computation if mu*D is out of the range (0.5 to 7).""" + if mud > 7 or mud < 0.5: + warnings.warn( + f"Input mu*D = {mud} is out of the acceptable range " + f"({np.min(MUD_LIST)} to {np.max(MUD_LIST)}) " + f"for polynomial interpolation. " + f"Proceeding with brute-force computation. " + ) + return _cve_brute_force(input_pattern, mud) + + coeffs = np.array([f(mud) for f in INTERPOLATION_FUNCTIONS]) + muls = np.polyval(coeffs, MULS) + cve = 1 / muls + cve_do = DiffractionObject( + xarray=TTH_GRID, + yarray=cve, + xtype="tth", + wavelength=input_pattern.wavelength, + scat_quantity="cve", + name=f"absorption correction, cve, for {input_pattern.name}", + metadata=input_pattern.metadata, + ) + return cve_do + + +def _cve_method(method): + """Retrieve the cve computation function for the given method.""" + methods = { + "brute_force": _cve_brute_force, + "polynomial_interpolation": _cve_polynomial_interpolation, + } + if method not in CVE_METHODS: + raise ValueError( + f"Unknown method: {method}. " + f"Allowed methods are {*CVE_METHODS, }." + ) + return methods[method] + + +def compute_cve( + input_pattern, mud, method="polynomial_interpolation", xtype="tth" +): + f"""Compute and interpolate the cylindrical volume effect (cve) + for the given input diffraction data and mu*D + using the selected method. + + Parameters + ---------- + input_pattern : DiffractionObject + The input diffraction object to which the cve will be applied. + mud : float + The mu*D value of the diffraction object, + where D is the diameter of the circle. + xtype : str + The quantity on the independent variable axis, + allowed values are {*XQUANTITIES, }. + method : str + The method used to calculate cve, must be one of {*CVE_METHODS, }. + + Returns + ------- + cve_do: DiffractionObject + The diffraction object that contains the cve to be applied. + """ + cve_function = _cve_method(method) + cve_do_on_global_grid = cve_function(input_pattern, mud) + orig_grid = input_pattern.on_xtype(xtype)[0] + global_xtype = cve_do_on_global_grid.on_xtype(xtype)[0] + cve_on_global_xtype = cve_do_on_global_grid.on_xtype(xtype)[1] + newcve = np.interp(orig_grid, global_xtype, cve_on_global_xtype) + cve_do = DiffractionObject( + xarray=orig_grid, + yarray=newcve, + xtype=xtype, + wavelength=input_pattern.wavelength, + scat_quantity="cve", + name=f"absorption correction, cve, for {input_pattern.name}", + metadata=input_pattern.metadata, + ) + return cve_do + + +def apply_corr(input_pattern, absorption_correction): + """Apply absorption correction to the given diffraction object with + the correction diffraction object. + + Parameters + ---------- + input_pattern : DiffractionObject + The input diffraction object to which the cve will be applied. + absorption_correction : DiffractionObject + The diffraction object that contains the cve to be applied. + + Returns + ------- + corrected_pattern: DiffractionObject + The corrected diffraction object + with the correction applied through multiplication. + """ + corrected_pattern = input_pattern * absorption_correction + return corrected_pattern diff --git a/src/diffpy/labpdfproc/version.py b/src/diffpy/labpdfproc/version.py index 44bbcfc..26fc43b 100644 --- a/src/diffpy/labpdfproc/version.py +++ b/src/diffpy/labpdfproc/version.py @@ -1,14 +1,17 @@ #!/usr/bin/env python ############################################################################## # -# (c) 2026 The Trustees of Columbia University in the City of New York. +# (c) 2024-2025, The Trustees of Columbia University in the City of New York. # All rights reserved. # -# File coded by: Yucong Chen, Till Schertenleib, -# Caden Myers, Billinge Group members. +# (c) 2026-present, diffpy.labpdfproc developers and contributors. +# All rights reserved. +# +# File coded by: Yucong Chen, Till Schertenleib, Caden Myers, +# Billinge Group members. # # See GitHub contributions for a more detailed list of contributors. -# https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors # noqa: E501 +# https://github.com/diffpy/diffpy.labpdfproc/graphs/contributors # # See LICENSE.rst for license information. # diff --git a/tests/conftest.py b/tests/conftest.py index e3b6313..cda2974 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,85 @@ @pytest.fixture def user_filesystem(tmp_path): base_dir = Path(tmp_path) + input_dir = base_dir / "input_dir" + input_dir.mkdir(parents=True, exist_ok=True) 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) + test_dir = base_dir / "test_dir" + test_dir.mkdir(parents=True, exist_ok=True) + output_dir = base_dir / "output_dir" + output_dir.mkdir(parents=True, exist_ok=True) + chi_data = ( + "dataformat = twotheta\n mode = " + "xray\n # chi_Q chi_I\n 1 2\n 3 4\n 5 6\n 7 8\n" + ) + xy_data = "1 2\n 3 4\n 5 6\n 7 8" + unreadable_data = ( + "This is a file with no data " "that is non-readable by LoadData" + ) + binary_data = b"\x00\x01\x02\x03\x04" - home_config_data = {"username": "home_username", "email": "home@email.com"} + with open(base_dir / "good_data.chi", "w") as f: + f.write(chi_data) + with open(base_dir / "good_data.xy", "w") as f: + f.write(xy_data) + with open(base_dir / "good_data.txt", "w") as f: + f.write(chi_data) + with open(base_dir / "unreadable_file.txt", "w") as f: + f.write(unreadable_data) + with open(base_dir / "binary.pkl", "wb") as f: + f.write(binary_data) + + with open(input_dir / "good_data.chi", "w") as f: + f.write(chi_data) + with open(input_dir / "good_data.xy", "w") as f: + f.write(xy_data) + with open(input_dir / "good_data.txt", "w") as f: + f.write(chi_data) + with open(input_dir / "unreadable_file.txt", "w") as f: + f.write(unreadable_data) + with open(input_dir / "binary.pkl", "wb") as f: + f.write(binary_data) + + with open(input_dir / "file_list.txt", "w") as f: + f.write( + "good_data.chi" + " \n good_data.xy" + " \n good_data.txt" + " \n missing_file.txt" + ) + with open(input_dir / "file_list_example2.txt", "w") as f: + f.write("input_dir/*.txt \n") + f.write("input_dir/good_data.chi \n") + f.write("good_data.xy \n") + f.write(f"{str(input_dir.resolve() / 'good_data.txt')}\n") + + with open(output_dir / "good_data-mud-corrected.chi", "w") as f: + f.write(chi_data) + with open(output_dir / "good_data-cve.chi", "w") as f: + f.write(chi_data) + home_config_data = { + "wavelength": 0.3, + "owner_name": "home_username", + "owner_email": "home@email.com", + "owner_orcid": "home_orcid", + } with open(home_dir / "diffpyconfig.json", "w") as f: json.dump(home_config_data, f) + z_scan_data = """ + -1.00000000 100000.00000000 + -0.77777778 100000.00000000 + -0.55555556 100000.00000000 + -0.33333333 10687.79256604 + -0.11111111 5366.53289631 + 0.11111111 5366.53289631 + 0.33333333 10687.79256604 + 0.55555556 100000.00000000 + 0.77777778 100000.00000000 + 1.00000000 100000.00000000 + """ + with open(test_dir / "testfile.xy", "w") as f: + f.write(z_scan_data) + yield tmp_path diff --git a/tests/test_functions.py b/tests/test_functions.py new file mode 100644 index 0000000..5671025 --- /dev/null +++ b/tests/test_functions.py @@ -0,0 +1,222 @@ +import re + +import numpy as np +import pytest + +from diffpy.labpdfproc.functions import ( + CVE_METHODS, + Gridded_circle, + apply_corr, + compute_cve, +) +from diffpy.utils.diffraction_objects import DiffractionObject + + +@pytest.mark.parametrize( + "inputs, expected_grid", + [ + ( + {"radius": 0.5, "n_points_on_diameter": 3, "mu": 1}, + {(0.0, -0.5), (0.0, 0.0), (0.5, 0.0), (-0.5, 0.0), (0.0, 0.5)}, + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1}, + { + (-0.333333, -0.333333), + (0.333333, -0.333333), + (-0.333333, 0.333333), + (0.333333, 0.333333), + }, + ), + ], +) +def test_get_grid_points(inputs, expected_grid): + actual_gs = Gridded_circle( + radius=inputs["radius"], + n_points_on_diameter=inputs["n_points_on_diameter"], + mu=inputs["mu"], + ) + actual_grid_sorted = sorted(actual_gs.grid) + expected_grid_sorted = sorted(expected_grid) + for actual_pt, expected_pt in zip( + actual_grid_sorted, expected_grid_sorted + ): + assert actual_pt == pytest.approx(expected_pt, rel=1e-4, abs=1e-6) + + +@pytest.mark.parametrize( + "inputs, expected_distances", + [ + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 45}, + [0, 1.4142135, 1.4142135, 2, 2], + ), + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 90}, + [0, 0, 2, 2, 2], + ), + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 120}, + [0, 0, 2, 3, 1.73205], + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 30}, + [2.057347, 2.044451, 1.621801, 1.813330], + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 90}, + [1.885618, 1.885618, 2.552285, 1.218951], + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 140}, + [1.139021, 2.200102, 2.744909, 1.451264], + ), + ], +) +def test_set_distances_at_angle(inputs, expected_distances): + actual_gs = Gridded_circle( + radius=inputs["radius"], + n_points_on_diameter=inputs["n_points_on_diameter"], + mu=inputs["mu"], + ) + actual_gs.set_distances_at_angle(inputs["angle"]) + actual_distances_sorted = sorted(actual_gs.distances) + expected_distances_sorted = sorted(expected_distances) + assert actual_distances_sorted == pytest.approx( + expected_distances_sorted, rel=1e-4, abs=1e-6 + ) + + +@pytest.mark.parametrize( + "input_mu, expected_muls", + [ + (1, [1, 1, 0.135335, 0.049787, 0.176921]), + (2, [1, 1, 0.018316, 0.002479, 0.031301]), + ], +) +def test_set_muls_at_angle(input_mu, expected_muls): + actual_gs = Gridded_circle(radius=1, n_points_on_diameter=3, mu=input_mu) + actual_gs.set_muls_at_angle(120) + actual_muls_sorted = sorted(actual_gs.muls) + expected_muls_sorted = sorted(expected_muls) + assert actual_muls_sorted == pytest.approx( + expected_muls_sorted, rel=1e-4, abs=1e-6 + ) + + +@pytest.mark.parametrize( + "input_diffraction_data, input_cve_params", + [ # Test that cve diffraction object contains the expected info + # Note that all cve values are interpolated to 0.5 + # cve do should contain the same input xarray, xtype, + # wavelength, and metadata + ( # C1: User did not specify method, default to fast calculation + { + "xarray": np.array([90, 90.1, 90.2]), + "yarray": np.array([2, 2, 2]), + }, + {"mud": 1, "xtype": "tth"}, + ), + ( # C2: User specified brute-force computation method + { + "xarray": np.array([5.1, 5.2, 5.3]), + "yarray": np.array([2, 2, 2]), + }, + {"mud": 1, "method": "brute_force", "xtype": "q"}, + ), + ( # C3: User specified mu*D outside the fast calculation range, + # default to brute-force computation + { + "xarray": np.array([5.1, 5.2, 5.3]), + "yarray": np.array([2, 2, 2]), + }, + {"mud": 20, "xtype": "q"}, + ), + ], +) +def test_compute_cve(mocker, input_diffraction_data, input_cve_params): + expected_xarray = input_diffraction_data["xarray"] + expected_cve = np.array([0.5, 0.5, 0.5]) + expected_xtype = input_cve_params["xtype"] + mocker.patch("diffpy.labpdfproc.functions.N_POINTS_ON_DIAMETER", 4) + mocker.patch("numpy.interp", return_value=expected_cve) + input_pattern = DiffractionObject( + xarray=input_diffraction_data["xarray"], + yarray=input_diffraction_data["yarray"], + xtype=input_cve_params["xtype"], + wavelength=1.54, + scat_quantity="x-ray", + name="test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + actual_cve_do = compute_cve(input_pattern, **input_cve_params) + expected_cve_do = DiffractionObject( + xarray=expected_xarray, + yarray=expected_cve, + xtype=expected_xtype, + wavelength=1.54, + scat_quantity="cve", + name="absorption correction, cve, for test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + assert actual_cve_do == expected_cve_do + + +def test_compute_cve_bad(mocker): + xarray, yarray = np.array([90, 90.1, 90.2]), np.array([2, 2, 2]) + expected_cve = np.array([0.5, 0.5, 0.5]) + mocker.patch("numpy.interp", return_value=expected_cve) + input_pattern = DiffractionObject( + xarray=xarray, + yarray=yarray, + xtype="tth", + wavelength=1.54, + scat_quantity="x-ray", + name="test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + # Test that the function raises a ValueError + # when an invalid method is provided + with pytest.raises( + ValueError, + match=re.escape( + f"Unknown method: invalid_method. " + f"Allowed methods are {*CVE_METHODS, }." + ), + ): + compute_cve(input_pattern, mud=1, method="invalid_method") + + +def test_apply_corr(mocker): + xarray, yarray = np.array([90, 90.1, 90.2]), np.array([2, 2, 2]) + expected_cve = np.array([0.5, 0.5, 0.5]) + mocker.patch("numpy.interp", return_value=expected_cve) + input_pattern = DiffractionObject( + xarray=xarray, + yarray=yarray, + xtype="tth", + wavelength=1.54, + scat_quantity="x-ray", + name="test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + absorption_correction = DiffractionObject( + xarray=xarray, + yarray=expected_cve, + xtype="tth", + wavelength=1.54, + scat_quantity="cve", + name="absorption correction, cve, for test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + actual_corr = apply_corr(input_pattern, absorption_correction) + expected_corr = DiffractionObject( + xarray=xarray, + yarray=np.array([1, 1, 1]), + xtype="tth", + wavelength=1.54, + scat_quantity="x-ray", + name="test", + metadata={"thing1": 1, "thing2": "thing2"}, + ) + assert actual_corr == expected_corr diff --git a/tests/test_version.py b/tests/test_version.py index 139eb32..994c681 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,6 +1,6 @@ """Unit tests for __version__.py.""" -import diffpy.labpdfproc # noqa +import diffpy.labpdfproc def test_package_version(): From 74b5fc922c880ef63724f7a8aa524ff68433bbcd Mon Sep 17 00:00:00 2001 From: Adib Kabir Date: Tue, 29 Sep 2026 17:58:06 -0700 Subject: [PATCH 3/5] skpkg: updated labpdfproc to latest version according to skpkg standard --- .../workflows/build-wheel-release-upload.yml | 64 ++++++++++++++++- .github/workflows/check-news-item.yml | 2 +- .pre-commit-config.yaml | 4 +- docs/make.bat | 72 +++++++++---------- docs/source/conf.py | 12 +--- pyproject.toml | 9 +-- tests/test_version.py | 2 +- 7 files changed, 107 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index 7f42d6d..cb88f6c 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -1,18 +1,76 @@ -name: Build Wheel, Release on GitHub/PyPI, and Deploy Docs +name: Build Wheel and Release +# Trigger on tag push or manual dispatch. +# Tag and release privilege are verified inside the reusable workflow. on: workflow_dispatch: push: tags: - - "*" # Trigger on all tags initially, but tag and release privilege are verified in _build-wheel-release-upload.yml + - "*" + +# ── Release modality ────────────────────────────────────────────────────────── +# Three options are provided below. Only ONE job should be active at a time. +# To switch: comment out the active job and uncomment your preferred option, +# then commit the change to main before tagging a release. +# ───────────────────────────────────────────────────────────────────────────── jobs: + # Option 1 (default): Release to GitHub, publish to PyPI, and deploy docs. + # + # The wheel is uploaded to PyPI so users can install with `pip install`. + # A GitHub release is created with the changelog as the release body, and + # the Sphinx documentation is rebuilt and deployed to GitHub Pages. + # + # Choose this for open-source packages distributed via PyPI and/or + # conda-forge where broad public availability is the goal. build-release: uses: scikit-package/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.labpdfproc c_extension: false - maintainer_GITHUB_username: sbillinge + maintainer_github_username: sbillinge secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + + # Option 2: Release to GitHub and deploy docs, without publishing to PyPI. + # + # A GitHub release is created and the Sphinx docs are deployed, but the + # wheel is not uploaded to PyPI. The source code remains publicly visible + # on GitHub and can be installed directly from there. + # + # Choose this when the package is public but you prefer to keep it off the + # default pip index — for example, if you distribute via conda-forge only, + # or if the package is not yet ready for a permanent PyPI presence. + # + # To use: comment out Option 1 above and uncomment the lines below. + # build-release-no-pypi: + # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-no-pypi.yml@v0 + # with: + # project: diffpy.labpdfproc + # c_extension: false + # maintainer_github_username: sbillinge + # secrets: + # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + + # Option 3: Release to GitHub with wheel, license, and instructions bundled + # as a downloadable zip attached to the GitHub release asset. + # + # The wheel is built and packaged together with INSTRUCTIONS.txt and the + # LICENSE file into a zip that is attached directly to the GitHub release. + # Users with access to the (private) repo download the zip, follow the + # instructions inside, and install locally with pip. No PyPI or conda-forge + # upload occurs, and no docs are deployed. + # + # Choose this for private or restricted packages where distribution must be + # controlled: only users with repo access can download the release asset, + # making the GitHub release itself the distribution channel. + # + # To use: comment out Option 1 above and uncomment the lines below. + # build-release-private: + # uses: scikit-package/release-scripts/.github/workflows/_build-release-github-private-pure.yml@v0 + # with: + # project: diffpy.labpdfproc + # maintainer_github_username: sbillinge + # secrets: + # PAT_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/check-news-item.yml b/.github/workflows/check-news-item.yml index 1f2a0cc..a172da8 100644 --- a/.github/workflows/check-news-item.yml +++ b/.github/workflows/check-news-item.yml @@ -3,7 +3,7 @@ name: Check for News on: pull_request_target: branches: - - main + - main # GitHub does not evaluate expressions in trigger filters; edit this value if your base branch is not main jobs: check-news-item: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9573476..7554bee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3 + python: python3.14 ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit hooks @@ -52,7 +52,7 @@ repos: - tomli # prettier - multi formatter for .json, .yml, and .md files - repo: https://github.com/pre-commit/mirrors-prettier - rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 + rev: v4.0.0-alpha.8 hooks: - id: prettier additional_dependencies: diff --git a/docs/make.bat b/docs/make.bat index 2be8306..ac53d5b 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=PackagingScientificPython - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=PackagingScientificPython + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py index 4ef7771..e759486 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# diffpy.labpdfproc documentation build configuration file, created by +# diffpy.labpdfproc documentation build configuration file, created by # noqa: E501 # sphinx-quickstart on Thu Jan 30 15:49:41 2014. # # This file is execfile()d with the current directory set to its @@ -22,14 +22,11 @@ try: fullversion = version("diffpy.labpdfproc") except Exception: - fullversion = ( - "No version found. " - "The correct version will appear in the released version." - ) + fullversion = "No version found. The correct version will appear in the released version." # noqa: E501 # 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 Path().resolve() to make it absolute, like shown here +# documentation root, use Path().resolve() to make it absolute, like shown here. # noqa: E501 # sys.path.insert(0, str(Path(".").resolve())) sys.path.insert(0, str(Path("../..").resolve())) sys.path.insert(0, str(Path("../../src").resolve())) @@ -108,9 +105,6 @@ # directories to ignore when looking for source files. exclude_patterns = ["build"] -# Mock imports so API docs render without installing dependencies. -autodoc_mock_imports = ["diffpy.utils"] - # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None diff --git a/pyproject.toml b/pyproject.toml index b7ac149..2350565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,17 +45,14 @@ template = "{tag}" dev_template = "{tag}" dirty_template = "{tag}" -[project.scripts] -labpdfproc = "diffpy.labpdfproc.labpdfprocapp:main" - [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.labpdfproc.tests*"] # exclude packages matching these glob patterns (empty by default) +exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) -[tool.setuptools.package-data] -"diffpy.labpdfproc" = ["data/*"] +[project.scripts] +diffpy-labpdfproc = "diffpy.labpdfproc.app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} diff --git a/tests/test_version.py b/tests/test_version.py index 994c681..139eb32 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,6 +1,6 @@ """Unit tests for __version__.py.""" -import diffpy.labpdfproc +import diffpy.labpdfproc # noqa def test_package_version(): From e99010ebe7fd5be8e1217c3654a75344946e935d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 29 Sep 2026 21:44:48 -0400 Subject: [PATCH 4/5] chore: add Adib's change that could not do his laptop --- docs/source/conf.py | 3 +++ pyproject.toml | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index e759486..6871539 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -105,6 +105,9 @@ # directories to ignore when looking for source files. exclude_patterns = ["build"] +# Mock imports so API docs render without installing dependencies. +autodoc_mock_imports = ["diffpy.utils"] + # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None diff --git a/pyproject.toml b/pyproject.toml index 2350565..dec1fa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,12 @@ 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 = [] # exclude packages matching these glob patterns (empty by default) +exclude = ["diffpy.labpdfproc.tests*"] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) +[tool.setuptools.package-data] +"diffpy.labpdfproc" = ["data/*"] + [project.scripts] diffpy-labpdfproc = "diffpy.labpdfproc.app:main" From 7d56dfe5f44e5488b2f6b9184e736414871deec6 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 29 Sep 2026 21:46:36 -0400 Subject: [PATCH 5/5] chore: fix the cli name: --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dec1fa0..809f615 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ namespaces = false # to disable scanning PEP 420 namespaces (true by default) "diffpy.labpdfproc" = ["data/*"] [project.scripts] -diffpy-labpdfproc = "diffpy.labpdfproc.app:main" +labpdfproc = "diffpy.labpdfproc.app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]}