diff --git a/.gitignore b/.gitignore index 1f74b6f04..94e2d56e1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *.swp *.swo *DS_Store +_version.py .tox/ build/ diff --git a/CHANGELOG b/CHANGELOG index 60a24e3c0..6dface96a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,9 @@ The rules for CHANGELOG file: .. inclusion-marker-changelog-start +Unreleased +---------- + 0.3.0 (2025/06/12) ------------------ - Add ``_BasePCov`` class (#248) diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..35c82c7c1 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,28 @@ +cff-version: 1.2.0 +message: "If you use scikit-matter for your work, please read and cite it as below." +title: >- + scikit-matter : a suite of generalisable machine learning methods born out of chemistry and materials science [version 2; peer review: 3 approved, 1 approved with reservations] +type: journalArticle +issue: 81 +volume: 3 +authors: + - family-names: Goscinski + given-names: Alexander + - family-names: Principe + given-names: Victor P. + - family-names: Fraux + given-names: Guillaume + - family-names: Kliavinek + given-names: Sergei + - family-names: Helfrecht + given-names: Benjamin A. + - family-names: Loche + given-names: Philip + - family-names: Ceriotti + given-names: Michele + - family-names: Cersonsky + given-names: Rose K. +date-published: 2023 +identifiers: + - type: doi + value: 10.12688/openreseurope.15789.2 diff --git a/MANIFEST.in b/MANIFEST.in index be2c55e83..fad9ed6a0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ graft src include LICENSE +include CITATION.cff include README.rst prune docs diff --git a/README.rst b/README.rst index 35d4a32e9..2b0ef1531 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,16 @@ scikit-matter ============= -|tests| |codecov| |pypi| |conda| |docs| |doi| + +|tests| |codecov| |pypi| |conda| |docs-stable| |docs-latest| |doi| A collection of ``scikit-learn`` compatible utilities that implement methods born out of the materials science and chemistry communities. -For details, tutorials, and examples, please have a look at our `documentation`_. +For details, tutorials, and examples, please have a look at our documentation_. We also +provide a `latest documentation`_ from the current unreleased development version. -.. _`documentation`: https://scikit-matter.readthedocs.io +.. _`documentation`: https://scikit-matter.readthedocs.io/en/v0.3/ +.. _`latest documentation`: https://scikit-matter.readthedocs.io/en/latest .. marker-installation @@ -98,9 +101,13 @@ Thanks goes to all people that make scikit-matter possible: :alt: Code coverage :target: https://codecov.io/gh/scikit-learn-contrib/scikit-matter/ -.. |docs| image:: https://img.shields.io/badge/📚_documentation-latest-sucess - :alt: Python - :target: documentation_ +.. |docs-stable| image:: https://img.shields.io/badge/📚_Documentation-stable-sucess + :alt: Documentation of stable released version + :target: `documentation`_ + +.. |docs-latest| image:: https://img.shields.io/badge/📒_Documentation-latest-yellow.svg + :alt: Documentation of latest unreleased version + :target: `latest documentation`_ .. |pypi| image:: https://img.shields.io/pypi/v/skmatter.svg :alt: Latest PYPI version diff --git a/docs/src/contributing.rst b/docs/src/contributing.rst index 09f6d088b..dfe477502 100644 --- a/docs/src/contributing.rst +++ b/docs/src/contributing.rst @@ -212,3 +212,49 @@ properly. It should look something like this: You're good to go! Time to submit a `pull request. `_ + +How to Perform a Release +------------------------- + +1. **Prepare a Release Pull Request** + + - Based on the main branch create branch ``release-x.y.z`` and a PR. + - Ensure that all `CI tests + `_ pass. + - Optionally, run the tests locally to double-check. + +2. **Update the Changelog** + + - Edit the changelog located in ``CHANGELOG``: + - Add a new section for the new version, summarizing the changes based on the + PRs merged since the last release. + - Leave a placeholder section titled *Unreleased* for future updates. + +3. **Merge the PR and Create a Tag** + + - Merge the release PR. + - Update the ``main`` branch and check that the latest commit is the release PR with + ``git log`` + - Create a tag on directly the ``main`` branch. + - Push the tag to GitHub. For example for a release of version ``x.y.z``: + + .. code-block:: bash + + git checkout main + git pull + git tag -a vx.y.z -m "Release vx.y.z" + git push --tags + +4. **Finalize the GitHub Release** + + - Once the PR is merged, the CI will automatically: + - Publish the package to PyPI. + - Create a draft release on GitHub. + - Update the GitHub release notes by pasting the changelog for the version. + +5. **Merge Conda Recipe Changes** + + - May resolve and then merge an automatically created PR on the `conda recipe + `_. + - Once thus PR is merged and the new version will be published automatically on the + `conda-forge `_ channel. diff --git a/examples/neighbors/pamm.py b/examples/neighbors/pamm.py index db36b4cb2..cba44e412 100644 --- a/examples/neighbors/pamm.py +++ b/examples/neighbors/pamm.py @@ -17,7 +17,6 @@ of the H-bond motif. """ - # %% from typing import Callable, Union @@ -150,7 +149,6 @@ def _update_cluster_cov( idxroot: np.ndarray, center_idx: np.ndarray, ): - if cell is not None: cov = _get_lcov_clusterp( len(X), nsamples, X, idxroot, center_idx[k], probs, cell @@ -194,7 +192,6 @@ def _get_lcov_cluster( probs: np.ndarray, cell: np.ndarray, ): - ww = np.zeros(N) normww = logsumexp(probs[clroots == idcl]) ww[clroots == idcl] = np.exp(probs[clroots == idcl] - normww) @@ -211,7 +208,6 @@ def _get_lcov_clusterp( probs: np.ndarray, cell: np.ndarray, ): - ww = np.zeros(N) totnormp = logsumexp(probs) cov = np.zeros((x.shape[1], x.shape[1]), dtype=float) diff --git a/examples/neighbors/sparse-kde.py b/examples/neighbors/sparse-kde.py index a8148d42a..723fd451c 100644 --- a/examples/neighbors/sparse-kde.py +++ b/examples/neighbors/sparse-kde.py @@ -20,7 +20,6 @@ Here we first sample from these three Gaussians. """ - # %% import time @@ -98,7 +97,6 @@ # %% class GaussianMixtureModel: - def __init__( self, weights: np.ndarray, @@ -116,7 +114,6 @@ def __init__( self.norm = 1 / np.sqrt((2 * np.pi) ** self.dimension * self.cov_det) def __call__(self, x: np.ndarray, i: int = None): - if len(x.shape) == 1: x = x[np.newaxis, :] if self.period is not None: diff --git a/examples/pcovr/PCovR.py b/examples/pcovr/PCovR.py index 1a222ff24..072a8b152 100644 --- a/examples/pcovr/PCovR.py +++ b/examples/pcovr/PCovR.py @@ -8,7 +8,6 @@ # %% # - import numpy as np from matplotlib import cm from matplotlib import pyplot as plt diff --git a/examples/pcovr/PCovR_Regressors.py b/examples/pcovr/PCovR_Regressors.py index 5600c15d2..15523c371 100644 --- a/examples/pcovr/PCovR_Regressors.py +++ b/examples/pcovr/PCovR_Regressors.py @@ -4,6 +4,7 @@ Choosing Different Regressors for PCovR ======================================= """ + # %% # import time diff --git a/examples/reconstruction/PlotLFRE.py b/examples/reconstruction/PlotLFRE.py index ead5d131f..17f7c8cbd 100644 --- a/examples/reconstruction/PlotLFRE.py +++ b/examples/reconstruction/PlotLFRE.py @@ -20,7 +20,6 @@ # %% # - import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np diff --git a/examples/reconstruction/PlotPointwiseGFRE.py b/examples/reconstruction/PlotPointwiseGFRE.py index 256b6011c..6abdabe9d 100644 --- a/examples/reconstruction/PlotPointwiseGFRE.py +++ b/examples/reconstruction/PlotPointwiseGFRE.py @@ -16,7 +16,6 @@ # %% # - import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np diff --git a/examples/regression/Ridge2FoldCVRegularization.py b/examples/regression/Ridge2FoldCVRegularization.py index cee2eebbb..2329f4e58 100644 --- a/examples/regression/Ridge2FoldCVRegularization.py +++ b/examples/regression/Ridge2FoldCVRegularization.py @@ -114,11 +114,15 @@ ) sklearn_ridge_2foldcv_tikhonov = RidgeCV( - alphas=alphas, cv=cv, fit_intercept=False # remove the incluence of learning bias + alphas=alphas, + cv=cv, + fit_intercept=False, # remove the incluence of learning bias ) sklearn_ridge_loocv_tikhonov = RidgeCV( - alphas=alphas, cv=None, fit_intercept=False # remove the incluence of learning bias + alphas=alphas, + cv=None, + fit_intercept=False, # remove the incluence of learning bias ) # %% @@ -331,7 +335,9 @@ def get_train_test_error(estimator): ) sklearn_ridge_loocv_tikhonov = RidgeCV( - alphas=alphas, cv=None, fit_intercept=False # remove the incluence of learning bias + alphas=alphas, + cv=None, + fit_intercept=False, # remove the incluence of learning bias ) print("skmatter 2-fold CV cutoff") diff --git a/examples/selection/FeatureSelection.py b/examples/selection/FeatureSelection.py index 917a9d0ff..b02fea44d 100644 --- a/examples/selection/FeatureSelection.py +++ b/examples/selection/FeatureSelection.py @@ -4,6 +4,7 @@ PCovR-Inspired Feature Selection ================================ """ + # %% # import numpy as np diff --git a/examples/selection/Selectors-Pipelines.py b/examples/selection/Selectors-Pipelines.py index e92ff6b04..9c4387555 100644 --- a/examples/selection/Selectors-Pipelines.py +++ b/examples/selection/Selectors-Pipelines.py @@ -9,7 +9,6 @@ # %% # - import numpy as np from matplotlib import pyplot as plt from sklearn.datasets import load_diabetes diff --git a/pyproject.toml b/pyproject.toml index 6c92104e6..02d259a44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools", + "setuptools >= 77", + "setuptools_scm >= 8", "wheel", ] build-backend = "setuptools.build_meta" @@ -57,12 +58,18 @@ repository = "https://github.com/scikit-learn-contrib/scikit-matter" issues = "https://github.com/scikit-learn-contrib/scikit-matterissues" changelog = "http://scikit-matter.readthedocs.io/en/latest/changelog.html" +[tool.check-manifest] +ignore = ["src/skmatter/_version.py"] + [tool.setuptools.packages.find] where = ["src"] [tool.setuptools.dynamic] version = {attr = "skmatter.__version__"} +[tool.setuptools_scm] +version_file = "src/skmatter/_version.py" + [tool.coverage.run] branch = true data_file = 'tests/.coverage' @@ -75,15 +82,6 @@ include = [ [tool.coverage.xml] output = 'tests/coverage.xml' -[tool.isort] -skip = "__init__.py" -profile = "black" -line_length = 88 -indent = 4 -include_trailing_comma = true -lines_after_imports = 2 -known_first_party = "skmatter" - [tool.pytest.ini_options] testpaths = ["tests"] addopts = [ @@ -94,7 +92,7 @@ addopts = [ ] [tool.ruff] -exclude = ["docs/src/examples/"] +exclude = ["docs/src/examples/", "src/torchpme/_version.py"] lint.ignore = [ "F401", "E203", diff --git a/src/skmatter/__init__.py b/src/skmatter/__init__.py index 4b0f10326..b37a4b702 100644 --- a/src/skmatter/__init__.py +++ b/src/skmatter/__init__.py @@ -7,4 +7,4 @@ coding guidelines to promote usability and interoperability with existing workflows. """ -__version__ = "0.3.0" +from ._version import __version__ # noqa: F401 diff --git a/src/skmatter/_selection.py b/src/skmatter/_selection.py index d1ab7e4fd..8c6406203 100644 --- a/src/skmatter/_selection.py +++ b/src/skmatter/_selection.py @@ -1059,7 +1059,6 @@ def __init__( full=False, random_state=0, ): - self.mixing = mixing self.initialize = initialize diff --git a/src/skmatter/decomposition/_kernel_pcovr.py b/src/skmatter/decomposition/_kernel_pcovr.py index e47e771fb..54ee8855c 100644 --- a/src/skmatter/decomposition/_kernel_pcovr.py +++ b/src/skmatter/decomposition/_kernel_pcovr.py @@ -238,7 +238,7 @@ def _fit(self, K, Yhat, W): _, S, Vt = self._decompose_truncated(K_tilde) else: raise ValueError( - "Unrecognized svd_solver='{0}'" "".format(self._fit_svd_solver) + "Unrecognized svd_solver='{0}'".format(self._fit_svd_solver) ) U = Vt.T diff --git a/src/skmatter/decomposition/_pcov.py b/src/skmatter/decomposition/_pcov.py index 972f097dc..878b22ff9 100644 --- a/src/skmatter/decomposition/_pcov.py +++ b/src/skmatter/decomposition/_pcov.py @@ -136,7 +136,7 @@ def _fit_sample_space(self, X, Y, Yhat, W, compute_pty_=True): U, S, Vt = self._decompose_truncated(Kt) else: raise ValueError( - "Unrecognized svd_solver='{0}'" "".format(self.fit_svd_solver_) + "Unrecognized svd_solver='{0}'".format(self.fit_svd_solver_) ) self.singular_values_ = np.sqrt(S.copy()) @@ -231,7 +231,7 @@ def _decompose_full(self, mat): if self.n_components_ == "mle": if self.n_samples_in_ < self.n_features_in_: raise ValueError( - "n_components='mle' is only supported " "if n_samples >= n_features" + "n_components='mle' is only supported if n_samples >= n_features" ) elif ( not 0 <= self.n_components_ <= min(self.n_samples_in_, self.n_features_in_) diff --git a/src/skmatter/metrics/_pairwise.py b/src/skmatter/metrics/_pairwise.py index 4455a7b3a..dd0773a7b 100644 --- a/src/skmatter/metrics/_pairwise.py +++ b/src/skmatter/metrics/_pairwise.py @@ -150,7 +150,6 @@ def pairwise_mahalanobis_distances( def _mahalanobis( cell: np.ndarray, X: np.ndarray, Y: np.ndarray, cov_inv: np.ndarray ): - XY = np.concatenate([x - Y for x in X]) if cell is not None: XY -= np.round(XY / cell) * cell diff --git a/src/skmatter/neighbors/_sparsekde.py b/src/skmatter/neighbors/_sparsekde.py index bbb7c1ea9..337381c25 100644 --- a/src/skmatter/neighbors/_sparsekde.py +++ b/src/skmatter/neighbors/_sparsekde.py @@ -296,7 +296,6 @@ def _check_dimension(self, X): raise ValueError("Cell dimension does not match the data dimension.") def _assign_descriptors_to_grids(self, X): - assigner = _NearestGridAssigner(self.metric, self.metric_params, self.verbose) assigner.fit(X) labels = assigner.predict(self.descriptors, sample_weight=self.weights) @@ -411,7 +410,6 @@ def _bandwidth_estimation_from_localization(self, X, wlocal, flocal, idx): return h, cov def _computes_kernel_density_estimation(self, X: np.ndarray): - prob = np.full(len(X), -np.inf) dummd1s_mat = pairwise_mahalanobis_distances( X, self._grids, self._bandwidth_inv, self.cell, squared=True @@ -488,7 +486,6 @@ def __init__( metric_params: Union[dict, None] = None, verbose: bool = False, ) -> None: - self.labels_ = None self.metric = metric self.metric_params = metric_params diff --git a/src/skmatter/preprocessing/_data.py b/src/skmatter/preprocessing/_data.py index 35ed36828..16b1dba86 100644 --- a/src/skmatter/preprocessing/_data.py +++ b/src/skmatter/preprocessing/_data.py @@ -475,8 +475,7 @@ def fit(self, Knm, Kmm, y=None, sample_weight=None): """ if Knm.shape[1] != Kmm.shape[0]: raise ValueError( - "The reference kernel is not commensurate shape with the " - "active kernel." + "The reference kernel is not commensurate shape with the active kernel." ) if Kmm.shape[0] != Kmm.shape[1]: diff --git a/tests/test_kernel_pcovr.py b/tests/test_kernel_pcovr.py index 9b6e0fb25..aeaf30dff 100644 --- a/tests/test_kernel_pcovr.py +++ b/tests/test_kernel_pcovr.py @@ -430,7 +430,7 @@ def test_bad_solver(self): kpcovr = self.model(svd_solver="bad") kpcovr.fit(self.X, self.Y) - self.assertTrue(str(cm.exception), "Unrecognized svd_solver='bad'" "") + self.assertTrue(str(cm.exception), "Unrecognized svd_solver='bad'") def test_good_n_components(self): """Check that PCovR will work with any allowed values of n_components.""" diff --git a/tests/test_metrics.py b/tests/test_metrics.py index cdaaf5519..9ccd57477 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -49,8 +49,7 @@ def test_local_prediction_rigidity(self): ) self.assertTrue( rank_diff == 0, - f"LPR Covariance matrix rank is not full, with a difference of:" - f"{rank_diff}", + f"LPR Covariance matrix rank is not full, with a difference of:{rank_diff}", ) def test_componentwise_prediction_rigidity(self): @@ -217,7 +216,6 @@ def test_local_reconstruction_error_test_idx(self): class DistanceTests(unittest.TestCase): - @classmethod def setUpClass(cls): cls.X = np.array([[1, 2], [3, 4], [5, 6]]) diff --git a/tests/test_pcovc.py b/tests/test_pcovc.py index 57e501270..dfd546035 100644 --- a/tests/test_pcovc.py +++ b/tests/test_pcovc.py @@ -265,7 +265,7 @@ def test_bad_solver(self): pcovc = self.model(svd_solver="bad", space=space) pcovc.fit(self.X, self.Y) - self.assertEqual(str(cm.exception), "Unrecognized svd_solver='bad'" "") + self.assertEqual(str(cm.exception), "Unrecognized svd_solver='bad'") def test_good_n_components(self): """Check that PCovC will work with any allowed values of n_components.""" @@ -291,7 +291,7 @@ def test_bad_n_components(self): pcovc.fit(self.X[:20], self.Y[:20]) self.assertEqual( str(cm.exception), - "n_components='mle' is only supported " "if n_samples >= n_features", + "n_components='mle' is only supported if n_samples >= n_features", ) with self.subTest(type="negative_ncomponents"): diff --git a/tests/test_pcovr.py b/tests/test_pcovr.py index 284a7e778..27f99e8e7 100644 --- a/tests/test_pcovr.py +++ b/tests/test_pcovr.py @@ -261,7 +261,7 @@ def test_bad_solver(self): pcovr = self.model(svd_solver="bad", space=space) pcovr.fit(self.X, self.Y) - self.assertEqual(str(cm.exception), "Unrecognized svd_solver='bad'" "") + self.assertEqual(str(cm.exception), "Unrecognized svd_solver='bad'") def test_good_n_components(self): """Check that PCovR will work with any allowed values of n_components.""" @@ -285,7 +285,7 @@ def test_bad_n_components(self): pcovr.fit(self.X[:2], self.Y[:2]) self.assertEqual( str(cm.exception), - "n_components='mle' is only supported " "if n_samples >= n_features", + "n_components='mle' is only supported if n_samples >= n_features", ) with self.subTest(type="negative_ncomponents"): diff --git a/tox.ini b/tox.ini index 61ebd7e5d..35cb69eff 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,6 @@ description = Runs the tests usedevelop = true changedir = tests deps = - ase parameterized pytest pytest-cov @@ -50,16 +49,11 @@ commands = description = Checks the code and doc for programmatic and stylistic errors skip_install = true deps = - black - blackdoc ruff - isort sphinx-lint commands = + ruff format --diff {[tox]lint_folders} ruff check {[tox]lint_folders} - black --check --diff {[tox]lint_folders} - blackdoc --check --diff {[tox]lint_folders} - isort --check-only --diff {[tox]lint_folders} sphinx-lint --enable all --max-line-length 88 \ -i "{toxinidir}/docs/src/examples" \ {[tox]lint_folders} "{toxinidir}/README.rst" @@ -73,15 +67,9 @@ description = skip_install = true deps = ruff - black - blackdoc - isort commands = - format: ruff check --fix {[tox]lint_folders} - format-unsafe: ruff check --fix --unsafe-fixes {[tox]lint_folders} - black {[tox]lint_folders} - blackdoc {[tox]lint_folders} - isort {[tox]lint_folders} + ruff format {[tox]lint_folders} + ruff check --fix-only {[tox]lint_folders} "{toxinidir}/README.rst" {posargs} [testenv:docs] description = Builds the documentation