pgalign 1.1.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {pgalign-1.1.0 → pgalign-2.0.0}/CITATION.cff +1 -1
  2. {pgalign-1.1.0/pgalign.egg-info → pgalign-2.0.0}/PKG-INFO +2 -2
  3. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/PeakCalling.py +12 -2
  4. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/__init__.py +3 -23
  5. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/util.py +5 -39
  6. {pgalign-1.1.0 → pgalign-2.0.0/pgalign.egg-info}/PKG-INFO +2 -2
  7. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign.egg-info/SOURCES.txt +1 -0
  8. {pgalign-1.1.0 → pgalign-2.0.0}/pyproject.toml +1 -1
  9. pgalign-2.0.0/tests/test_docs.py +52 -0
  10. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_import.py +15 -1
  11. pgalign-2.0.0/tests/test_peak_calling.py +43 -0
  12. pgalign-1.1.0/tests/test_peak_calling.py +0 -15
  13. {pgalign-1.1.0 → pgalign-2.0.0}/LICENSE +0 -0
  14. {pgalign-1.1.0 → pgalign-2.0.0}/MANIFEST.in +0 -0
  15. {pgalign-1.1.0 → pgalign-2.0.0}/README.md +0 -0
  16. {pgalign-1.1.0 → pgalign-2.0.0}/images/pgalign_workflow.png +0 -0
  17. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/AnnDataMALDI.py +0 -0
  18. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/cli.py +0 -0
  19. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/data/mouse_pancreas_toy.npz +0 -0
  20. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/datasets.py +0 -0
  21. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/pipeline.py +0 -0
  22. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign/readers.py +0 -0
  23. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign.egg-info/dependency_links.txt +0 -0
  24. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign.egg-info/entry_points.txt +0 -0
  25. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign.egg-info/requires.txt +0 -0
  26. {pgalign-1.1.0 → pgalign-2.0.0}/pgalign.egg-info/top_level.txt +0 -0
  27. {pgalign-1.1.0 → pgalign-2.0.0}/setup.cfg +0 -0
  28. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_align.py +0 -0
  29. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_alignment.py +0 -0
  30. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_ambiguity.py +0 -0
  31. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_checks.py +0 -0
  32. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_cli.py +0 -0
  33. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_deprecation.py +0 -0
  34. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_readers.py +0 -0
  35. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_toy_dataset.py +0 -0
  36. {pgalign-1.1.0 → pgalign-2.0.0}/tests/test_toy_outputs.py +0 -0
@@ -2,7 +2,7 @@ cff-version: 1.2.0
2
2
  message: "If you use PGAlign in your research, please cite the accompanying paper."
3
3
  title: "PGAlign: peak group alignment of mass spectrometry data"
4
4
  type: software
5
- version: "1.1.0"
5
+ version: "2.0.0"
6
6
  authors:
7
7
  - family-names: Deng
8
8
  given-names: Anji
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgalign
3
- Version: 1.1.0
4
- Summary: PGAlign: peak group alignment of mass spectrometry data for imaging and spatial metabolomics.
3
+ Version: 2.0.0
4
+ Summary: Peak group alignment of mass spectrometry imaging data onto a reference m/z grid.
5
5
  Author: Anji Deng, Yuyang Zhang
6
6
  Author-email: Qihuang Zhang <qihuang.zhang@mcgill.ca>
7
7
  Maintainer-email: Qihuang Zhang <qihuang.zhang@mcgill.ca>
@@ -136,8 +136,13 @@ class PeakCalling(object):
136
136
  self.jointcluster = [clusteri for clusteri in self.jointcluster if clusteri[-1] < mzboundary]
137
137
 
138
138
  def importpeak(self, peakUnk, peakRef):
139
+ """Use peaks called elsewhere in place of Step 1.
140
+
141
+ :param peakUnk: indices of the peak points in the unknown spectrum.
142
+ :param peakRef: indices of the peak points in the reference spectrum.
143
+ """
139
144
  self.peakUnk = np.zeros(self.mz_valueUnk.shape[0], dtype=bool)
