Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v9.0.0
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.3.2
with:
python-version: "3.14"
enable-cache: true
Expand All @@ -28,8 +28,8 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v9.0.0
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.3.2
with:
python-version: "3.14"
enable-cache: true
Expand All @@ -48,9 +48,9 @@ jobs:

runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v7.0.0
- name: Set up uv and Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v9.0.0
uses: astral-sh/setup-uv@v8.3.2
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v9.0.0
- uses: astral-sh/setup-uv@v8.3.2
with:
python-version: "3.14"
enable-cache: true
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v7.0.0
- name: Set up uv
uses: astral-sh/setup-uv@v9.0.0
uses: astral-sh/setup-uv@v8.3.2
with:
python-version: "3.14"
enable-cache: true
Expand All @@ -31,7 +31,7 @@ jobs:
- name: Build package
run: uv build
- name: Publish package
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
"adjustText>=0.8",
"openTSNE>=1.0.0",
"llvmlite>=0.40",
"pacmap>=0.7.0",
]

[project.optional-dependencies]
Expand Down
35 changes: 35 additions & 0 deletions src/elementembeddings/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import numpy as np
import pandas as pd
from openTSNE import TSNE
from pacmap import PaCMAP
from scipy.stats import energy_distance, pearsonr, spearmanr, wasserstein_distance
from sklearn import decomposition
from sklearn.metrics import DistanceMetric
Expand Down Expand Up @@ -260,6 +261,40 @@ def calculate_umap(self, n_components: int = 2, standardise: bool = True, **kwar
umap = UMAP(n_components=n_components, **kwargs)
return umap.fit_transform(embeddings_array)

def calculate_pacmap(
self,
n_components: int = 2,
standardise: bool = True,
init: str = "pca",
**kwargs,
):
Comment on lines +264 to +270

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left off for consistency with the sibling reducers — calculate_pca / calculate_tsne / calculate_umap are also unannotated. Glad to add -> np.ndarray across all four reducer methods if you'd like them annotated together.

"""Calculate PaCMAP (Pairwise Controlled Manifold Approximation) embeddings.

Args:
----
n_components (int): The number of components to project the embeddings to.
standardise (bool): Whether to scale the embeddings before projecting.
init (str): Initialisation of the low-dimensional embedding, passed to
``PaCMAP.fit_transform`` (e.g. ``"pca"`` or ``"random"``).
**kwargs: Other keyword arguments to be passed to PaCMAP.
"""
if standardise:
if self.is_standardised:
embeddings_array = np.array(list(self.embeddings.values()))
else:
self.standardise(inplace=True)
embeddings_array = np.array(list(self.embeddings.values()))
else:
warnings.warn(
"""It is recommended to scale the embeddings
before projecting with PaCMAP.
To do so, set `standardise=True`.""",
)
Comment on lines +288 to +292

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning mirrors the wording and triple-quoted format of the existing calculate_pca / calculate_tsne / calculate_umap warnings verbatim, so I kept it identical for consistency. On stacklevel: B028 is in the ignore list in pyproject.toml and none of the sibling warnings set it, so adding it only here would be inconsistent. Happy to do a repo-wide stacklevel=2 pass (and drop the B028 ignore) as a separate cleanup if you'd prefer.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
embeddings_array = np.array(list(self.embeddings.values()))

pacmap = PaCMAP(n_components=n_components, **kwargs)
return pacmap.fit_transform(embeddings_array, init=init)

def compute_correlation_metric(
self,
el_sp1: str,
Expand Down
7 changes: 5 additions & 2 deletions src/elementembeddings/plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ def dimension_plotter(
embedding (Embedding): The embedding to be plotted.
ax (plt.axes, optional): The axes to plot on, by default None
n_components (int): The number of components to reduce to, by default 2
reducer (str): The dimensionality reduction algorithm to use, by default "umap"
reducer (str): The dimensionality reduction algorithm to use. One of
"umap", "tsne", "pca" or "pacmap", by default "umap"
Comment on lines +118 to +119

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6089661 — the dispatch ValueError now includes the invalid value and lists the accepted reducers ('umap', 'tsne', 'pca', 'pacmap'). On the docstring: the accepted-values continuation line is indented to match the existing convention in this function's docstring (e.g. the reducer_params description wraps at the same level), so I kept it consistent with the surrounding args.

adjusttext (bool): Whether to avoid overlap of the text labels, by default True
reducer_params (dict, optional): Additional keyword arguments to pass to
the reducer, by default None
Expand All @@ -132,8 +133,10 @@ def dimension_plotter(
reduced = embedding.calculate_tsne(n_components=n_components, **reducer_params)
elif reducer == "pca":
reduced = embedding.calculate_pca(n_components=n_components, **reducer_params)
elif reducer == "pacmap":
reduced = embedding.calculate_pacmap(n_components=n_components, **reducer_params)
else:
msg = "Unrecognised reducer."
msg = f"Unrecognised reducer: {reducer!r}. Expected one of 'umap', 'tsne', 'pca' or 'pacmap'."
raise ValueError(msg)

if isinstance(embedding, Embedding):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,18 @@ def test_UMAP(self):
umap2 = self.test_matscholar.calculate_umap(n_neighbors=15, random_state=42)
assert (umap1 == umap2).all()

def test_PaCMAP(self):
"""Test the PaCMAP function."""
pacmap_params = {"random_state": 42}
assert isinstance(self.test_matscholar.calculate_pacmap(), np.ndarray)
assert self.test_matscholar.calculate_pacmap().shape == (
len(self.test_matscholar.element_list),
2,
)
pacmap1 = self.test_matscholar.calculate_pacmap(**pacmap_params)
pacmap2 = self.test_matscholar.calculate_pacmap(**pacmap_params)
assert (pacmap1 == pacmap2).all()
Comment on lines +477 to +487

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept exact equality to match the existing test_PCA / test_tSNE / test_UMAP determinism checks, which all assert (x == y).all(). I verified PaCMAP is bit-reproducible within a process under a fixed random_state (same kNN backend, two calls → identical arrays), so the exact check is stable here. Happy to switch all four reducer tests to np.testing.assert_allclose together if you'd prefer a tolerance-based convention.



class SpeciesEmbeddingTest(unittest.TestCase):
"""Test the SpeciesEmbedding class."""
Expand Down
7 changes: 7 additions & 0 deletions tests/test_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ def test_dimension_2d_plotter(self):
adjusttext=True,
)
assert isinstance(skipatom_umap_plot, plt.Axes)
skipatom_pacmap_plot = dimension_plotter(
self.test_skipatom,
n_components=2,
reducer="pacmap",
adjusttext=False,
)
assert isinstance(skipatom_pacmap_plot, plt.Axes)

with pytest.raises(ValueError):
dimension_plotter(
Expand Down
Loading