masster 0.2.5__py3-none-any.whl → 0.3.1__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.
Potentially problematic release.
This version of masster might be problematic. Click here for more details.
- masster/__init__.py +27 -27
- masster/_version.py +17 -17
- masster/chromatogram.py +497 -503
- masster/data/examples/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.featureXML +199787 -0
- masster/data/examples/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.sample5 +0 -0
- masster/logger.py +318 -244
- masster/sample/__init__.py +9 -9
- masster/sample/defaults/__init__.py +15 -15
- masster/sample/defaults/find_adducts_def.py +325 -325
- masster/sample/defaults/find_features_def.py +366 -366
- masster/sample/defaults/find_ms2_def.py +285 -285
- masster/sample/defaults/get_spectrum_def.py +314 -318
- masster/sample/defaults/sample_def.py +374 -378
- masster/sample/h5.py +1321 -1297
- masster/sample/helpers.py +833 -364
- masster/sample/lib.py +762 -0
- masster/sample/load.py +1220 -1187
- masster/sample/parameters.py +131 -131
- masster/sample/plot.py +1685 -1622
- masster/sample/processing.py +1402 -1416
- masster/sample/quant.py +209 -0
- masster/sample/sample.py +393 -387
- masster/sample/sample5_schema.json +181 -181
- masster/sample/save.py +737 -736
- masster/sample/sciex.py +1213 -0
- masster/spectrum.py +1287 -1319
- masster/study/__init__.py +9 -9
- masster/study/defaults/__init__.py +21 -19
- masster/study/defaults/align_def.py +267 -267
- masster/study/defaults/export_def.py +41 -40
- masster/study/defaults/fill_chrom_def.py +264 -264
- masster/study/defaults/fill_def.py +260 -0
- masster/study/defaults/find_consensus_def.py +256 -256
- masster/study/defaults/find_ms2_def.py +163 -163
- masster/study/defaults/integrate_chrom_def.py +225 -225
- masster/study/defaults/integrate_def.py +221 -0
- masster/study/defaults/merge_def.py +256 -0
- masster/study/defaults/study_def.py +272 -269
- masster/study/export.py +674 -287
- masster/study/h5.py +1406 -886
- masster/study/helpers.py +1713 -433
- masster/study/helpers_optimized.py +317 -0
- masster/study/load.py +1231 -1078
- masster/study/parameters.py +99 -99
- masster/study/plot.py +632 -645
- masster/study/processing.py +1057 -1046
- masster/study/save.py +161 -134
- masster/study/study.py +612 -522
- masster/study/study5_schema.json +253 -241
- {masster-0.2.5.dist-info → masster-0.3.1.dist-info}/METADATA +15 -10
- masster-0.3.1.dist-info/RECORD +59 -0
- {masster-0.2.5.dist-info → masster-0.3.1.dist-info}/licenses/LICENSE +661 -661
- masster-0.2.5.dist-info/RECORD +0 -50
- {masster-0.2.5.dist-info → masster-0.3.1.dist-info}/WHEEL +0 -0
- {masster-0.2.5.dist-info → masster-0.3.1.dist-info}/entry_points.txt +0 -0
masster/study/plot.py
CHANGED
|
@@ -1,645 +1,632 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import holoviews as hv
|
|
7
|
-
import numpy as np
|
|
8
|
-
import panel
|
|
9
|
-
import polars as pl
|
|
10
|
-
|
|
11
|
-
from bokeh.io.export import export_png
|
|
12
|
-
from bokeh.models import ColumnDataSource
|
|
13
|
-
from bokeh.models import HoverTool
|
|
14
|
-
from bokeh.palettes import Turbo256
|
|
15
|
-
from bokeh.plotting import figure
|
|
16
|
-
from bokeh.plotting import output_file
|
|
17
|
-
from bokeh.plotting import show
|
|
18
|
-
from tqdm import tqdm
|
|
19
|
-
|
|
20
|
-
hv.extension("bokeh")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def plot_alignment(self, filename=None):
|
|
24
|
-
import matplotlib.pyplot as plt
|
|
25
|
-
import numpy as np
|
|
26
|
-
|
|
27
|
-
if self.features_maps is None or len(self.features_maps) == 0:
|
|
28
|
-
self.load_features()
|
|
29
|
-
|
|
30
|
-
feature_maps = self.features_maps
|
|
31
|
-
ref_index = self.alignment_ref_index
|
|
32
|
-
if ref_index is None:
|
|
33
|
-
self.logger.error("No alignment performed yet.")
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
fmaps = [
|
|
37
|
-
feature_maps[ref_index],
|
|
38
|
-
*feature_maps[:ref_index],
|
|
39
|
-
*feature_maps[ref_index + 1 :],
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
fig = plt.figure(figsize=(12, 6))
|
|
43
|
-
|
|
44
|
-
ax = fig.add_subplot(1, 2, 1)
|
|
45
|
-
ax.set_title("Feature maps before alignment")
|
|
46
|
-
ax.set_ylabel("m/z")
|
|
47
|
-
ax.set_xlabel("RT")
|
|
48
|
-
|
|
49
|
-
# use alpha value to display feature intensity
|
|
50
|
-
ax.scatter(
|
|
51
|
-
[f.getRT() for f in fmaps[0]],
|
|
52
|
-
[f.getMZ() for f in fmaps[0]],
|
|
53
|
-
alpha=np.asarray([f.getIntensity() for f in fmaps[0]])
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[f.
|
|
61
|
-
[f.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ax
|
|
68
|
-
ax.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[f.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
from bokeh.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
("
|
|
267
|
-
("
|
|
268
|
-
("
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
color_bar
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
self.logger.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
for uid in sample_uids:
|
|
403
|
-
sample_data = features_pd[features_pd["sample_uid"] == uid]
|
|
404
|
-
if sample_data.empty:
|
|
405
|
-
continue
|
|
406
|
-
|
|
407
|
-
sample_name =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
renderer = p.
|
|
438
|
-
x="rt",
|
|
439
|
-
y="mz",
|
|
440
|
-
|
|
441
|
-
color=color_values[uid],
|
|
442
|
-
alpha="alpha",
|
|
443
|
-
legend_label=sample_name,
|
|
444
|
-
source=source,
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
uids=
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
# Save as PNG using Panel's export_png if filename ends with .png
|
|
634
|
-
if filename.endswith(".png"):
|
|
635
|
-
from panel.io.save import save_png
|
|
636
|
-
|
|
637
|
-
# Convert Holoviews overlays to Bokeh models before saving
|
|
638
|
-
bokeh_layout = panel.panel(layout).get_root() # type: ignore[attr-defined]
|
|
639
|
-
save_png(bokeh_layout, filename=filename)
|
|
640
|
-
else:
|
|
641
|
-
panel.panel(layout).save(filename, embed=True) # type: ignore[attr-defined]
|
|
642
|
-
else:
|
|
643
|
-
# In a server context, return the panel object instead of showing or saving directly
|
|
644
|
-
# return panel.panel(layout)
|
|
645
|
-
panel.panel(layout).show()
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import holoviews as hv
|
|
7
|
+
import numpy as np
|
|
8
|
+
import panel
|
|
9
|
+
import polars as pl
|
|
10
|
+
|
|
11
|
+
from bokeh.io.export import export_png
|
|
12
|
+
from bokeh.models import ColumnDataSource
|
|
13
|
+
from bokeh.models import HoverTool
|
|
14
|
+
from bokeh.palettes import Turbo256
|
|
15
|
+
from bokeh.plotting import figure
|
|
16
|
+
from bokeh.plotting import output_file
|
|
17
|
+
from bokeh.plotting import show
|
|
18
|
+
from tqdm import tqdm
|
|
19
|
+
|
|
20
|
+
hv.extension("bokeh")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def plot_alignment(self, filename=None):
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
if self.features_maps is None or len(self.features_maps) == 0:
|
|
28
|
+
self.load_features()
|
|
29
|
+
|
|
30
|
+
feature_maps = self.features_maps
|
|
31
|
+
ref_index = self.alignment_ref_index
|
|
32
|
+
if ref_index is None:
|
|
33
|
+
self.logger.error("No alignment performed yet.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
fmaps = [
|
|
37
|
+
feature_maps[ref_index],
|
|
38
|
+
*feature_maps[:ref_index],
|
|
39
|
+
*feature_maps[ref_index + 1 :],
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
fig = plt.figure(figsize=(12, 6))
|
|
43
|
+
|
|
44
|
+
ax = fig.add_subplot(1, 2, 1)
|
|
45
|
+
ax.set_title("Feature maps before alignment")
|
|
46
|
+
ax.set_ylabel("m/z")
|
|
47
|
+
ax.set_xlabel("RT")
|
|
48
|
+
|
|
49
|
+
# use alpha value to display feature intensity
|
|
50
|
+
ax.scatter(
|
|
51
|
+
[f.getRT() for f in fmaps[0]],
|
|
52
|
+
[f.getMZ() for f in fmaps[0]],
|
|
53
|
+
alpha=np.asarray([f.getIntensity() for f in fmaps[0]]) / max([f.getIntensity() for f in fmaps[0]]),
|
|
54
|
+
s=4,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for fm in fmaps[1:]:
|
|
58
|
+
ax.scatter(
|
|
59
|
+
[f.getMetaValue("original_RT") for f in fm],
|
|
60
|
+
[f.getMZ() for f in fm],
|
|
61
|
+
alpha=np.asarray([f.getIntensity() for f in fm]) / max([f.getIntensity() for f in fm]),
|
|
62
|
+
s=2, # Set symbol size to 3
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
ax = fig.add_subplot(1, 2, 2)
|
|
66
|
+
ax.set_title("Feature maps after alignment")
|
|
67
|
+
ax.set_ylabel("m/z")
|
|
68
|
+
ax.set_xlabel("RT")
|
|
69
|
+
|
|
70
|
+
for fm in fmaps:
|
|
71
|
+
ax.scatter(
|
|
72
|
+
[f.getRT() for f in fm],
|
|
73
|
+
[f.getMZ() for f in fm],
|
|
74
|
+
alpha=np.asarray([f.getIntensity() for f in fm]) / max([f.getIntensity() for f in fm]),
|
|
75
|
+
s=2, # Set symbol size to 3
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
fig.tight_layout()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def plot_alignment_bokeh(self, filename=None):
|
|
82
|
+
from bokeh.plotting import figure, show, output_file
|
|
83
|
+
from bokeh.layouts import gridplot
|
|
84
|
+
|
|
85
|
+
feature_maps = self.features_maps
|
|
86
|
+
ref_index = self.alignment_ref_index
|
|
87
|
+
if ref_index is None:
|
|
88
|
+
self.logger.warning("No alignment performed yet.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
fmaps = [
|
|
92
|
+
feature_maps[ref_index],
|
|
93
|
+
*feature_maps[:ref_index],
|
|
94
|
+
*feature_maps[ref_index + 1 :],
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Create Bokeh figures
|
|
98
|
+
p1 = figure(
|
|
99
|
+
title="Feature maps before alignment",
|
|
100
|
+
width=600,
|
|
101
|
+
height=400,
|
|
102
|
+
)
|
|
103
|
+
p1.xaxis.axis_label = "RT"
|
|
104
|
+
p1.yaxis.axis_label = "m/z"
|
|
105
|
+
p2 = figure(
|
|
106
|
+
title="Feature maps after alignment",
|
|
107
|
+
width=600,
|
|
108
|
+
height=400,
|
|
109
|
+
)
|
|
110
|
+
p2.xaxis.axis_label = "RT"
|
|
111
|
+
p2.yaxis.axis_label = "m/z"
|
|
112
|
+
|
|
113
|
+
# Plot before alignment
|
|
114
|
+
p1.scatter(
|
|
115
|
+
x=[f.getRT() for f in fmaps[0]],
|
|
116
|
+
y=[f.getMZ() for f in fmaps[0]],
|
|
117
|
+
size=4,
|
|
118
|
+
alpha=[f.getIntensity() / max([f.getIntensity() for f in fmaps[0]]) for f in fmaps[0]],
|
|
119
|
+
color="blue",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for fm in fmaps[1:]:
|
|
123
|
+
p1.scatter(
|
|
124
|
+
x=[f.getMetaValue("original_RT") for f in fm],
|
|
125
|
+
y=[f.getMZ() for f in fm],
|
|
126
|
+
size=2,
|
|
127
|
+
alpha=[f.getIntensity() / max([f.getIntensity() for f in fm]) for f in fm],
|
|
128
|
+
color="green",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Plot after alignment
|
|
132
|
+
for fm in fmaps:
|
|
133
|
+
p2.scatter(
|
|
134
|
+
x=[f.getRT() for f in fm],
|
|
135
|
+
y=[f.getMZ() for f in fm],
|
|
136
|
+
size=2,
|
|
137
|
+
alpha=[f.getIntensity() / max([f.getIntensity() for f in fm]) for f in fm],
|
|
138
|
+
color="red",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Arrange plots in a grid
|
|
142
|
+
# Link the x_range and y_range of both plots for synchronized zooming/panning
|
|
143
|
+
p2.x_range = p1.x_range
|
|
144
|
+
p2.y_range = p1.y_range
|
|
145
|
+
|
|
146
|
+
grid = gridplot([[p1, p2]])
|
|
147
|
+
|
|
148
|
+
# Output to file and show
|
|
149
|
+
if filename:
|
|
150
|
+
output_file(filename)
|
|
151
|
+
show(grid)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def plot_consensus_2d(
|
|
155
|
+
self,
|
|
156
|
+
filename=None,
|
|
157
|
+
colorby="number_samples",
|
|
158
|
+
sizeby="inty_mean",
|
|
159
|
+
markersize=6,
|
|
160
|
+
alpha=0.7,
|
|
161
|
+
cmap=None,
|
|
162
|
+
):
|
|
163
|
+
if self.consensus_df is None:
|
|
164
|
+
self.logger.error("No consensus map found.")
|
|
165
|
+
return
|
|
166
|
+
data = self.consensus_df.clone()
|
|
167
|
+
if colorby not in data.columns:
|
|
168
|
+
self.logger.error(f"Column {colorby} not found in consensus_df.")
|
|
169
|
+
return
|
|
170
|
+
if sizeby not in data.columns:
|
|
171
|
+
self.logger.warning(f"Column {sizeby} not found in consensus_df.")
|
|
172
|
+
sizeby = None
|
|
173
|
+
# if sizeby is not None, set markersize to sizeby
|
|
174
|
+
if sizeby is not None:
|
|
175
|
+
# set markersize to sizeby
|
|
176
|
+
if sizeby in ["inty_mean"]:
|
|
177
|
+
# use log10 of sizeby
|
|
178
|
+
# Filter out empty or all-NA entries before applying np.log10
|
|
179
|
+
data = data.with_columns([
|
|
180
|
+
pl.when(
|
|
181
|
+
(pl.col(sizeby).is_not_null()) & (pl.col(sizeby).is_finite()) & (pl.col(sizeby) > 0),
|
|
182
|
+
)
|
|
183
|
+
.then((pl.col(sizeby).log10() * markersize / 12).pow(2))
|
|
184
|
+
.otherwise(markersize)
|
|
185
|
+
.alias("markersize"),
|
|
186
|
+
])
|
|
187
|
+
else:
|
|
188
|
+
max_size = data[sizeby].max()
|
|
189
|
+
data = data.with_columns([
|
|
190
|
+
(pl.col(sizeby) / max_size * markersize).alias("markersize"),
|
|
191
|
+
])
|
|
192
|
+
else:
|
|
193
|
+
data = data.with_columns([pl.lit(markersize).alias("markersize")])
|
|
194
|
+
# sort by ascending colorby
|
|
195
|
+
data = data.sort(colorby)
|
|
196
|
+
# convert consensus_id to string - check if column exists
|
|
197
|
+
if "consensus_id" in data.columns:
|
|
198
|
+
# Handle Object dtype by converting to string first
|
|
199
|
+
data = data.with_columns([
|
|
200
|
+
pl.col("consensus_id")
|
|
201
|
+
.map_elements(
|
|
202
|
+
lambda x: str(x) if x is not None else None,
|
|
203
|
+
return_dtype=pl.Utf8,
|
|
204
|
+
)
|
|
205
|
+
.alias("consensus_id"),
|
|
206
|
+
])
|
|
207
|
+
elif "consensus_uid" in data.columns:
|
|
208
|
+
data = data.with_columns([
|
|
209
|
+
pl.col("consensus_uid").cast(pl.Utf8).alias("consensus_id"),
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
if cmap is None:
|
|
213
|
+
cmap = "vi"
|
|
214
|
+
elif cmap == "grey":
|
|
215
|
+
cmap = "Greys256"
|
|
216
|
+
|
|
217
|
+
# plot with bokeh
|
|
218
|
+
import bokeh.plotting as bp
|
|
219
|
+
|
|
220
|
+
from bokeh.models import BasicTicker
|
|
221
|
+
from bokeh.models import ColumnDataSource
|
|
222
|
+
from bokeh.models import HoverTool
|
|
223
|
+
from bokeh.models import LinearColorMapper
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from bokeh.models import ColorBar # type: ignore[attr-defined]
|
|
227
|
+
except ImportError:
|
|
228
|
+
from bokeh.models.annotations import ColorBar
|
|
229
|
+
from bokeh.palettes import viridis
|
|
230
|
+
|
|
231
|
+
# Convert Polars DataFrame to pandas for Bokeh compatibility
|
|
232
|
+
data_pd = data.to_pandas()
|
|
233
|
+
source = ColumnDataSource(data_pd)
|
|
234
|
+
color_mapper = LinearColorMapper(
|
|
235
|
+
palette=viridis(256),
|
|
236
|
+
low=data[colorby].min(),
|
|
237
|
+
high=data[colorby].max(),
|
|
238
|
+
)
|
|
239
|
+
# scatter plot rt vs mz
|
|
240
|
+
p = bp.figure(
|
|
241
|
+
width=800,
|
|
242
|
+
height=600,
|
|
243
|
+
title="Consensus map",
|
|
244
|
+
)
|
|
245
|
+
p.xaxis.axis_label = "Retention Time (min)"
|
|
246
|
+
p.yaxis.axis_label = "m/z"
|
|
247
|
+
scatter_renderer = p.scatter(
|
|
248
|
+
x="rt",
|
|
249
|
+
y="mz",
|
|
250
|
+
size="markersize",
|
|
251
|
+
fill_color={"field": colorby, "transform": color_mapper},
|
|
252
|
+
line_color=None,
|
|
253
|
+
alpha=alpha,
|
|
254
|
+
source=source,
|
|
255
|
+
)
|
|
256
|
+
# add hover tool
|
|
257
|
+
hover = HoverTool(
|
|
258
|
+
tooltips=[
|
|
259
|
+
("consensus_uid", "@consensus_uid"),
|
|
260
|
+
("consensus_id", "@consensus_id"),
|
|
261
|
+
("number_samples", "@number_samples"),
|
|
262
|
+
("number_ms2", "@number_ms2"),
|
|
263
|
+
("rt", "@rt"),
|
|
264
|
+
("mz", "@mz"),
|
|
265
|
+
("inty_mean", "@inty_mean"),
|
|
266
|
+
("iso_mean", "@iso_mean"),
|
|
267
|
+
("coherence_mean", "@chrom_coherence_mean"),
|
|
268
|
+
("prominence_mean", "@chrom_prominence_mean"),
|
|
269
|
+
],
|
|
270
|
+
renderers=[scatter_renderer],
|
|
271
|
+
)
|
|
272
|
+
p.add_tools(hover)
|
|
273
|
+
|
|
274
|
+
# add colorbar
|
|
275
|
+
color_bar = ColorBar(
|
|
276
|
+
color_mapper=color_mapper,
|
|
277
|
+
label_standoff=12,
|
|
278
|
+
location=(0, 0),
|
|
279
|
+
title=colorby,
|
|
280
|
+
ticker=BasicTicker(desired_num_ticks=8),
|
|
281
|
+
)
|
|
282
|
+
p.add_layout(color_bar, "right")
|
|
283
|
+
|
|
284
|
+
if filename is not None:
|
|
285
|
+
bp.output_file(filename)
|
|
286
|
+
bp.show(p)
|
|
287
|
+
return p
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def plot_samples_2d(
|
|
291
|
+
self,
|
|
292
|
+
samples=None,
|
|
293
|
+
filename=None,
|
|
294
|
+
markersize=2,
|
|
295
|
+
size="const",
|
|
296
|
+
alpha_max=0.8,
|
|
297
|
+
alpha="inty",
|
|
298
|
+
cmap="Turbo256",
|
|
299
|
+
max_features=50000, # Reduced default for better performance with many samples
|
|
300
|
+
):
|
|
301
|
+
"""
|
|
302
|
+
Plot all feature maps for sample_uid in parameter uids in an overlaid scatter plot.
|
|
303
|
+
Each sample is a different color. Alpha scales with intensity.
|
|
304
|
+
OPTIMIZED VERSION: Uses vectorized operations and batch processing.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
sample_uids = self._get_sample_uids(samples)
|
|
308
|
+
|
|
309
|
+
if not sample_uids:
|
|
310
|
+
self.logger.error("No valid sample_uids provided.")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
colors = Turbo256
|
|
314
|
+
color_map = {uid: colors[i * (256 // max(1, len(sample_uids)))] for i, uid in enumerate(sample_uids)}
|
|
315
|
+
|
|
316
|
+
p = figure(
|
|
317
|
+
width=600,
|
|
318
|
+
height=600,
|
|
319
|
+
title="Sample Features",
|
|
320
|
+
)
|
|
321
|
+
p.xaxis.axis_label = "Retention Time (RT)"
|
|
322
|
+
p.yaxis.axis_label = "m/z"
|
|
323
|
+
|
|
324
|
+
# OPTIMIZATION 1: Batch filter all features for selected samples at once
|
|
325
|
+
features_batch = self.features_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
326
|
+
|
|
327
|
+
if features_batch.is_empty():
|
|
328
|
+
self.logger.error("No features found for the selected samples.")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# OPTIMIZATION 8: Fast sampling for very large datasets to maintain interactivity
|
|
332
|
+
max_features_per_plot = max_features # Limit for interactive performance
|
|
333
|
+
total_features = len(features_batch)
|
|
334
|
+
|
|
335
|
+
if total_features > max_features_per_plot:
|
|
336
|
+
# OPTIMIZED: Much faster random sampling without groupby operations
|
|
337
|
+
sample_ratio = max_features_per_plot / total_features
|
|
338
|
+
self.logger.info(
|
|
339
|
+
f"Large dataset detected ({total_features:,} features). "
|
|
340
|
+
f"Sampling {sample_ratio:.1%} for visualization performance.",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# FAST: Use simple random sampling instead of expensive stratified sampling
|
|
344
|
+
n_samples = min(max_features_per_plot, total_features)
|
|
345
|
+
features_batch = features_batch.sample(n=n_samples, seed=42)
|
|
346
|
+
|
|
347
|
+
# OPTIMIZATION 2: Join with samples_df to get sample names in one operation
|
|
348
|
+
samples_info = self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
349
|
+
features_with_names = features_batch.join(
|
|
350
|
+
samples_info.select(["sample_uid", "sample_name"]),
|
|
351
|
+
on="sample_uid",
|
|
352
|
+
how="left",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# OPTIMIZATION 4: Fast pre-calculation of alpha values for all features
|
|
356
|
+
if alpha == "inty":
|
|
357
|
+
# OPTIMIZED: Use efficient Polars operations instead of pandas groupby transform
|
|
358
|
+
# Calculate max intensity per sample in Polars (much faster)
|
|
359
|
+
max_inty_per_sample = features_with_names.group_by("sample_uid").agg(
|
|
360
|
+
pl.col("inty").max().alias("max_inty"),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Join back and calculate alpha efficiently
|
|
364
|
+
features_batch = (
|
|
365
|
+
features_with_names.join(
|
|
366
|
+
max_inty_per_sample,
|
|
367
|
+
on="sample_uid",
|
|
368
|
+
how="left",
|
|
369
|
+
)
|
|
370
|
+
.with_columns(
|
|
371
|
+
(pl.col("inty") / pl.col("max_inty") * alpha_max).alias("alpha"),
|
|
372
|
+
)
|
|
373
|
+
.drop("max_inty")
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Convert to pandas once after all Polars operations
|
|
377
|
+
features_pd = features_batch.to_pandas()
|
|
378
|
+
else:
|
|
379
|
+
# Convert to pandas and add constant alpha
|
|
380
|
+
features_pd = features_with_names.to_pandas()
|
|
381
|
+
features_pd["alpha"] = alpha_max
|
|
382
|
+
|
|
383
|
+
# OPTIMIZATION 9: NEW - Batch create all ColumnDataSources at once
|
|
384
|
+
# Group all data by sample_uid and create sources efficiently
|
|
385
|
+
sources = {}
|
|
386
|
+
renderers: list[Any] = []
|
|
387
|
+
|
|
388
|
+
# Pre-compute color mapping to avoid repeated lookups
|
|
389
|
+
color_values = {}
|
|
390
|
+
sample_names = {}
|
|
391
|
+
|
|
392
|
+
for uid in sample_uids:
|
|
393
|
+
sample_data = features_pd[features_pd["sample_uid"] == uid]
|
|
394
|
+
if sample_data.empty:
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
sample_name = sample_data["sample_name"].iloc[0]
|
|
398
|
+
sample_names[uid] = sample_name
|
|
399
|
+
color_values[uid] = color_map[uid]
|
|
400
|
+
|
|
401
|
+
# OPTIMIZATION 10: Batch renderer creation with pre-computed values
|
|
402
|
+
for uid in sample_uids:
|
|
403
|
+
sample_data = features_pd[features_pd["sample_uid"] == uid]
|
|
404
|
+
if sample_data.empty:
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
sample_name = sample_names[uid]
|
|
408
|
+
color_values[uid]
|
|
409
|
+
|
|
410
|
+
# OPTIMIZATION 11: Direct numpy array access for better performance
|
|
411
|
+
source = ColumnDataSource(
|
|
412
|
+
data={
|
|
413
|
+
"rt": sample_data["rt"].values,
|
|
414
|
+
"mz": sample_data["mz"].values,
|
|
415
|
+
"inty": sample_data["inty"].values,
|
|
416
|
+
"alpha": sample_data["alpha"].values,
|
|
417
|
+
"sample": np.full(len(sample_data), sample_name, dtype=object),
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
sources[uid] = source
|
|
422
|
+
|
|
423
|
+
# OPTIMIZATION 12: Use pre-computed color value
|
|
424
|
+
# Create renderer with pre-computed values
|
|
425
|
+
renderer: Any
|
|
426
|
+
if size.lower() in ["dyn", "dynamic"]:
|
|
427
|
+
renderer = p.circle(
|
|
428
|
+
x="rt",
|
|
429
|
+
y="mz",
|
|
430
|
+
radius=markersize / 10,
|
|
431
|
+
color=color_values[uid],
|
|
432
|
+
alpha="alpha",
|
|
433
|
+
legend_label=sample_name,
|
|
434
|
+
source=source,
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
renderer = p.scatter(
|
|
438
|
+
x="rt",
|
|
439
|
+
y="mz",
|
|
440
|
+
size=markersize,
|
|
441
|
+
color=color_values[uid],
|
|
442
|
+
alpha="alpha",
|
|
443
|
+
legend_label=sample_name,
|
|
444
|
+
source=source,
|
|
445
|
+
)
|
|
446
|
+
renderers.append(renderer)
|
|
447
|
+
|
|
448
|
+
# OPTIMIZATION 13: Simplified hover tool for better performance with many samples
|
|
449
|
+
if renderers:
|
|
450
|
+
hover = HoverTool(
|
|
451
|
+
tooltips=[
|
|
452
|
+
("sample", "@sample"),
|
|
453
|
+
("rt", "@rt{0.00}"),
|
|
454
|
+
("mz", "@mz{0.0000}"),
|
|
455
|
+
("intensity", "@inty{0.0e+0}"),
|
|
456
|
+
],
|
|
457
|
+
renderers=renderers,
|
|
458
|
+
)
|
|
459
|
+
p.add_tools(hover)
|
|
460
|
+
|
|
461
|
+
# Remove legend from plot
|
|
462
|
+
p.legend.visible = False
|
|
463
|
+
if filename:
|
|
464
|
+
if filename.endswith(".html"):
|
|
465
|
+
output_file(filename)
|
|
466
|
+
show(p)
|
|
467
|
+
elif filename.endswith(".png"):
|
|
468
|
+
export_png(p, filename=filename)
|
|
469
|
+
else:
|
|
470
|
+
output_file(filename)
|
|
471
|
+
show(p)
|
|
472
|
+
else:
|
|
473
|
+
show(p)
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def plot_chrom(
|
|
478
|
+
self,
|
|
479
|
+
uids=None,
|
|
480
|
+
samples=None,
|
|
481
|
+
filename=None,
|
|
482
|
+
aligned=True,
|
|
483
|
+
width=800,
|
|
484
|
+
height=300,
|
|
485
|
+
):
|
|
486
|
+
cons_uids = self._get_consensus_uids(uids)
|
|
487
|
+
sample_uids = self._get_sample_uids(samples)
|
|
488
|
+
|
|
489
|
+
chroms = self.get_chrom(uids=cons_uids, samples=sample_uids)
|
|
490
|
+
|
|
491
|
+
if chroms is None or chroms.is_empty():
|
|
492
|
+
self.logger.error("No chromatogram data found.")
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# Assign a fixed color to each sample/column
|
|
496
|
+
sample_names = [col for col in chroms.columns if col not in ["consensus_uid"]]
|
|
497
|
+
if not sample_names:
|
|
498
|
+
self.logger.error("No sample names found in chromatogram data.")
|
|
499
|
+
return
|
|
500
|
+
color_map = {sample: Turbo256[i * (256 // max(1, len(sample_names)))] for i, sample in enumerate(sample_names)}
|
|
501
|
+
|
|
502
|
+
plots = []
|
|
503
|
+
self.logger.info(f"Plotting {chroms.shape[0]} chromatograms...")
|
|
504
|
+
tdqm_disable = self.log_level not in ["TRACE", "DEBUG", "INFO"]
|
|
505
|
+
for row in tqdm(
|
|
506
|
+
chroms.iter_rows(named=True),
|
|
507
|
+
total=chroms.shape[0],
|
|
508
|
+
desc=f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Plot chromatograms",
|
|
509
|
+
disable=tdqm_disable,
|
|
510
|
+
):
|
|
511
|
+
consensus_uid = row["consensus_uid"] # Get consensus_uid from the row
|
|
512
|
+
consensus_id = consensus_uid # Use the same value for consensus_id
|
|
513
|
+
curves = []
|
|
514
|
+
rt_min = np.inf
|
|
515
|
+
rt_max = 0
|
|
516
|
+
for sample in sample_names:
|
|
517
|
+
chrom = row[sample]
|
|
518
|
+
if chrom is not None:
|
|
519
|
+
# check if chrom is nan
|
|
520
|
+
if isinstance(chrom, float) and np.isnan(chrom):
|
|
521
|
+
continue
|
|
522
|
+
|
|
523
|
+
chrom = chrom.to_dict()
|
|
524
|
+
rt = chrom["rt"].copy()
|
|
525
|
+
if len(rt) == 0:
|
|
526
|
+
continue
|
|
527
|
+
if aligned and "rt_shift" in chrom:
|
|
528
|
+
rt_shift = chrom["rt_shift"]
|
|
529
|
+
if rt_shift is not None:
|
|
530
|
+
# Convert to numpy array if it's a list, then add scalar
|
|
531
|
+
if isinstance(rt, list):
|
|
532
|
+
rt = np.array(rt)
|
|
533
|
+
rt = rt + rt_shift # Add scalar to array
|
|
534
|
+
|
|
535
|
+
# update rt_min and rt_max
|
|
536
|
+
if rt[0] < rt_min:
|
|
537
|
+
rt_min = rt[0]
|
|
538
|
+
if rt[-1] > rt_max:
|
|
539
|
+
rt_max = rt[-1]
|
|
540
|
+
|
|
541
|
+
inty = chrom["inty"]
|
|
542
|
+
|
|
543
|
+
# Convert both rt and inty to numpy arrays if they're lists
|
|
544
|
+
if isinstance(rt, list):
|
|
545
|
+
rt = np.array(rt)
|
|
546
|
+
if isinstance(inty, list):
|
|
547
|
+
inty = np.array(inty)
|
|
548
|
+
|
|
549
|
+
# Ensure both rt and inty are arrays and have the same length and are not empty
|
|
550
|
+
if rt.size > 0 and inty.size > 0 and rt.shape == inty.shape:
|
|
551
|
+
# sort rt and inty by rt
|
|
552
|
+
sorted_indices = np.argsort(rt)
|
|
553
|
+
rt = rt[sorted_indices]
|
|
554
|
+
inty = inty[sorted_indices]
|
|
555
|
+
curve = hv.Curve((rt, inty), kdims=["RT"], vdims=["inty"]).opts(
|
|
556
|
+
color=color_map[sample],
|
|
557
|
+
line_width=1,
|
|
558
|
+
)
|
|
559
|
+
curves.append(curve)
|
|
560
|
+
|
|
561
|
+
if "feature_start" in chrom and "feature_end" in chrom:
|
|
562
|
+
# Add vertical lines for feature start and end
|
|
563
|
+
feature_start = chrom["feature_start"]
|
|
564
|
+
feature_end = chrom["feature_end"]
|
|
565
|
+
if aligned and "rt_shift" in chrom:
|
|
566
|
+
rt_shift = chrom["rt_shift"]
|
|
567
|
+
if rt_shift is not None:
|
|
568
|
+
feature_start += rt_shift
|
|
569
|
+
feature_end += rt_shift
|
|
570
|
+
if feature_start < rt_min:
|
|
571
|
+
rt_min = feature_start
|
|
572
|
+
if feature_end > rt_max:
|
|
573
|
+
rt_max = feature_end
|
|
574
|
+
# Add vertical lines to the curves
|
|
575
|
+
curves.append(
|
|
576
|
+
hv.VLine(feature_start).opts(
|
|
577
|
+
color=color_map[sample],
|
|
578
|
+
line_dash="dotted",
|
|
579
|
+
line_width=1,
|
|
580
|
+
),
|
|
581
|
+
)
|
|
582
|
+
curves.append(
|
|
583
|
+
hv.VLine(feature_end).opts(
|
|
584
|
+
color=color_map[sample],
|
|
585
|
+
line_dash="dotted",
|
|
586
|
+
line_width=1,
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
if curves:
|
|
590
|
+
# find row in consensus_df with consensus_id
|
|
591
|
+
consensus_row = self.consensus_df.filter(
|
|
592
|
+
pl.col("consensus_uid") == consensus_id,
|
|
593
|
+
)
|
|
594
|
+
rt_start_mean = consensus_row["rt_start_mean"][0]
|
|
595
|
+
rt_end_mean = consensus_row["rt_end_mean"][0]
|
|
596
|
+
# Add vertical lines to overlay
|
|
597
|
+
curves.append(hv.VLine(rt_start_mean).opts(color="black", line_width=2))
|
|
598
|
+
curves.append(hv.VLine(rt_end_mean).opts(color="black", line_width=2))
|
|
599
|
+
|
|
600
|
+
overlay = hv.Overlay(curves).opts(
|
|
601
|
+
height=height,
|
|
602
|
+
width=width,
|
|
603
|
+
title=f"Consensus UID: {consensus_id}, mz: {consensus_row['mz'][0]:.4f}, rt: {consensus_row['rt'][0]:.2f}{' (aligned)' if aligned else ''}",
|
|
604
|
+
xlim=(rt_min, rt_max),
|
|
605
|
+
shared_axes=False,
|
|
606
|
+
)
|
|
607
|
+
plots.append(overlay)
|
|
608
|
+
|
|
609
|
+
if not plots:
|
|
610
|
+
self.logger.warning("No valid chromatogram curves to plot.")
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
# stack vertically.
|
|
614
|
+
# Stack all plots vertically in a Panel column
|
|
615
|
+
layout = panel.Column(*[panel.panel(plot) for plot in plots])
|
|
616
|
+
if filename is not None:
|
|
617
|
+
if filename.endswith(".html"):
|
|
618
|
+
panel.panel(layout).save(filename, embed=True) # type: ignore[attr-defined]
|
|
619
|
+
else:
|
|
620
|
+
# Save as PNG using Panel's export_png if filename ends with .png
|
|
621
|
+
if filename.endswith(".png"):
|
|
622
|
+
from panel.io.save import save_png
|
|
623
|
+
|
|
624
|
+
# Convert Holoviews overlays to Bokeh models before saving
|
|
625
|
+
bokeh_layout = panel.panel(layout).get_root() # type: ignore[attr-defined]
|
|
626
|
+
save_png(bokeh_layout, filename=filename)
|
|
627
|
+
else:
|
|
628
|
+
panel.panel(layout).save(filename, embed=True) # type: ignore[attr-defined]
|
|
629
|
+
else:
|
|
630
|
+
# In a server context, return the panel object instead of showing or saving directly
|
|
631
|
+
# return panel.panel(layout)
|
|
632
|
+
panel.panel(layout).show()
|