140
- self.peakRef = self.peakUnk
145
+ self.peakRef = np.zeros(self.mz_valueRef.shape[0], dtype=bool)
141
146
  for i in range(peakUnk.shape[0]):
142
147
  self.peakUnk[peakUnk[i]] = True
143
148
  for j in range(peakRef.shape[0]):
@@ -187,7 +192,12 @@ class PeakCalling_single(PeakCalling):
187
192
  self.jointcluster = [clusteri for clusteri in self.jointcluster if clusteri[-1] < mzboundary]
188
193
 
189
194
  def importpeak(self, peakUnk, peakRef):
190
- self.peakRef = self.peakUnk
195
+ """Use peaks called elsewhere in place of Step 1; ``peakUnk`` is ignored.
196
+
197
+ :param peakUnk: unused, kept for the signature of ``PeakCalling.importpeak``.
198
+ :param peakRef: indices of the peak points in the reference spectrum.
199
+ """
200
+ self.peakRef = np.zeros(self.mz_valueRef.shape[0], dtype=bool)
191
201
  for j in range(peakRef.shape[0]):
192
202
  self.peakRef[peakRef[j]] = True
193
203
  self.meanspectrumRefpeak = self.meanspectrumRef[self.peakRef]
@@ -11,22 +11,12 @@ The four pipeline steps in the paper map to these public objects:
11
11
  align() runs all four with the manuscript's defaults and returns the aligned dataset.
12
12
  """
13
13
 
14
- __version__ = "1.1.0"
14
+ __version__ = "2.0.0"
15
15
 
16
16
  from .util import (
17
- prefilter_cells,
18
- prefilter_genes,
19
- prefilter_specialgenes,
17
+ gridding,
20
18
  save_clusterresults,
21
- rotate,
22
- group_range,
23
- in_range_lookup,
24
- comp_clusters,
25
- find_nearest,
26
- get_unk_comp_clusters,
27
19
  spectrum_save,
28
- GKernal,
29
- gridding,
30
20
  )
31
21
  from .AnnDataMALDI import (
32
22
  AnnDataMALDI,
@@ -71,17 +61,7 @@ __all__ = [
71
61
  # Toy data
72
62
  "datasets",
73
63
  # Utilities
74
- "prefilter_cells",
75
- "prefilter_genes",
76
- "prefilter_specialgenes",
64
+ "gridding",
77
65
  "save_clusterresults",
78
- "rotate",
79
- "group_range",
80
- "in_range_lookup",
81
- "comp_clusters",
82
- "find_nearest",
83
- "get_unk_comp_clusters",
84
66
  "spectrum_save",
85
- "GKernal",
86
- "gridding",
87
67
  ]
@@ -1,44 +1,10 @@
1
- """Utility helpers used by PGAlign's peak-calling and alignment routines."""
1
+ """Utilities. ``gridding``, ``save_clusterresults`` and ``spectrum_save`` are public;
2
+ the other helpers serve the step classes and are not exported from ``pgalign``."""
2
3
 
3
- import logging
4
4
  import math
5
5
 
6
6
  import numpy as np
7
7
  import pandas as pd
