pgalign 1.1.0__py3-none-any.whl
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.
- pgalign/AnnDataMALDI.py +476 -0
- pgalign/PeakCalling.py +282 -0
- pgalign/__init__.py +87 -0
- pgalign/cli.py +84 -0
- pgalign/data/mouse_pancreas_toy.npz +0 -0
- pgalign/datasets.py +39 -0
- pgalign/pipeline.py +153 -0
- pgalign/readers.py +130 -0
- pgalign/util.py +135 -0
- pgalign-1.1.0.dist-info/METADATA +318 -0
- pgalign-1.1.0.dist-info/RECORD +15 -0
- pgalign-1.1.0.dist-info/WHEEL +5 -0
- pgalign-1.1.0.dist-info/entry_points.txt +2 -0
- pgalign-1.1.0.dist-info/licenses/LICENSE +21 -0
- pgalign-1.1.0.dist-info/top_level.txt +1 -0
pgalign/AnnDataMALDI.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""Group pairing and fine alignment (PGAlign Steps 3 and 4).
|
|
2
|
+
|
|
3
|
+
Implements the "Peak group pairing" and "Fine alignment assessment"
|
|
4
|
+
subsections of the Methods in Deng, Zhang & Zhang (2026).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import random
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import scanpy as sc
|
|
13
|
+
from scipy.stats import pearsonr
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
|
|
16
|
+
from .util import (
|
|
17
|
+
in_range_lookup,
|
|
18
|
+
comp_clusters,
|
|
19
|
+
get_unk_comp_clusters,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnnDataMALDI(object):
|
|
24
|
+
"""Orchestrator for PGAlign's pairing and fine-alignment steps.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
AnnDataUnk : anndata.AnnData
|
|
29
|
+
The unknown spectrum (to be aligned).
|
|
30
|
+
AnnDataRef : anndata.AnnData
|
|
31
|
+
The reference spectrum.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, AnnDataUnk, AnnDataRef):
|
|
35
|
+
self.AnnDataUnk = AnnDataUnk
|
|
36
|
+
self.AnnDataRef = AnnDataRef
|
|
37
|
+
self.mz_valueUnk = AnnDataUnk.var
|
|
38
|
+
self.mz_valueRef = AnnDataRef.var
|
|
39
|
+
self.meanspectrumUnk = np.mean(AnnDataUnk.X, axis=0)
|
|
40
|
+
self.meanspectrumRef = np.mean(AnnDataRef.X, axis=0)
|
|
41
|
+
|
|
42
|
+
def ClusterPrep(self, cluster):
|
|
43
|
+
"""Prepare reference / unknown cluster lists from joint peak groups.
|
|
44
|
+
|
|
45
|
+
:param cluster: the joint peak group object (jointcluster) from PeakCalling.
|
|
46
|
+
"""
|
|
47
|
+
mz_unk = list(self.mz_valueUnk["m/z"])
|
|
48
|
+
select_list_1 = in_range_lookup(mz_unk, cluster)
|
|
49
|
+
mz_ref = list(self.mz_valueRef["m/z"])
|
|
50
|
+
select_list_2 = in_range_lookup(mz_ref, cluster)
|
|
51
|
+
select_list = [(e_i | e_j) for (e_i, e_j) in zip(select_list_1, select_list_2)]
|
|
52
|
+
#
|
|
53
|
+
self.ref_clusters = comp_clusters(select_list, self.mz_valueRef.index)
|
|
54
|
+
self.unk_clusters = get_unk_comp_clusters(self.ref_clusters, self.mz_valueUnk.index)
|
|
55
|
+
if (len(self.ref_clusters) > len(self.unk_clusters)):
|
|
56
|
+
self.ref_clusters = self.ref_clusters[0:len(self.unk_clusters)]
|
|
57
|
+
self.nclusters = len(self.ref_clusters)
|
|
58
|
+
#
|
|
59
|
+
self.midpoints_unk = [clusteri[len(clusteri) // 2] for clusteri in self.unk_clusters]
|
|
60
|
+
|
|
61
|
+
def get_corr_peakgroup_refined(self, cluster):
|
|
62
|
+
"""Compute the similarity matrix across each peak group.
|
|
63
|
+
|
|
64
|
+
Uses Pearson's correlation with a sliding-window refinement of +/- 4 grid
|
|
65
|
+
points, that is four spacings of the m/z grid rather than an m/z interval of
|
|
66
|
+
four, so the physical width of the search scales with the gridding increment
|
|
67
|
+
(Methods, "Peak group pairing").
|
|
68
|
+
|
|
69
|
+
Rows of the returned matrix are indexed by unknown peak groups and columns by
|
|
70
|
+
reference peak groups. The matrix is square but is not symmetric, because the
|
|
71
|
+
unknown window is centred on the midpoint of the unknown group while the
|
|
72
|
+
reference group is read from its own boundaries. Candidate pairs are
|
|
73
|
+
restricted to a band of +/- 2 groups around the diagonal; entries outside the
|
|
74
|
+
band are left at zero and can never be selected.
|
|
75
|
+
|
|
76
|
+
:param cluster: the joint peak group object (jointcluster) from PeakCalling.
|
|
77
|
+
"""
|
|
78
|
+
self.cluster = cluster
|
|
79
|
+
self.ClusterPrep(cluster)
|
|
80
|
+
PearsonMatrix = np.zeros((self.nclusters, self.nclusters))
|
|
81
|
+
PearsonMatrixFull = np.zeros((self.nclusters * 9, self.nclusters * 9))
|
|
82
|
+
for i_unk in tqdm(range(self.nclusters)):
|
|
83
|
+
for i_ref in range(max(i_unk - 2, 0), min(i_unk + 3, self.nclusters)):
|
|
84
|
+
midindexint = list(self.mz_valueUnk.index).index(self.midpoints_unk[i_unk])
|
|
85
|
+
headindexint_ref = list(self.mz_valueRef.index).index(self.ref_clusters[i_ref][0])
|
|
86
|
+
mzdiff = self.mz_valueUnk["m/z"].iloc[midindexint] - self.mz_valueRef["m/z"].iloc[
|
|
87
|
+
list(self.mz_valueRef.index).index(self.ref_clusters[i_ref][len(self.ref_clusters[i_ref]) // 2])
|
|
88
|
+
]
|
|
89
|
+
Pearson_Record = -2
|
|
90
|
+
Can_align = True
|
|
91
|
+
for slide_unk in range(-4, 5):
|
|
92
|
+
for slide_ref in range(-4, 5):
|
|
93
|
+
indexlist_unk = slide_unk + np.arange(
|
|
94
|
+
midindexint - len(self.ref_clusters[i_ref]) // 2,
|
|
95
|
+
midindexint + len(self.ref_clusters[i_ref]) - len(self.ref_clusters[i_ref]) // 2,
|
|
96
|
+
)
|
|
97
|
+
indexlist_ref = slide_ref + np.arange(
|
|
98
|
+
headindexint_ref,
|
|
99
|
+
headindexint_ref + len(self.ref_clusters[i_ref]),
|
|
100
|
+
)
|
|
101
|
+
if indexlist_unk[0] < 0 or indexlist_ref[0] < 0:
|
|
102
|
+
# numpy indexes negatives cyclically, so the IndexError
|
|
103
|
+
# below only catches the high-m/z end of the spectrum
|
|
104
|
+
Can_align = False
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
specsub_unk = self.meanspectrumUnk[indexlist_unk.tolist()]
|
|
108
|
+
specsub_ref = self.meanspectrumRef[indexlist_ref.tolist()]
|
|
109
|
+
PearsonMatrixFull[i_unk * 9 + slide_unk + 4, i_ref * 9 + slide_ref + 4] = pearsonr(specsub_unk, specsub_ref)[0]
|
|
110
|
+
if PearsonMatrixFull[i_unk * 9 + slide_unk + 4, i_ref * 9 + slide_ref + 4] > Pearson_Record:
|
|
111
|
+
Pearson_Record = PearsonMatrixFull[i_unk * 9 + slide_unk + 4, i_ref * 9 + slide_ref + 4]
|
|
112
|
+
except IndexError:
|
|
113
|
+
Can_align = False
|
|
114
|
+
if Can_align:
|
|
115
|
+
PearsonMatrix[i_unk, i_ref] = Pearson_Record / (abs(mzdiff) + 1)
|
|
116
|
+
else:
|
|
117
|
+
PearsonMatrix[i_unk, i_ref] = -1
|
|
118
|
+
self.PearsonMatrix = PearsonMatrix
|
|
119
|
+
self.PearsonMatrixFull = PearsonMatrixFull
|
|
120
|
+
|
|
121
|
+
def group_align_onestep(self, matrix, criteria, origin):
|
|
122
|
+
if all(matrix.shape):
|
|
123
|
+
ind = np.argwhere(matrix == matrix.max())
|
|
124
|
+
maxvalue = matrix[ind[0, 0], ind[0, 1]]
|
|
125
|
+
ind0 = np.array([ind[0, :]])
|
|
126
|
+
if maxvalue > criteria:
|
|
127
|
+
record = list(ind0 + origin)
|
|
128
|
+
results1 = self.group_align_onestep(matrix[0:ind0[0, 0], 0:ind0[0, 1]], criteria, origin)
|
|
129
|
+
results2 = self.group_align_onestep(matrix[(ind0[0, 0] + 1):, (ind0[0, 1] + 1):], criteria, ind0 + origin + 1)
|
|
130
|
+
record = record + results1 + results2
|
|
131
|
+
return record
|
|
132
|
+
else:
|
|
133
|
+
return []
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
def peak_group_pairing(self, criteria=0):
|
|
137
|
+
"""PGAlign Step 3: greedily pair peak groups based on the similarity matrix.
|
|
138
|
+
|
|
139
|
+
Operates on ``self.PearsonMatrix`` (computed by ``get_corr_peakgroup_refined``).
|
|
140
|
+
Pairs survive only when the (distance-penalised) Pearson correlation
|
|
141
|
+
exceeds ``criteria`` (delta in the paper). The result is stored on
|
|
142
|
+
``self.align_group`` as an n-by-2 matrix of matched (unknown, reference)
|
|
143
|
+
peak-group indices.
|
|
144
|
+
|
|
145
|
+
:param criteria: minimum (distance-penalised) Pearson correlation. Defaults to 0.
|
|
146
|
+
"""
|
|
147
|
+
alignlist = self.group_align_onestep(self.PearsonMatrix, criteria, origin=(0, 0))
|
|
148
|
+
align_results = np.empty((len(alignlist), 2))
|
|
149
|
+
for i, ind in enumerate(alignlist):
|
|
150
|
+
align_results[i, 0] = ind[0]
|
|
151
|
+
align_results[i, 1] = ind[1]
|
|
152
|
+
align_results_sort = align_results[align_results[:, 1].argsort()]
|
|
153
|
+
self.align_group = align_results_sort.astype(int)
|
|
154
|
+
self.align_group = np.unique(self.align_group, axis=0)
|
|
155
|
+
self.nclusters = self.align_group.shape[0]
|
|
156
|
+
|
|
157
|
+
def greedy_match(self, criteria=0):
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"greedy_match() is deprecated; use peak_group_pairing() to match the manuscript terminology.",
|
|
160
|
+
DeprecationWarning,
|
|
161
|
+
stacklevel=2,
|
|
162
|
+
)
|
|
163
|
+
return self.peak_group_pairing(criteria=criteria)
|
|
164
|
+
|
|
165
|
+
def fine_alignment_assessment(self, threshold=0.2, ignore=False, **kwargs):
|
|
166
|
+
"""PGAlign Step 4: rigid-body translation of unknown m/z values per paired group.
|
|
167
|
+
|
|
168
|
+
For each paired peak group, find the +/- 4 grid-point offset whose
|
|
169
|
+
diagonal-mean Pearson correlation is maximal; if that maximum exceeds
|
|
170
|
+
``threshold`` (or if ``ignore=True``), apply that integer-index shift. An
|
|
171
|
+
offset of zero is a legitimate outcome: a group that is already aligned
|
|
172
|
+
attains its maximum there and is left in place.
|
|
173
|
+
|
|
174
|
+
:param threshold: minimum diagonal-mean Pearson correlation to accept the shift.
|
|
175
|
+
Defaults to 0.2.
|
|
176
|
+
:param ignore: if True, accept the best shift regardless of threshold.
|
|
177
|
+
"""
|
|
178
|
+
# Backward-compat: accept the historical typo "threshould=" as an alias.
|
|
179
|
+
if "threshould" in kwargs:
|
|
180
|
+
warnings.warn(
|
|
181
|
+
"fine_alignment_assessment(threshould=...) is deprecated; use threshold=... instead.",
|
|
182
|
+
DeprecationWarning,
|
|
183
|
+
stacklevel=2,
|
|
184
|
+
)
|
|
185
|
+
threshold = kwargs.pop("threshould")
|
|
186
|
+
if kwargs:
|
|
187
|
+
raise TypeError(f"Unexpected keyword arguments: {list(kwargs)}")
|
|
188
|
+
|
|
189
|
+
aligned_mz_clusters_ref = []
|
|
190
|
+
aligned_mz_clusters_unk = []
|
|
191
|
+
changerecord = []
|
|
192
|
+
#
|
|
193
|
+
for i in tqdm(range(self.align_group.shape[0])):
|
|
194
|
+
submatrix = self.PearsonMatrixFull[
|
|
195
|
+
(self.align_group[i, 0] * 9):(self.align_group[i, 0] * 9 + 9),
|
|
196
|
+
(self.align_group[i, 1] * 9):(self.align_group[i, 1] * 9 + 9),
|
|
197
|
+
]
|
|
198
|
+
diagnallist = [np.nanmean(np.diag(submatrix, k=i)) for i in range(-4, 5)]
|
|
199
|
+
diagnallist = [-1 if value != value else value for value in diagnallist] # NaN -> -1
|
|
200
|
+
if (max(diagnallist) > threshold) | ignore:
|
|
201
|
+
change = -4 + diagnallist.index(max(diagnallist))
|
|
202
|
+
midindexint = list(self.mz_valueUnk.index).index(self.midpoints_unk[self.align_group[i, 0]])
|
|
203
|
+
headindexint_ref = list(self.mz_valueRef.index).index(self.ref_clusters[self.align_group[i, 1]][0])
|
|
204
|
+
indexlist_ref = np.arange(
|
|
205
|
+
headindexint_ref,
|
|
206
|
+
headindexint_ref + len(self.ref_clusters[self.align_group[i, 1]]),
|
|
207
|
+
)
|
|
208
|
+
indexlist_unk = - change + np.arange(
|
|
209
|
+
midindexint - len(self.ref_clusters[self.align_group[i, 0]]) // 2,
|
|
210
|
+
midindexint + len(self.ref_clusters[self.align_group[i, 0]]) - len(self.ref_clusters[self.align_group[i, 0]]) // 2,
|
|
211
|
+
)
|
|
212
|
+
aligned_mz_clusters_ref.append(indexlist_ref)
|
|
213
|
+
aligned_mz_clusters_unk.append(indexlist_unk)
|
|
214
|
+
changerecord.append(- change)
|
|
215
|
+
self.aligned_mz_clusters_unk = aligned_mz_clusters_unk
|
|
216
|
+
self.aligned_mz_clusters_ref = aligned_mz_clusters_ref
|
|
217
|
+
self.changerecord = changerecord
|
|
218
|
+
|
|
219
|
+
def fine_align(self, threshould=0.2, ignore=False):
|
|
220
|
+
warnings.warn(
|
|
221
|
+
"fine_align() is deprecated; use fine_alignment_assessment() to match the manuscript terminology.",
|
|
222
|
+
DeprecationWarning,
|
|
223
|
+
stacklevel=2,
|
|
224
|
+
)
|
|
225
|
+
return self.fine_alignment_assessment(threshold=threshould, ignore=ignore)
|
|
226
|
+
|
|
227
|
+
def flag_ambiguous_alignments(self, delta=0.1):
|
|
228
|
+
"""Flag shifted peak groups whose original position scores almost as well.
|
|
229
|
+
|
|
230
|
+
A shifted peak group has one alternative correspondence, namely to be left
|
|
231
|
+
at its original position. When unknown group t is paired with a different
|
|
232
|
+
reference group k, the pair is flagged if the similarity of t to the
|
|
233
|
+
reference group at its own position is at least the similarity of the
|
|
234
|
+
selected pair minus ``delta`` (S_tt >= S_kt - tau in the paper; here
|
|
235
|
+
``PearsonMatrix[t, t] >= PearsonMatrix[t, k] - delta``, since rows of
|
|
236
|
+
``PearsonMatrix`` are unknown groups). When a group is paired at its own
|
|
237
|
+
position but moved by a non-zero offset, the pair is flagged if the mean
|
|
238
|
+
correlation on the zero-offset diagonal, D_0, lies within ``delta`` of the
|
|
239
|
+
maximum diagonal mean. Groups paired at their own position with a zero
|
|
240
|
+
offset are not shifted and are not reported. An in-place similarity of -1
|
|
241
|
+
(a window ran outside the spectrum) is not a valid alternative, so such a
|
|
242
|
+
pair is reported with a missing in-place score and is not flagged.
|
|
243
|
+
|
|
244
|
+
Call after ``fine_alignment_assessment``. Only groups accepted there, that
|
|
245
|
+
is groups with an entry in ``changerecord``, are assessed. The result is
|
|
246
|
+
also stored on ``self.ambiguity_table``.
|
|
247
|
+
|
|
248
|
+
:param delta: tolerance on the similarity scale. Defaults to 0.1.
|
|
249
|
+
:return: a DataFrame with one row per shifted matched group and columns
|
|
250
|
+
``aligned_group`` (position in ``changerecord``), ``unknown_group``,
|
|
251
|
+
``reference_group``, ``mz_start`` and ``mz_end`` (reference m/z range),
|
|
252
|
+
``offset`` (the ``changerecord`` entry), ``rule`` ("group" for a
|
|
253
|
+
different reference group, "offset" for a non-zero offset),
|
|
254
|
+
``score_selected``, ``score_in_place``, ``margin``
|
|
255
|
+
(``score_selected - score_in_place``) and ``flagged``.
|
|
256
|
+
"""
|
|
257
|
+
index_ref = list(self.mz_valueRef.index)
|
|
258
|
+
mz_ref = np.asarray(self.mz_valueRef["m/z"])
|
|
259
|
+
rows = []
|
|
260
|
+
j = 0
|
|
261
|
+
for i in range(self.align_group.shape[0]):
|
|
262
|
+
i_unk, i_ref = self.align_group[i, 0], self.align_group[i, 1]
|
|
263
|
+
headindexint_ref = index_ref.index(self.ref_clusters[i_ref][0])
|
|
264
|
+
# fine_alignment_assessment appends only accepted groups, in align_group order
|
|
265
|
+
if j >= len(self.changerecord) or self.aligned_mz_clusters_ref[j][0] != headindexint_ref:
|
|
266
|
+
continue
|
|
267
|
+
offset = self.changerecord[j]
|
|
268
|
+
if i_unk != i_ref:
|
|
269
|
+
rule = "group"
|
|
270
|
+
score_selected = self.PearsonMatrix[i_unk, i_ref]
|
|
271
|
+
score_in_place = self.PearsonMatrix[i_unk, i_unk]
|
|
272
|
+
if score_in_place == -1:
|
|
273
|
+
score_in_place = np.nan
|
|
274
|
+
elif offset != 0:
|
|
275
|
+
rule = "offset"
|
|
276
|
+
submatrix = self.PearsonMatrixFull[(i_unk * 9):(i_unk * 9 + 9), (i_ref * 9):(i_ref * 9 + 9)]
|
|
277
|
+
diagnallist = [np.nanmean(np.diag(submatrix, k=d)) for d in range(-4, 5)]
|
|
278
|
+
diagnallist = [-1 if value != value else value for value in diagnallist] # NaN -> -1
|
|
279
|
+
score_selected = max(diagnallist)
|
|
280
|
+
score_in_place = diagnallist[4]
|
|
281
|
+
else:
|
|
282
|
+
j += 1
|
|
283
|
+
continue
|
|
284
|
+
margin = score_selected - score_in_place
|
|
285
|
+
rows.append({
|
|
286
|
+
"aligned_group": j,
|
|
287
|
+
"unknown_group": int(i_unk),
|
|
288
|
+
"reference_group": int(i_ref),
|
|
289
|
+
"mz_start": mz_ref[self.aligned_mz_clusters_ref[j][0]],
|
|
290
|
+
"mz_end": mz_ref[self.aligned_mz_clusters_ref[j][-1]],
|
|
291
|
+
"offset": int(offset),
|
|
292
|
+
"rule": rule,
|
|
293
|
+
"score_selected": score_selected,
|
|
294
|
+
"score_in_place": score_in_place,
|
|
295
|
+
"margin": margin,
|
|
296
|
+
"flagged": bool(margin <= delta),
|
|
297
|
+
})
|
|
298
|
+
j += 1
|
|
299
|
+
if j != len(self.changerecord):
|
|
300
|
+
raise ValueError("changerecord does not match align_group; rerun fine_alignment_assessment.")
|
|
301
|
+
self.ambiguity_table = pd.DataFrame(rows, columns=[
|
|
302
|
+
"aligned_group", "unknown_group", "reference_group", "mz_start", "mz_end", "offset",
|
|
303
|
+
"rule", "score_selected", "score_in_place", "margin", "flagged",
|
|
304
|
+
])
|
|
305
|
+
return self.ambiguity_table
|
|
306
|
+
|
|
307
|
+
def summarize(self):
|
|
308
|
+
"""Concatenate paired unknown/reference index arrays into flat alignment vectors."""
|
|
309
|
+
self.unknownalign = np.concatenate(self.aligned_mz_clusters_unk)
|
|
310
|
+
self.referenalign = np.concatenate(self.aligned_mz_clusters_ref)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class MALDI_SIM(object):
|
|
314
|
+
def __init__(self, AnnDataMALDI):
|
|
315
|
+
self.origindata = AnnDataMALDI.AnnDataRef.copy()
|
|
316
|
+
self.aligned_mz_clusters_ref = AnnDataMALDI.aligned_mz_clusters_ref.copy()
|
|
317
|
+
self.changerecord = AnnDataMALDI.changerecord.copy()
|
|
318
|
+
self.shiftplot_data()
|
|
319
|
+
|
|
320
|
+
def shiftplot_data(self):
|
|
321
|
+
self.mz_valueRef = self.origindata.var.copy()
|
|
322
|
+
shiftdata = np.zeros((len(self.aligned_mz_clusters_ref), 3))
|
|
323
|
+
for i in tqdm(range(len(self.aligned_mz_clusters_ref))):
|
|
324
|
+
shiftdata[i, 0] = self.mz_valueRef["m/z"].iloc[self.aligned_mz_clusters_ref[i][0]]
|
|
325
|
+
shiftdata[i, 1] = self.mz_valueRef["m/z"].iloc[self.aligned_mz_clusters_ref[i][-1]]
|
|
326
|
+
shiftdata[i, 2] = self.changerecord[i]
|
|
327
|
+
shiftdatadf = pd.DataFrame(shiftdata)
|
|
328
|
+
shiftdatadf = shiftdatadf.rename(columns={0: 'mzvalue_start', 1: 'mzvalue_end', 2: 'shift_fromorigin'})
|
|
329
|
+
shiftdatadf["shift_fromprev"] = np.concatenate((
|
|
330
|
+
[shiftdatadf["shift_fromorigin"][0]],
|
|
331
|
+
np.diff(shiftdatadf["shift_fromorigin"]),
|
|
332
|
+
))
|
|
333
|
+
self.shiftdatadf = shiftdatadf
|
|
334
|
+
|
|
335
|
+
def addin(self, add_at_mz, addnumber):
|
|
336
|
+
nearest_idx = np.where(abs(self.mz_valueRef - add_at_mz) == abs(self.mz_valueRef - add_at_mz).min())[0].max()
|
|
337
|
+
for i in range(addnumber):
|
|
338
|
+
self.arraydata = np.insert(self.arraydata, nearest_idx + 1, self.arraydata[:, nearest_idx], axis=1)
|
|
339
|
+
self.mz_valueRef = np.insert(self.mz_valueRef, nearest_idx + 1, self.mz_valueRef[nearest_idx] + (i + 1) * 0.0001)
|
|
340
|
+
|
|
341
|
+
def delout(self, del_at_mz, delnumber):
|
|
342
|
+
for i in range(delnumber):
|
|
343
|
+
nearest_idx = np.where(abs(self.mz_valueRef - del_at_mz) == abs(self.mz_valueRef - del_at_mz).min())[0].max()
|
|
344
|
+
self.arraydata = np.delete(self.arraydata, nearest_idx, axis=1)
|
|
345
|
+
self.mz_valueRef = np.delete(self.mz_valueRef, nearest_idx)
|
|
346
|
+
|
|
347
|
+
def get_at_mz(self, idx):
|
|
348
|
+
if idx > 0:
|
|
349
|
+
return (self.shiftdatadf.iloc[idx - 1, 1] + self.shiftdatadf.iloc[idx, 0]) / 2
|
|
350
|
+
else:
|
|
351
|
+
return self.shiftdatadf.iloc[idx, 0] / 2
|
|
352
|
+
|
|
353
|
+
def region_shuffle(self, ary, nregion):
|
|
354
|
+
arylength = len(ary)
|
|
355
|
+
intervals = list((np.array(range(1, nregion)) * arylength / nregion).astype("int"))
|
|
356
|
+
if nregion > 1:
|
|
357
|
+
listary = np.split(ary, intervals, axis=0)
|
|
358
|
+
else:
|
|
359
|
+
listary = [ary]
|
|
360
|
+
[random.shuffle(listaryi) for listaryi in listary]
|
|
361
|
+
newary = np.concatenate(listary)
|
|
362
|
+
return newary
|
|
363
|
+
|
|
364
|
+
def SIMULATEdata(self, shuffle, sigma, nregion):
|
|
365
|
+
self.mz_valueRef = np.array(self.origindata.var["m/z"]).copy()
|
|
366
|
+
self.arraydata = self.origindata.X.copy()
|
|
367
|
+
unitdiff = self.mz_valueRef[1] - self.mz_valueRef[0]
|
|
368
|
+
shift_resample = np.array(self.shiftdatadf["shift_fromprev"]).copy()
|
|
369
|
+
if shuffle is True:
|
|
370
|
+
shift_resample = self.region_shuffle(shift_resample, nregion=nregion)
|
|
371
|
+
## Shifting the unit
|
|
372
|
+
for i in tqdm(range(self.shiftdatadf.shape[0])):
|
|
373
|
+
if shift_resample[i] > 0:
|
|
374
|
+
self.addin(add_at_mz=self.get_at_mz(i), addnumber=int(shift_resample[i]))
|
|
375
|
+
if shift_resample[i] < 0:
|
|
376
|
+
self.delout(del_at_mz=self.get_at_mz(i), delnumber=int(abs(shift_resample[i])))
|
|
377
|
+
## Adding Noises
|
|
378
|
+
noise = np.random.lognormal(mean=0.0, sigma=sigma, size=self.arraydata.shape[0] * self.arraydata.shape[1])
|
|
379
|
+
noise.shape = self.arraydata.shape
|
|
380
|
+
self.newarray = self.arraydata + noise
|
|
381
|
+
self.truemz = self.mz_valueRef
|
|
382
|
+
self.newmz = np.array(range(0, len(self.truemz))) * unitdiff + self.truemz[0]
|
|
383
|
+
self.currentshift = shift_resample
|
|
384
|
+
self.shiftdatadf["currentshift_fromprev"] = shift_resample
|
|
385
|
+
self.shiftdatadf["currentshift_fromorg"] = np.cumsum(shift_resample)
|
|
386
|
+
|
|
387
|
+
def getAnnSim(self, shuffle=False, sigma=0.1, nregion=1):
|
|
388
|
+
self.SIMULATEdata(shuffle, sigma, nregion)
|
|
389
|
+
mz_value = pd.DataFrame(list(self.newmz), index=list(self.newmz)).astype('float')
|
|
390
|
+
mz_value = mz_value.rename(columns={0: "m/z"})
|
|
391
|
+
MALDISimData = sc.AnnData(X=self.newarray, var=mz_value, obs=self.origindata.obs)
|
|
392
|
+
return MALDISimData
|
|
393
|
+
|
|
394
|
+
def MSE(self, unkidx, refidx):
|
|
395
|
+
refmz = np.array(self.origindata.var["m/z"]).copy()
|
|
396
|
+
MSE = (np.square(self.truemz[unkidx] - refmz[refidx]))
|
|
397
|
+
return [MSE, self.truemz[unkidx], refmz[refidx]]
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class PGmzalign(MALDI_SIM):
|
|
401
|
+
"""Produce the aligned dataset: the unknown spectra on the reference m/z grid.
|
|
402
|
+
|
|
403
|
+
Build it from an ``AnnDataMALDI`` object after ``fine_alignment_assessment``,
|
|
404
|
+
then call ``getAnnSim()``. Despite the method's name, nothing is simulated;
|
|
405
|
+
the class reuses the column-shifting machinery of ``MALDI_SIM``.
|
|
406
|
+
|
|
407
|
+
:param AnnDataMALDI: an ``AnnDataMALDI`` object that has run Steps 3 and 4.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
def __init__(self, AnnDataMALDI):
|
|
411
|
+
self.unknowndata = AnnDataMALDI.AnnDataUnk.copy()
|
|
412
|
+
self.aligned_mz_clusters_unk = AnnDataMALDI.aligned_mz_clusters_unk.copy()
|
|
413
|
+
super(PGmzalign, self).__init__(AnnDataMALDI)
|
|
414
|
+
|
|
415
|
+
def shiftplot_data(self):
|
|
416
|
+
self.mz_valueRef = self.origindata.var.copy()
|
|
417
|
+
shiftdata = np.zeros((len(self.aligned_mz_clusters_ref), 4))
|
|
418
|
+
for i in tqdm(range(len(self.aligned_mz_clusters_ref))):
|
|
419
|
+
shiftdata[i, 0] = self.mz_valueRef["m/z"].iloc[self.aligned_mz_clusters_ref[i][0]]
|
|
420
|
+
shiftdata[i, 1] = self.mz_valueRef["m/z"].iloc[self.aligned_mz_clusters_ref[i][-1]]
|
|
421
|
+
shiftdata[i, 2] = self.changerecord[i]
|
|
422
|
+
shiftdata[i, 3] = self.aligned_mz_clusters_ref[i][0] - self.aligned_mz_clusters_unk[i][0]
|
|
423
|
+
shiftdatadf = pd.DataFrame(shiftdata)
|
|
424
|
+
shiftdatadf = shiftdatadf.rename(columns={
|
|
425
|
+
0: 'mzvalue_start',
|
|
426
|
+
1: 'mzvalue_end',
|
|
427
|
+
2: 'shift_fromorigin',
|
|
428
|
+
3: 'shift_unit_fromorigin',
|
|
429
|
+
})
|
|
430
|
+
shiftdatadf["shift_fromprev"] = np.concatenate((
|
|
431
|
+
[shiftdatadf["shift_fromorigin"][0]],
|
|
432
|
+
np.diff(shiftdatadf["shift_fromorigin"]),
|
|
433
|
+
))
|
|
434
|
+
shiftdatadf["shift_unit_fromprev"] = np.concatenate((
|
|
435
|
+
[shiftdatadf["shift_unit_fromorigin"][0]],
|
|
436
|
+
np.diff(shiftdatadf["shift_unit_fromorigin"]),
|
|
437
|
+
))
|
|
438
|
+
self.shiftdatadf = shiftdatadf
|
|
439
|
+
|
|
440
|
+
def SIMULATEdata(self):
|
|
441
|
+
self.mz_valueRef = np.array(self.unknowndata.var["m/z"]).copy()
|
|
442
|
+
self.arraydata = self.unknowndata.X.copy()
|
|
443
|
+
shift_resample = np.array(self.shiftdatadf["shift_unit_fromprev"]).copy()
|
|
444
|
+
## Shifting the unit
|
|
445
|
+
for i in tqdm(range(self.shiftdatadf.shape[0])):
|
|
446
|
+
if shift_resample[i] > 0:
|
|
447
|
+
self.addin(add_at_mz=self.get_at_mz(i), addnumber=int(abs(shift_resample[i])))
|
|
448
|
+
if shift_resample[i] < 0:
|
|
449
|
+
self.delout(del_at_mz=self.get_at_mz(i), delnumber=int(abs(shift_resample[i])))
|
|
450
|
+
self.newmz = np.array(self.origindata.var["m/z"]).copy()
|
|
451
|
+
self.currentshift = shift_resample
|
|
452
|
+
self.shiftdatadf["currentshift_fromprev"] = shift_resample
|
|
453
|
+
self.shiftdatadf["currentshift_fromorg"] = np.cumsum(shift_resample)
|
|
454
|
+
|
|
455
|
+
def getAnnSim(self):
|
|
456
|
+
"""Return the aligned unknown spectra as an AnnData on the reference m/z grid.
|
|
457
|
+
|
|
458
|
+
Columns of the unknown spectra are deleted, or copies of a neighbouring
|
|
459
|
+
column inserted, in the gaps between peak groups, so that every matched
|
|
460
|
+
group lands on the indices of its reference group. ``var["m/z"]`` holds the
|
|
461
|
+
reference m/z values and ``obs`` the unknown spectra. The result is cut to
|
|
462
|
+
the shorter of the two m/z axes, so it can lack a few bins at the top of the
|
|
463
|
+
range (two on the bundled toy data).
|
|
464
|
+
|
|
465
|
+
:return: the aligned spectra as an ``anndata.AnnData``.
|
|
466
|
+
"""
|
|
467
|
+
self.SIMULATEdata()
|
|
468
|
+
mz_value = pd.DataFrame(list(self.newmz), index=list(self.newmz)).astype('float')
|
|
469
|
+
mz_value = mz_value.rename(columns={0: "m/z"})
|
|
470
|
+
dim = min(self.arraydata.shape[1], mz_value.shape[0])
|
|
471
|
+
MALDISimData = sc.AnnData(
|
|
472
|
+
X=self.arraydata[:, range(dim)],
|
|
473
|
+
var=mz_value.iloc[range(dim), :],
|
|
474
|
+
obs=self.unknowndata.obs,
|
|
475
|
+
)
|
|
476
|
+
return MALDISimData
|