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.
@@ -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