8
- import scanpy as sc
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
-
13
- def prefilter_cells(adata, min_counts=None, max_counts=None, min_genes=200, max_genes=None):
14
- if min_genes is None and min_counts is None and max_genes is None and max_counts is None:
15
- raise ValueError('Provide one of min_counts, min_genes, max_counts or max_genes.')
16
- id_tmp = np.asarray([True] * adata.shape[0], dtype=bool)
17
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_cells(adata.X, min_genes=min_genes)[0]) if min_genes is not None else id_tmp
18
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_cells(adata.X, max_genes=max_genes)[0]) if max_genes is not None else id_tmp
19
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_cells(adata.X, min_counts=min_counts)[0]) if min_counts is not None else id_tmp
20
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_cells(adata.X, max_counts=max_counts)[0]) if max_counts is not None else id_tmp
21
- adata._inplace_subset_obs(id_tmp)
22
- adata.raw = sc.pp.log1p(adata, copy=True)
23
- logger.debug("adata.raw.var_names.is_unique = %s", adata.raw.var_names.is_unique)
24
-
25
-
26
- def prefilter_genes(adata, min_counts=None, max_counts=None, min_cells=10, max_cells=None):
27
- if min_cells is None and min_counts is None and max_cells is None and max_counts is None:
28
- raise ValueError('Provide one of min_counts, min_genes, max_counts or max_genes.')
29
- id_tmp = np.asarray([True] * adata.shape[1], dtype=bool)
30
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_genes(adata.X, min_cells=min_cells)[0]) if min_cells is not None else id_tmp
31
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_genes(adata.X, max_cells=max_cells)[0]) if max_cells is not None else id_tmp
32
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_genes(adata.X, min_counts=min_counts)[0]) if min_counts is not None else id_tmp
33
- id_tmp = np.logical_and(id_tmp, sc.pp.filter_genes(adata.X, max_counts=max_counts)[0]) if max_counts is not None else id_tmp
34
- adata._inplace_subset_var(id_tmp)
35
-
36
-
37
- def prefilter_specialgenes(adata, Gene1Pattern="ERCC", Gene2Pattern="MT-"):
38
- id_tmp1 = np.asarray([not str(name).startswith(Gene1Pattern) for name in adata.var_names], dtype=bool)
39
- id_tmp2 = np.asarray([not str(name).startswith(Gene2Pattern) for name in adata.var_names], dtype=bool)
40
- id_tmp = np.logical_and(id_tmp1, id_tmp2)
41
- adata._inplace_subset_var(id_tmp)
42
8
 
43
9
 
44
10
  def save_clusterresults(cluster, filepath):
@@ -110,8 +76,8 @@ def spectrum_save(mz, intensity, name):
110
76
  originaltable.to_csv("{name}.csv".format(name=name))
111
77
 
112
78
 
113
- def GKernal(x0, x, y, sig=1.):
114
- """Gaussian kernel smoother evaluated at ``x0``."""
79
+ def nadaraya_watson(x0, x, y, sig=1.):
80
+ """Nadaraya-Watson estimate of ``y`` at ``x0``, with a Gaussian kernel of bandwidth ``sig``."""
115
81
  ax = x - x0
116
82
  Kernal = np.exp(-0.5 * np.square(ax) / np.square(sig))
117
83
  Weighted = Kernal * y
@@ -127,7 +93,7 @@ def gridding(mz, intensity, start=None, end=None, increment=0.125, epsilon=0.025
127
93
  target_grid = np.arange(start, end, increment)
128
94
  intensity_out = np.zeros(target_grid.shape[0])
129
95
  for i in range(target_grid.shape[0]):
130
- calculation = GKernal(target_grid[i], mz, intensity, sig=epsilon)
96
+ calculation = nadaraya_watson(target_grid[i], mz, intensity, sig=epsilon)
131
97
  if calculation == calculation:
132
98
  intensity_out[i] = calculation
133
99
  else:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgalign
3
- Version: 1.1.0
4
- Summary: PGAlign: peak group alignment of mass spectrometry data for imaging and spatial metabolomics.
3
+ Version: 2.0.0
4
+ Summary: Peak group alignment of mass spectrometry imaging data onto a reference m/z grid.
5
5
  Author: Anji Deng, Yuyang Zhang
6
6
  Author-email: Qihuang Zhang <qihuang.zhang@mcgill.ca>
7
7
  Maintainer-email: Qihuang Zhang <qihuang.zhang@mcgill.ca>
@@ -25,6 +25,7 @@ tests/test_ambiguity.py
25
25
  tests/test_checks.py
26
26
  tests/test_cli.py
27
27
  tests/test_deprecation.py
28
+ tests/test_docs.py
28
29
  tests/test_import.py
29
30
  tests/test_peak_calling.py
30
31
  tests/test_readers.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pgalign"
7
7
  dynamic = ["version"]
8
- description = "PGAlign: peak group alignment of mass spectrometry data for imaging and spatial metabolomics."
8
+ description = "Peak group alignment of mass spectrometry imaging data onto a reference m/z grid."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
@@ -0,0 +1,52 @@
1
+ """Run the Python examples of README.md and docs/ as written, on the bundled toy data."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ PAGES = ["README.md"] + sorted(str(p.relative_to(ROOT)) for p in (ROOT / "docs").glob("*.md"))
11
+ # Examples that read the reader's own files cannot run here.
12
+ NEEDS_FILES = ('read_imzml("', 'read_scils_csv("', 'read_h5ad("')
13
+ # The README step-by-step example leaves the inputs to the reader; supply the toy data.
14
+ PLACEHOLDER = ("MALDIdataAnn1 = ... # unknown spectrum\n", "MALDIdataAnn2 = ... # reference spectrum\n")
15
+ TOY = "MALDIdataAnn1, MALDIdataAnn2 = pg.datasets.load_mouse_pancreas_toy()\n"
16
+
17
+
18
+ def _blocks(page):
19
+ text = (ROOT / page).read_text(encoding="utf-8")
20
+ return [b for b in re.findall(r"```python\n(.*?)```", text, flags=re.S) if not any(s in b for s in NEEDS_FILES)]
21
+
22
+
23
+ def _run(page):
24
+ namespace = {}
25
+ for block in _blocks(page):
26
+ exec(block.replace(PLACEHOLDER[0], TOY).replace(PLACEHOLDER[1], ""), namespace)
27
+ if "res" in namespace and "res.summary()" in block:
28
+ assert namespace["res"].summary() in block, f"{page}: the summary in a comment no longer matches"
29
+ return namespace
30
+
31
+
32
+ @pytest.mark.parametrize("page", [p for p in PAGES if (ROOT / p).exists() and _blocks(p)])
33
+ def test_examples_run_as_written(page):
34
+ _run(page)
35
+
36
+
37
+ def test_readme_examples_align_onto_the_reference_grid():
38
+ if not (ROOT / "README.md").exists():
39
+ pytest.skip("README.md is not in this checkout")
40
+ ns = _run("README.md")
41
+ aligned, ref = ns["MALDIdataAnn1_aligned"], ns["MALDIdataAnn2"]
42
+ np.testing.assert_array_equal(aligned.var["m/z"].to_numpy(), ref.var["m/z"].to_numpy()[:aligned.n_vars])
43
+ assert len(ns["table"]) > 0
44
+
45
+
46
+ def test_tutorial_step_by_step_equals_align():
47
+ if not (ROOT / "docs" / "tutorial.md").exists():
48
+ pytest.skip("docs/ is not in this checkout")
49
+ ns = _run("docs/tutorial.md")
50
+ # the toy shift is a rigid +2, so the matrix alone cannot tell the two peak callers apart
51
+ np.testing.assert_array_equal(ns["align"].align_group, ns["res"].steps.align_group)
52
+ np.testing.assert_array_equal(np.asarray(ns["aligned"].X), np.asarray(ns["res"].aligned.X))
@@ -19,11 +19,25 @@ def test_public_api_present():
19
19
  "MALDI_SIM",
20
20
  "PGmzalign",
21
21
  "gridding",
22
- "GKernal",
22
+ "save_clusterresults",
23
+ "spectrum_save",
24
+ "read_imzml",
25
+ "read_scils_csv",
26
+ "read_h5ad",
27
+ "write_h5ad",
23
28
  ]:
24
29
  assert hasattr(pg, name), f"pgalign is missing public name: {name}"
25
30
 
26
31
 
32
+ def test_removed_names_are_gone():
33
+ """Single-cell leftovers were deleted in 2.0; internal helpers stay in pgalign.util only."""
34
+ for name in ["prefilter_cells", "prefilter_genes", "prefilter_specialgenes", "GKernal",
35
+ "rotate", "group_range", "in_range_lookup", "comp_clusters", "find_nearest",
36
+ "get_unk_comp_clusters"]:
37
+ assert not hasattr(pg, name), f"pgalign still exports {name}"
38
+ assert callable(pg.util.in_range_lookup) and callable(pg.util.nadaraya_watson)
39
+
40
+
27
41
  def test_paper_aligned_methods_exist():
28
42
  """Method names from the current PGAlign manuscript are exposed on the right classes."""
29
43
  assert hasattr(pg.PeakCalling, "peak_calling")
@@ -0,0 +1,43 @@
1
+ """Smoke test for PGAlign Steps 1 and 2 on synthetic data."""
2
+
3
+ import numpy as np
4
+
5
+ import pgalign as pg
6
+
7
+
8
+ def test_peak_calling_and_grouping_produce_clusters(tiny_anndata):
9
+ unk, ref = tiny_anndata
10
+ pc = pg.PeakCalling(unk, ref)
11
+ pc.peak_calling(threshold=0.9)
12
+ pc.peak_grouping(percentile=0.9)
13
+ # At least one joint peak group must be detected on the four-peak fixture.
14
+ assert len(pc.jointcluster) > 0
15
+ # Each joint peak group is a list of m/z values.
16
+ for group in pc.jointcluster:
17
+ assert len(group) >= 1
18
+
19
+
20
+ def test_importpeak_keeps_the_two_spectra_apart(tiny_anndata):
21
+ unk, ref = tiny_anndata
22
+ pc = pg.PeakCalling(unk, ref)
23
+ pc.importpeak(np.array([10, 20]), np.array([30, 40]))
24
+ assert np.flatnonzero(pc.peakUnk).tolist() == [10, 20]
25
+ assert np.flatnonzero(pc.peakRef).tolist() == [30, 40]
26
+
27
+
28
+ def test_imported_peaks_give_the_same_groups_as_step_1(tiny_anndata):
29
+ unk, ref = tiny_anndata
30
+ called = pg.PeakCalling(unk, ref)
31
+ called.peak_calling(threshold=0.9)
32
+ called.peak_grouping(percentile=0.9)
33
+ imported = pg.PeakCalling(unk, ref)
34
+ imported.importpeak(np.flatnonzero(called.peakUnk), np.flatnonzero(called.peakRef))
35
+ imported.peak_grouping(percentile=0.9)
36
+ assert imported.jointcluster == called.jointcluster
37
+
38
+
39
+ def test_single_spectrum_importpeak(tiny_anndata):
40
+ _, ref = tiny_anndata
41
+ single = pg.PeakCalling_single(ref)
42
+ single.importpeak(None, np.array([30, 40]))
43
+ assert np.flatnonzero(single.peakRef).tolist() == [30, 40]
@@ -1,15 +0,0 @@
1
- """Smoke test for PGAlign Steps 1 and 2 on synthetic data."""
2
-
3
- import pgalign as pg
4
-
5
-
6
- def test_peak_calling_and_grouping_produce_clusters(tiny_anndata):
7
- unk, ref = tiny_anndata
8
- pc = pg.PeakCalling(unk, ref)
9
- pc.peak_calling(threshold=0.9)
10
- pc.peak_grouping(percentile=0.9)
11
- # At least one joint peak group must be detected on the four-peak fixture.
12
- assert len(pc.jointcluster) > 0
13
- # Each joint peak group is a list of m/z values.
14
- for group in pc.jointcluster:
15
- assert len(group) >= 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes