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/h5.py
CHANGED
|
@@ -1,886 +1,1406 @@
|
|
|
1
|
-
"""
|
|
2
|
-
_study_h5.py
|
|
3
|
-
|
|
4
|
-
This module provides HDF5-based save/load functionality for the Study class.
|
|
5
|
-
It handles serialization and deserialization of Polars DataFrames with complex objects
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
if
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
if
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
if
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1
|
+
"""
|
|
2
|
+
_study_h5.py
|
|
3
|
+
|
|
4
|
+
This module provides HDF5-based save/load functionality for the Study class.
|
|
5
|
+
It handles serialization and deserialization of Polars DataFrames with complex objects
|
|
6
|
+
It handles serialization and deserialization of Polars DataFrames with complex objects
|
|
7
|
+
It handles serialization and deserialization of Polars DataFrames with complex objects
|
|
8
|
+
like Chromatogram and Spectrum instances.
|
|
9
|
+
|
|
10
|
+
Key Features:
|
|
11
|
+
- **HDF5 Storage**: Efficient compressed storage using HDF5 format
|
|
12
|
+
- **Complex Object Serialization**: JSON-based serialization for Chromatogram and Spectrum objects
|
|
13
|
+
- **Schema-based loading**: Uses study5_schema.json for proper type handling
|
|
14
|
+
- **Error Handling**: Robust error handling and logging
|
|
15
|
+
|
|
16
|
+
Dependencies:
|
|
17
|
+
- `h5py`: For HDF5 file operations
|
|
18
|
+
- `polars`: For DataFrame handling
|
|
19
|
+
- `json`: For complex object serialization
|
|
20
|
+
- `numpy`: For numerical array operations
|
|
21
|
+
|
|
22
|
+
Functions:
|
|
23
|
+
- `_save_study5()`: Save study to .study5 HDF5 file (new format)
|
|
24
|
+
- `_load_study5()`: Load study from .study5 HDF5 file (new format)
|
|
25
|
+
- `_save_h5()`: Save study to .h5 file (legacy format)
|
|
26
|
+
- `_load_h5()`: Load study from .h5 file (legacy format)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
|
|
34
|
+
import h5py
|
|
35
|
+
import polars as pl
|
|
36
|
+
from tqdm import tqdm
|
|
37
|
+
|
|
38
|
+
from masster.chromatogram import Chromatogram
|
|
39
|
+
from masster.spectrum import Spectrum
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Helper functions for HDF5 operations
|
|
43
|
+
def _load_schema(schema_path: str) -> dict:
|
|
44
|
+
"""Load schema from JSON file with error handling."""
|
|
45
|
+
try:
|
|
46
|
+
with open(schema_path) as f:
|
|
47
|
+
return json.load(f) # type: ignore
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _decode_bytes_attr(attr_value):
|
|
53
|
+
"""Decode metadata attribute, handling both bytes and string types."""
|
|
54
|
+
if isinstance(attr_value, bytes):
|
|
55
|
+
return attr_value.decode("utf-8")
|
|
56
|
+
return str(attr_value) if attr_value is not None else ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _save_dataframe_optimized(df, group, schema, df_name, logger, chunk_size=10000):
|
|
60
|
+
"""
|
|
61
|
+
Save an entire DataFrame to HDF5 with optimized batch processing and memory efficiency.
|
|
62
|
+
|
|
63
|
+
This function replaces individual column processing with batch operations for much
|
|
64
|
+
better performance on large datasets (300+ samples).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
df: Polars DataFrame to save
|
|
68
|
+
group: HDF5 group to save to
|
|
69
|
+
schema: Schema for column ordering
|
|
70
|
+
df_name: Name of the DataFrame for schema lookup
|
|
71
|
+
logger: Logger instance
|
|
72
|
+
chunk_size: Number of rows to process at once for memory efficiency
|
|
73
|
+
"""
|
|
74
|
+
if df is None or df.is_empty():
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Reorder columns according to schema
|
|
79
|
+
df_ordered = _reorder_columns_by_schema(df.clone(), schema, df_name)
|
|
80
|
+
total_rows = len(df_ordered)
|
|
81
|
+
|
|
82
|
+
# Group columns by processing type for batch optimization
|
|
83
|
+
numeric_cols = []
|
|
84
|
+
string_cols = []
|
|
85
|
+
object_cols = []
|
|
86
|
+
|
|
87
|
+
for col in df_ordered.columns:
|
|
88
|
+
dtype = str(df_ordered[col].dtype).lower()
|
|
89
|
+
if dtype == "object":
|
|
90
|
+
object_cols.append(col)
|
|
91
|
+
elif dtype in ["string", "utf8"]:
|
|
92
|
+
string_cols.append(col)
|
|
93
|
+
else:
|
|
94
|
+
numeric_cols.append(col)
|
|
95
|
+
|
|
96
|
+
logger.debug(f"Saving {df_name}: {total_rows} rows, {len(numeric_cols)} numeric, {len(string_cols)} string, {len(object_cols)} object columns")
|
|
97
|
+
|
|
98
|
+
# Process numeric columns in batch (most efficient)
|
|
99
|
+
if numeric_cols:
|
|
100
|
+
for col in numeric_cols:
|
|
101
|
+
_save_numeric_column_fast(group, col, df_ordered[col], logger)
|
|
102
|
+
|
|
103
|
+
# Process string columns in batch
|
|
104
|
+
if string_cols:
|
|
105
|
+
for col in string_cols:
|
|
106
|
+
_save_string_column_fast(group, col, df_ordered[col], logger)
|
|
107
|
+
|
|
108
|
+
# Process object columns with optimized serialization
|
|
109
|
+
if object_cols:
|
|
110
|
+
_save_object_columns_optimized(group, df_ordered, object_cols, logger, chunk_size)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Failed to save DataFrame {df_name}: {e}")
|
|
114
|
+
# Fallback to old method for safety
|
|
115
|
+
_save_dataframe_column_legacy(df, group, schema, df_name, logger)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _save_numeric_column_fast(group, col, data_series, logger):
|
|
119
|
+
"""Fast numeric column saving with optimal compression."""
|
|
120
|
+
try:
|
|
121
|
+
import numpy as np
|
|
122
|
+
|
|
123
|
+
# Get compression settings based on column name
|
|
124
|
+
if col in ["consensus_uid", "feature_uid", "scan_id", "rt", "mz", "intensity"]:
|
|
125
|
+
compression_kwargs = {"compression": "lzf", "shuffle": True}
|
|
126
|
+
else:
|
|
127
|
+
compression_kwargs = {"compression": "lzf"}
|
|
128
|
+
|
|
129
|
+
# Convert to numpy array efficiently
|
|
130
|
+
try:
|
|
131
|
+
data_array = data_series.to_numpy()
|
|
132
|
+
except Exception:
|
|
133
|
+
# Fallback for complex data types
|
|
134
|
+
data_array = np.array(data_series.to_list())
|
|
135
|
+
|
|
136
|
+
# Handle None/null values efficiently
|
|
137
|
+
if data_array.dtype == object:
|
|
138
|
+
# Check if this is actually a list/array column that should be treated as object
|
|
139
|
+
sample_value = None
|
|
140
|
+
for val in data_array:
|
|
141
|
+
if val is not None:
|
|
142
|
+
sample_value = val
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
# If sample value is a list/array, treat as object column
|
|
146
|
+
if isinstance(sample_value, (list, tuple, np.ndarray)):
|
|
147
|
+
logger.debug(f"Column '{col}' contains array-like data, treating as object")
|
|
148
|
+
_save_dataframe_column_legacy_single(group, col, data_series.to_list(), "object", logger)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Otherwise, convert None values to -123 sentinel for mixed-type numeric columns
|
|
152
|
+
try:
|
|
153
|
+
data_array = np.array([(-123 if x is None else float(x)) for x in data_array])
|
|
154
|
+
except (ValueError, TypeError):
|
|
155
|
+
# If conversion fails, this is not a numeric column
|
|
156
|
+
logger.debug(f"Column '{col}' is not numeric, treating as object")
|
|
157
|
+
_save_dataframe_column_legacy_single(group, col, data_series.to_list(), "object", logger)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
group.create_dataset(col, data=data_array, **compression_kwargs)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning(f"Failed to save numeric column '{col}' efficiently: {e}")
|
|
164
|
+
# Fallback to old method
|
|
165
|
+
_save_dataframe_column_legacy_single(group, col, data_series.to_list(), str(data_series.dtype), logger)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _save_string_column_fast(group, col, data_series, logger):
|
|
169
|
+
"""Fast string column saving with optimal compression."""
|
|
170
|
+
try:
|
|
171
|
+
# Convert to string array efficiently
|
|
172
|
+
string_data = ["None" if x is None else str(x) for x in data_series.to_list()]
|
|
173
|
+
|
|
174
|
+
compression_kwargs = {"compression": "gzip", "compression_opts": 6}
|
|
175
|
+
group.create_dataset(col, data=string_data, **compression_kwargs)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Failed to save string column '{col}' efficiently: {e}")
|
|
179
|
+
# Fallback to old method
|
|
180
|
+
_save_dataframe_column_legacy_single(group, col, data_series.to_list(), "string", logger)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _save_object_columns_optimized(group, df, object_cols, logger, chunk_size):
|
|
184
|
+
"""Optimized object column processing with chunking and parallel serialization."""
|
|
185
|
+
import json
|
|
186
|
+
|
|
187
|
+
def serialize_chunk(col_name, chunk_data):
|
|
188
|
+
"""Serialize a chunk of object data."""
|
|
189
|
+
serialized_chunk = []
|
|
190
|
+
|
|
191
|
+
if col_name == "chrom":
|
|
192
|
+
# Handle Chromatogram objects
|
|
193
|
+
for item in chunk_data:
|
|
194
|
+
if item is not None:
|
|
195
|
+
serialized_chunk.append(item.to_json())
|
|
196
|
+
else:
|
|
197
|
+
serialized_chunk.append("None")
|
|
198
|
+
elif col_name == "ms2_scans":
|
|
199
|
+
# Handle MS2 scan lists
|
|
200
|
+
for item in chunk_data:
|
|
201
|
+
if item is not None:
|
|
202
|
+
serialized_chunk.append(json.dumps(list(item)))
|
|
203
|
+
else:
|
|
204
|
+
serialized_chunk.append("None")
|
|
205
|
+
elif col_name == "ms2_specs":
|
|
206
|
+
# Handle MS2 spectrum lists
|
|
207
|
+
for item in chunk_data:
|
|
208
|
+
if item is not None:
|
|
209
|
+
json_strings = []
|
|
210
|
+
for spectrum in item:
|
|
211
|
+
if spectrum is not None:
|
|
212
|
+
json_strings.append(spectrum.to_json())
|
|
213
|
+
else:
|
|
214
|
+
json_strings.append("None")
|
|
215
|
+
serialized_chunk.append(json.dumps(json_strings))
|
|
216
|
+
else:
|
|
217
|
+
serialized_chunk.append(json.dumps(["None"]))
|
|
218
|
+
elif col_name in ["adducts", "adduct_values"]:
|
|
219
|
+
# Handle lists
|
|
220
|
+
for item in chunk_data:
|
|
221
|
+
if item is not None:
|
|
222
|
+
serialized_chunk.append(json.dumps(item))
|
|
223
|
+
else:
|
|
224
|
+
serialized_chunk.append("[]")
|
|
225
|
+
elif col_name == "spec":
|
|
226
|
+
# Handle single Spectrum objects
|
|
227
|
+
for item in chunk_data:
|
|
228
|
+
if item is not None:
|
|
229
|
+
serialized_chunk.append(item.to_json())
|
|
230
|
+
else:
|
|
231
|
+
serialized_chunk.append("None")
|
|
232
|
+
else:
|
|
233
|
+
logger.warning(f"Unknown object column '{col_name}', using default serialization")
|
|
234
|
+
for item in chunk_data:
|
|
235
|
+
serialized_chunk.append(str(item) if item is not None else "None")
|
|
236
|
+
|
|
237
|
+
return serialized_chunk
|
|
238
|
+
|
|
239
|
+
# Process each object column
|
|
240
|
+
for col in object_cols:
|
|
241
|
+
try:
|
|
242
|
+
data_list = df[col].to_list()
|
|
243
|
+
total_items = len(data_list)
|
|
244
|
+
|
|
245
|
+
if total_items == 0:
|
|
246
|
+
group.create_dataset(col, data=[], compression="gzip", compression_opts=6)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# For small datasets, process directly
|
|
250
|
+
if total_items <= chunk_size:
|
|
251
|
+
serialized_data = serialize_chunk(col, data_list)
|
|
252
|
+
group.create_dataset(col, data=serialized_data, compression="gzip", compression_opts=6)
|
|
253
|
+
else:
|
|
254
|
+
# For large datasets, use chunked processing with parallel serialization
|
|
255
|
+
logger.debug(f"Processing large object column '{col}' with {total_items} items in chunks")
|
|
256
|
+
|
|
257
|
+
all_serialized = []
|
|
258
|
+
num_chunks = (total_items + chunk_size - 1) // chunk_size
|
|
259
|
+
|
|
260
|
+
# Use thread pool for parallel serialization of chunks
|
|
261
|
+
with ThreadPoolExecutor(max_workers=min(4, num_chunks)) as executor:
|
|
262
|
+
futures = {}
|
|
263
|
+
|
|
264
|
+
for i in range(0, total_items, chunk_size):
|
|
265
|
+
chunk = data_list[i:i + chunk_size]
|
|
266
|
+
future = executor.submit(serialize_chunk, col, chunk)
|
|
267
|
+
futures[future] = i
|
|
268
|
+
|
|
269
|
+
# Collect results in order
|
|
270
|
+
results = {}
|
|
271
|
+
for future in as_completed(futures):
|
|
272
|
+
chunk_start = futures[future]
|
|
273
|
+
try:
|
|
274
|
+
chunk_result = future.result()
|
|
275
|
+
results[chunk_start] = chunk_result
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.warning(f"Failed to serialize chunk starting at {chunk_start} for column '{col}': {e}")
|
|
278
|
+
# Fallback to simple string conversion for this chunk
|
|
279
|
+
chunk = data_list[chunk_start:chunk_start + chunk_size]
|
|
280
|
+
results[chunk_start] = [str(item) if item is not None else "None" for item in chunk]
|
|
281
|
+
|
|
282
|
+
# Reassemble in correct order
|
|
283
|
+
for i in range(0, total_items, chunk_size):
|
|
284
|
+
if i in results:
|
|
285
|
+
all_serialized.extend(results[i])
|
|
286
|
+
|
|
287
|
+
group.create_dataset(col, data=all_serialized, compression="gzip", compression_opts=6)
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.warning(f"Failed to save object column '{col}' with optimization: {e}")
|
|
291
|
+
# Fallback to old method
|
|
292
|
+
_save_dataframe_column_legacy_single(group, col, df[col].to_list(), "object", logger)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _save_dataframe_column_legacy_single(group, col: str, data, dtype: str, logger, compression="gzip"):
|
|
296
|
+
"""Legacy single column save method for fallback."""
|
|
297
|
+
# This is the original _save_dataframe_column method for compatibility
|
|
298
|
+
return _save_dataframe_column_legacy(group, col, data, dtype, logger, compression)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _save_dataframe_column_legacy(group, col: str, data, dtype: str, logger, compression="gzip"):
|
|
302
|
+
"""
|
|
303
|
+
Save a single DataFrame column to an HDF5 group with optimized compression.
|
|
304
|
+
|
|
305
|
+
This optimized version uses context-aware compression strategies for better
|
|
306
|
+
performance and smaller file sizes. Different compression algorithms are
|
|
307
|
+
selected based on data type and column name patterns.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
group: HDF5 group to save to
|
|
311
|
+
col: Column name
|
|
312
|
+
data: Column data
|
|
313
|
+
dtype: Data type string
|
|
314
|
+
logger: Logger instance
|
|
315
|
+
compression: Default compression (used for compatibility, but overridden by optimization)
|
|
316
|
+
|
|
317
|
+
Compression Strategy:
|
|
318
|
+
- LZF + shuffle: Fast access data (consensus_uid, rt, mz, intensity, scan_id)
|
|
319
|
+
- GZIP level 6: JSON objects (chromatograms, spectra) and string data
|
|
320
|
+
- GZIP level 9: Bulk storage data (large collections)
|
|
321
|
+
- LZF: Standard numeric arrays
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Optimized compression configuration
|
|
325
|
+
COMPRESSION_CONFIG = {
|
|
326
|
+
"fast_access": {"compression": "lzf", "shuffle": True}, # Fast I/O for IDs, rt, mz
|
|
327
|
+
"numeric": {"compression": "lzf"}, # Standard numeric data
|
|
328
|
+
"string": {"compression": "gzip", "compression_opts": 6}, # String data
|
|
329
|
+
"json": {"compression": "gzip", "compression_opts": 6}, # JSON objects
|
|
330
|
+
"bulk": {"compression": "gzip", "compression_opts": 9}, # Large bulk data
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
def get_optimal_compression(column_name, data_type, data_size=None):
|
|
334
|
+
"""Get optimal compression settings based on column type and usage pattern."""
|
|
335
|
+
# Fast access columns (frequently read IDs and coordinates)
|
|
336
|
+
if column_name in [
|
|
337
|
+
"consensus_uid",
|
|
338
|
+
"feature_uid",
|
|
339
|
+
"scan_id",
|
|
340
|
+
"rt",
|
|
341
|
+
"mz",
|
|
342
|
+
"intensity",
|
|
343
|
+
"rt_original",
|
|
344
|
+
"mz_original",
|
|
345
|
+
]:
|
|
346
|
+
return COMPRESSION_CONFIG["fast_access"]
|
|
347
|
+
|
|
348
|
+
# JSON object columns (complex serialized data)
|
|
349
|
+
elif column_name in ["spectrum", "chromatogram", "chromatograms", "ms2_specs", "chrom"]:
|
|
350
|
+
return COMPRESSION_CONFIG["json"]
|
|
351
|
+
|
|
352
|
+
# String/text columns
|
|
353
|
+
elif data_type in ["string", "object"] and column_name in ["sample_name", "file_path", "label", "file_type"]:
|
|
354
|
+
return COMPRESSION_CONFIG["string"]
|
|
355
|
+
|
|
356
|
+
# Large bulk numeric data
|
|
357
|
+
elif data_size and data_size > 100000:
|
|
358
|
+
return COMPRESSION_CONFIG["bulk"]
|
|
359
|
+
|
|
360
|
+
# Standard numeric data
|
|
361
|
+
else:
|
|
362
|
+
return COMPRESSION_CONFIG["numeric"]
|
|
363
|
+
|
|
364
|
+
# Get data size for optimization decisions
|
|
365
|
+
data_size = len(data) if hasattr(data, "__len__") else None
|
|
366
|
+
|
|
367
|
+
# Get optimal compression settings
|
|
368
|
+
optimal_compression = get_optimal_compression(col, dtype, data_size)
|
|
369
|
+
if dtype == "object" or dtype.startswith("list"):
|
|
370
|
+
if col == "chrom":
|
|
371
|
+
# Handle Chromatogram objects
|
|
372
|
+
data_as_str = []
|
|
373
|
+
for item in data:
|
|
374
|
+
if item is not None:
|
|
375
|
+
data_as_str.append(item.to_json())
|
|
376
|
+
else:
|
|
377
|
+
data_as_str.append("None")
|
|
378
|
+
group.create_dataset(col, data=data_as_str, compression=compression)
|
|
379
|
+
elif col == "ms2_scans":
|
|
380
|
+
# Handle MS2 scan lists
|
|
381
|
+
data_as_json_strings = []
|
|
382
|
+
for item in data:
|
|
383
|
+
if item is not None:
|
|
384
|
+
data_as_json_strings.append(json.dumps(list(item)))
|
|
385
|
+
else:
|
|
386
|
+
data_as_json_strings.append("None")
|
|
387
|
+
group.create_dataset(col, data=data_as_json_strings, **optimal_compression)
|
|
388
|
+
elif col == "ms2_specs":
|
|
389
|
+
# Handle MS2 spectrum lists
|
|
390
|
+
data_as_lists_of_strings = []
|
|
391
|
+
for item in data:
|
|
392
|
+
if item is not None:
|
|
393
|
+
json_strings = []
|
|
394
|
+
for spectrum in item:
|
|
395
|
+
if spectrum is not None:
|
|
396
|
+
json_strings.append(spectrum.to_json())
|
|
397
|
+
else:
|
|
398
|
+
json_strings.append("None")
|
|
399
|
+
data_as_lists_of_strings.append(json_strings)
|
|
400
|
+
else:
|
|
401
|
+
data_as_lists_of_strings.append(["None"])
|
|
402
|
+
# Convert to serialized data
|
|
403
|
+
serialized_data = [json.dumps(item) for item in data_as_lists_of_strings]
|
|
404
|
+
group.create_dataset(col, data=serialized_data, **optimal_compression)
|
|
405
|
+
elif col == "adducts":
|
|
406
|
+
# Handle adducts lists (List(String))
|
|
407
|
+
data_as_json_strings = []
|
|
408
|
+
for item in data:
|
|
409
|
+
if item is not None:
|
|
410
|
+
data_as_json_strings.append(json.dumps(item))
|
|
411
|
+
else:
|
|
412
|
+
data_as_json_strings.append("[]")
|
|
413
|
+
group.create_dataset(col, data=data_as_json_strings, **optimal_compression)
|
|
414
|
+
elif col == "adduct_values":
|
|
415
|
+
# Handle adduct_values lists (List(Struct))
|
|
416
|
+
data_as_json_strings = []
|
|
417
|
+
for item in data:
|
|
418
|
+
if item is not None:
|
|
419
|
+
data_as_json_strings.append(json.dumps(item))
|
|
420
|
+
else:
|
|
421
|
+
data_as_json_strings.append("[]")
|
|
422
|
+
group.create_dataset(col, data=data_as_json_strings, **optimal_compression)
|
|
423
|
+
elif col == "spec":
|
|
424
|
+
# Handle single Spectrum objects
|
|
425
|
+
data_as_str = []
|
|
426
|
+
for item in data:
|
|
427
|
+
if item is not None:
|
|
428
|
+
data_as_str.append(item.to_json())
|
|
429
|
+
else:
|
|
430
|
+
data_as_str.append("None")
|
|
431
|
+
group.create_dataset(col, data=data_as_str, compression=compression)
|
|
432
|
+
else:
|
|
433
|
+
logger.warning(f"Unexpectedly, column '{col}' has dtype '{dtype}'. Implement serialization for this column.")
|
|
434
|
+
elif dtype == "string":
|
|
435
|
+
# Handle string columns
|
|
436
|
+
string_data = ["None" if x is None else str(x) for x in data]
|
|
437
|
+
group.create_dataset(col, data=string_data, **optimal_compression)
|
|
438
|
+
else:
|
|
439
|
+
# Handle numeric columns
|
|
440
|
+
try:
|
|
441
|
+
# Convert None values to -123 sentinel value for numeric columns
|
|
442
|
+
import numpy as np
|
|
443
|
+
|
|
444
|
+
data_array = np.array(data)
|
|
445
|
+
|
|
446
|
+
# Check if it's a numeric dtype that might have None/null values
|
|
447
|
+
if data_array.dtype == object:
|
|
448
|
+
# Convert None values to -123 for numeric columns with mixed types
|
|
449
|
+
processed_data = []
|
|
450
|
+
for item in data:
|
|
451
|
+
if item is None:
|
|
452
|
+
processed_data.append(-123)
|
|
453
|
+
else:
|
|
454
|
+
try:
|
|
455
|
+
# Try to convert to float to check if it's numeric
|
|
456
|
+
processed_data.append(int(float(item)))
|
|
457
|
+
except (ValueError, TypeError):
|
|
458
|
+
# If conversion fails, keep original value (might be string)
|
|
459
|
+
processed_data.append(item)
|
|
460
|
+
data_array = np.array(processed_data)
|
|
461
|
+
|
|
462
|
+
group.create_dataset(col, data=data_array, **optimal_compression)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.warning(f"Failed to save column '{col}': {e}")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# Keep the original function as _save_dataframe_column for backward compatibility
|
|
468
|
+
_save_dataframe_column = _save_dataframe_column_legacy
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _reconstruct_object_column(data_col, col_name: str):
|
|
472
|
+
"""Reconstruct object columns from serialized HDF5 data."""
|
|
473
|
+
reconstructed_data: list = []
|
|
474
|
+
|
|
475
|
+
for item in data_col:
|
|
476
|
+
if isinstance(item, bytes):
|
|
477
|
+
item = item.decode("utf-8")
|
|
478
|
+
|
|
479
|
+
# Handle non-string data (e.g., float32 NaN from corrupted compression)
|
|
480
|
+
if not isinstance(item, str):
|
|
481
|
+
import numpy as np
|
|
482
|
+
if isinstance(item, (float, np.floating)) and np.isnan(item):
|
|
483
|
+
reconstructed_data.append(None)
|
|
484
|
+
continue
|
|
485
|
+
else:
|
|
486
|
+
reconstructed_data.append(None)
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
if item == "None" or item == "":
|
|
490
|
+
reconstructed_data.append(None)
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
if col_name == "chrom":
|
|
495
|
+
reconstructed_data.append(Chromatogram.from_json(item))
|
|
496
|
+
elif col_name == "ms2_scans":
|
|
497
|
+
scan_list = json.loads(item)
|
|
498
|
+
reconstructed_data.append(scan_list)
|
|
499
|
+
elif col_name == "ms2_specs":
|
|
500
|
+
json_list = json.loads(item)
|
|
501
|
+
if json_list == ["None"]:
|
|
502
|
+
reconstructed_data.append(None)
|
|
503
|
+
else:
|
|
504
|
+
spectrum_list: list = []
|
|
505
|
+
for json_str in json_list:
|
|
506
|
+
if json_str == "None":
|
|
507
|
+
spectrum_list.append(None)
|
|
508
|
+
else:
|
|
509
|
+
spectrum_list.append(Spectrum.from_json(json_str))
|
|
510
|
+
reconstructed_data.append(spectrum_list)
|
|
511
|
+
elif col_name == "spec":
|
|
512
|
+
reconstructed_data.append(Spectrum.from_json(item))
|
|
513
|
+
elif col_name == "adducts":
|
|
514
|
+
# Handle adducts lists (List(Struct)) - now contains dicts instead of strings
|
|
515
|
+
adducts_list = json.loads(item)
|
|
516
|
+
reconstructed_data.append(adducts_list)
|
|
517
|
+
else:
|
|
518
|
+
# Unknown object column
|
|
519
|
+
reconstructed_data.append(None)
|
|
520
|
+
except (json.JSONDecodeError, ValueError):
|
|
521
|
+
reconstructed_data.append(None)
|
|
522
|
+
|
|
523
|
+
return reconstructed_data
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _clean_string_nulls(df: pl.DataFrame) -> pl.DataFrame:
|
|
527
|
+
"""Convert string null representations to proper nulls."""
|
|
528
|
+
for col in df.columns:
|
|
529
|
+
if df[col].dtype == pl.Utf8:
|
|
530
|
+
df = df.with_columns([
|
|
531
|
+
pl.when(pl.col(col).is_in(["None", "", "null", "NULL"])).then(None).otherwise(pl.col(col)).alias(col),
|
|
532
|
+
])
|
|
533
|
+
return df
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _apply_schema_casting(df: pl.DataFrame, schema: dict, df_name: str) -> pl.DataFrame:
|
|
537
|
+
"""Apply schema-based type casting to DataFrame columns."""
|
|
538
|
+
if df_name not in schema or "columns" not in schema[df_name]:
|
|
539
|
+
return df
|
|
540
|
+
|
|
541
|
+
schema_columns = schema[df_name]["columns"]
|
|
542
|
+
cast_exprs = []
|
|
543
|
+
|
|
544
|
+
for col in df.columns:
|
|
545
|
+
if col in schema_columns:
|
|
546
|
+
dtype_str = schema_columns[col]["dtype"]
|
|
547
|
+
# Convert string representation to actual Polars type
|
|
548
|
+
if dtype_str == "pl.Object":
|
|
549
|
+
cast_exprs.append(pl.col(col)) # Keep Object type as is
|
|
550
|
+
elif dtype_str == "pl.Int64":
|
|
551
|
+
cast_exprs.append(pl.col(col).cast(pl.Int64, strict=False))
|
|
552
|
+
elif dtype_str == "pl.Float64":
|
|
553
|
+
cast_exprs.append(pl.col(col).cast(pl.Float64, strict=False))
|
|
554
|
+
elif dtype_str == "pl.Utf8":
|
|
555
|
+
cast_exprs.append(pl.col(col).cast(pl.Utf8, strict=False))
|
|
556
|
+
elif dtype_str == "pl.Int32":
|
|
557
|
+
cast_exprs.append(pl.col(col).cast(pl.Int32, strict=False))
|
|
558
|
+
elif dtype_str == "pl.Boolean":
|
|
559
|
+
cast_exprs.append(pl.col(col).cast(pl.Boolean, strict=False))
|
|
560
|
+
elif dtype_str == "pl.Null":
|
|
561
|
+
cast_exprs.append(pl.col(col).cast(pl.Null, strict=False))
|
|
562
|
+
else:
|
|
563
|
+
cast_exprs.append(pl.col(col)) # Keep original type
|
|
564
|
+
else:
|
|
565
|
+
cast_exprs.append(pl.col(col)) # Keep original type
|
|
566
|
+
|
|
567
|
+
if cast_exprs:
|
|
568
|
+
df = df.with_columns(cast_exprs)
|
|
569
|
+
|
|
570
|
+
return df
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _reorder_columns_by_schema(df: pl.DataFrame, schema: dict, df_name: str) -> pl.DataFrame:
|
|
574
|
+
"""Reorder DataFrame columns to match schema order."""
|
|
575
|
+
if df_name not in schema or "columns" not in schema[df_name]:
|
|
576
|
+
return df
|
|
577
|
+
|
|
578
|
+
schema_columns = list(schema[df_name]["columns"].keys())
|
|
579
|
+
# Only reorder columns that exist in both schema and DataFrame
|
|
580
|
+
existing_columns = [col for col in schema_columns if col in df.columns]
|
|
581
|
+
# Add any extra columns not in schema at the end
|
|
582
|
+
extra_columns = [col for col in df.columns if col not in schema_columns]
|
|
583
|
+
final_column_order = existing_columns + extra_columns
|
|
584
|
+
|
|
585
|
+
return df.select(final_column_order)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _create_dataframe_with_objects(data: dict, object_columns: list) -> pl.DataFrame:
|
|
589
|
+
"""Create DataFrame handling Object columns properly."""
|
|
590
|
+
object_data = {k: v for k, v in data.items() if k in object_columns}
|
|
591
|
+
regular_data = {k: v for k, v in data.items() if k not in object_columns}
|
|
592
|
+
|
|
593
|
+
# Determine expected length from regular data or first object column
|
|
594
|
+
expected_length = None
|
|
595
|
+
if regular_data:
|
|
596
|
+
for values in regular_data.values():
|
|
597
|
+
if values is not None and hasattr(values, '__len__'):
|
|
598
|
+
expected_length = len(values)
|
|
599
|
+
break
|
|
600
|
+
|
|
601
|
+
if expected_length is None and object_data:
|
|
602
|
+
for values in object_data.values():
|
|
603
|
+
if values is not None and hasattr(values, '__len__'):
|
|
604
|
+
expected_length = len(values)
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
if expected_length is None:
|
|
608
|
+
expected_length = 0
|
|
609
|
+
|
|
610
|
+
# Fix any object columns that have None or empty values
|
|
611
|
+
for col in object_columns:
|
|
612
|
+
if col in object_data:
|
|
613
|
+
values = object_data[col]
|
|
614
|
+
if values is None or (hasattr(values, '__len__') and len(values) == 0):
|
|
615
|
+
object_data[col] = [None] * expected_length
|
|
616
|
+
# print(f"DEBUG: Fixed object column '{col}' to have length {expected_length}")
|
|
617
|
+
|
|
618
|
+
# Create DataFrame with regular columns first
|
|
619
|
+
if regular_data:
|
|
620
|
+
df = pl.DataFrame(regular_data)
|
|
621
|
+
# print(f"DEBUG: Created DataFrame with regular columns, shape: {df.shape}")
|
|
622
|
+
# Add Object columns one by one
|
|
623
|
+
for col, values in object_data.items():
|
|
624
|
+
# print(f"DEBUG: Adding object column '{col}', type: {type(values)}, length: {len(values) if values is not None else 'None'}")
|
|
625
|
+
if col == "adducts":
|
|
626
|
+
# Handle adducts as List(Struct) - now contains dicts
|
|
627
|
+
df = df.with_columns([pl.Series(col, values, dtype=pl.List(pl.Struct([
|
|
628
|
+
pl.Field("adduct", pl.Utf8),
|
|
629
|
+
pl.Field("count", pl.Int64),
|
|
630
|
+
pl.Field("percentage", pl.Float64),
|
|
631
|
+
pl.Field("mass", pl.Float64)
|
|
632
|
+
])))])
|
|
633
|
+
else:
|
|
634
|
+
# Other object columns stay as Object
|
|
635
|
+
df = df.with_columns([pl.Series(col, values, dtype=pl.Object)])
|
|
636
|
+
else:
|
|
637
|
+
# Only Object columns
|
|
638
|
+
df = pl.DataFrame()
|
|
639
|
+
for col, values in object_data.items():
|
|
640
|
+
# print(f"DEBUG: Creating object column '{col}', type: {type(values)}, length: {len(values) if values is not None else 'None'}")
|
|
641
|
+
if col == "adducts":
|
|
642
|
+
# Handle adducts as List(Struct) - now contains dicts
|
|
643
|
+
df = df.with_columns([pl.Series(col, values, dtype=pl.List(pl.Struct([
|
|
644
|
+
pl.Field("adduct", pl.Utf8),
|
|
645
|
+
pl.Field("count", pl.Int64),
|
|
646
|
+
pl.Field("percentage", pl.Float64),
|
|
647
|
+
pl.Field("mass", pl.Float64)
|
|
648
|
+
])))])
|
|
649
|
+
else:
|
|
650
|
+
# Other object columns stay as Object
|
|
651
|
+
df = df.with_columns([pl.Series(col, values, dtype=pl.Object)])
|
|
652
|
+
|
|
653
|
+
return df
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _load_dataframe_from_group(group, schema: dict, df_name: str, logger, object_columns: list = None) -> pl.DataFrame:
|
|
657
|
+
"""Load a DataFrame from HDF5 group using schema."""
|
|
658
|
+
if object_columns is None:
|
|
659
|
+
object_columns = []
|
|
660
|
+
|
|
661
|
+
data: dict = {}
|
|
662
|
+
missing_columns = []
|
|
663
|
+
|
|
664
|
+
# Iterate through schema columns in order to maintain column ordering
|
|
665
|
+
logger.debug(
|
|
666
|
+
f"Loading {df_name} - schema type: {type(schema)}, content: {schema.keys() if isinstance(schema, dict) else 'Not a dict'}",
|
|
667
|
+
)
|
|
668
|
+
schema_section = schema.get(df_name, {}) if isinstance(schema, dict) else {}
|
|
669
|
+
logger.debug(f"Schema section for {df_name}: {schema_section}")
|
|
670
|
+
schema_columns = schema_section.get("columns", []) if isinstance(schema_section, dict) else []
|
|
671
|
+
logger.debug(f"Schema columns for {df_name}: {schema_columns}")
|
|
672
|
+
if schema_columns is None:
|
|
673
|
+
schema_columns = []
|
|
674
|
+
|
|
675
|
+
# First pass: load all existing columns
|
|
676
|
+
for col in schema_columns or []:
|
|
677
|
+
if col not in group:
|
|
678
|
+
missing_columns.append(col)
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
dtype = schema[df_name]["columns"][col].get("dtype", "native")
|
|
682
|
+
if dtype == "pl.Object" or col in object_columns:
|
|
683
|
+
# Handle object columns specially
|
|
684
|
+
data[col] = _reconstruct_object_column(group[col][:], col)
|
|
685
|
+
else:
|
|
686
|
+
# Regular columns
|
|
687
|
+
column_data = group[col][:]
|
|
688
|
+
|
|
689
|
+
# Convert -123 sentinel values back to None for numeric columns
|
|
690
|
+
if len(column_data) > 0:
|
|
691
|
+
# Check if it's a numeric column that might contain sentinel values
|
|
692
|
+
try:
|
|
693
|
+
import numpy as np
|
|
694
|
+
|
|
695
|
+
data_array = np.array(column_data)
|
|
696
|
+
if data_array.dtype in [np.float32, np.float64, np.int32, np.int64]:
|
|
697
|
+
# Replace -123 sentinel values with None
|
|
698
|
+
processed_data: list = []
|
|
699
|
+
for item in column_data:
|
|
700
|
+
if item == -123:
|
|
701
|
+
processed_data.append(None)
|
|
702
|
+
else:
|
|
703
|
+
processed_data.append(item)
|
|
704
|
+
data[col] = processed_data
|
|
705
|
+
else:
|
|
706
|
+
data[col] = column_data
|
|
707
|
+
except Exception:
|
|
708
|
+
# If any error occurs, use original data
|
|
709
|
+
data[col] = column_data
|
|
710
|
+
else:
|
|
711
|
+
data[col] = column_data
|
|
712
|
+
|
|
713
|
+
# Determine expected DataFrame length from loaded columns
|
|
714
|
+
expected_length = None
|
|
715
|
+
for col, values in data.items():
|
|
716
|
+
if values is not None and hasattr(values, '__len__'):
|
|
717
|
+
expected_length = len(values)
|
|
718
|
+
logger.debug(f"Determined expected_length={expected_length} from loaded column '{col}'")
|
|
719
|
+
break
|
|
720
|
+
|
|
721
|
+
# If no data loaded yet, try HDF5 columns directly
|
|
722
|
+
if expected_length is None:
|
|
723
|
+
hdf5_columns = list(group.keys())
|
|
724
|
+
for col in hdf5_columns:
|
|
725
|
+
col_data = group[col][:]
|
|
726
|
+
if expected_length is None:
|
|
727
|
+
expected_length = len(col_data)
|
|
728
|
+
logger.debug(f"Determined expected_length={expected_length} from HDF5 column '{col}'")
|
|
729
|
+
break
|
|
730
|
+
|
|
731
|
+
# Default to 0 if no data found
|
|
732
|
+
if expected_length is None:
|
|
733
|
+
expected_length = 0
|
|
734
|
+
logger.debug("No columns found, setting expected_length=0")
|
|
735
|
+
|
|
736
|
+
# Second pass: handle missing columns
|
|
737
|
+
for col in missing_columns:
|
|
738
|
+
logger.warning(f"Column '{col}' not found in {df_name}.")
|
|
739
|
+
# For missing columns, create appropriately sized array of None values
|
|
740
|
+
if col in object_columns:
|
|
741
|
+
data[col] = [None] * expected_length
|
|
742
|
+
logger.debug(f"Created missing object column '{col}' with length {expected_length}")
|
|
743
|
+
else:
|
|
744
|
+
data[col] = [None] * expected_length
|
|
745
|
+
logger.debug(f"Created missing regular column '{col}' with length {expected_length}")
|
|
746
|
+
|
|
747
|
+
# Check for columns in HDF5 file that are not in schema (for backward compatibility)
|
|
748
|
+
hdf5_columns = list(group.keys())
|
|
749
|
+
extra_columns = [col for col in hdf5_columns if col not in (schema_columns or [])]
|
|
750
|
+
|
|
751
|
+
for col in extra_columns:
|
|
752
|
+
logger.info(f"Loading extra column '{col}' not in schema for {df_name}")
|
|
753
|
+
column_data = group[col][:]
|
|
754
|
+
|
|
755
|
+
# Try to determine if this should be treated as an object column
|
|
756
|
+
# by checking if the data looks like JSON strings
|
|
757
|
+
if len(column_data) > 0 and isinstance(column_data[0], bytes):
|
|
758
|
+
try:
|
|
759
|
+
# Check if it looks like JSON
|
|
760
|
+
test_decode = column_data[0].decode('utf-8')
|
|
761
|
+
if test_decode.startswith('[') or test_decode.startswith('{'):
|
|
762
|
+
# Looks like JSON, treat as object column
|
|
763
|
+
data[col] = _reconstruct_object_column(column_data, col)
|
|
764
|
+
if col not in object_columns:
|
|
765
|
+
object_columns.append(col)
|
|
766
|
+
else:
|
|
767
|
+
# Regular string data
|
|
768
|
+
data[col] = [item.decode('utf-8') if isinstance(item, bytes) else item for item in column_data]
|
|
769
|
+
except Exception:
|
|
770
|
+
# If decoding fails, treat as regular data
|
|
771
|
+
data[col] = column_data
|
|
772
|
+
else:
|
|
773
|
+
data[col] = column_data
|
|
774
|
+
|
|
775
|
+
if not data:
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
# Handle byte string conversion for non-object columns
|
|
779
|
+
# Only convert to strings for columns that should actually be strings
|
|
780
|
+
for col, values in data.items():
|
|
781
|
+
if col not in object_columns and values is not None and len(values) > 0 and isinstance(values[0], bytes):
|
|
782
|
+
# Check schema to see if this should be a string column
|
|
783
|
+
should_be_string = False
|
|
784
|
+
if df_name in schema and "columns" in schema[df_name] and col in schema[df_name]["columns"]:
|
|
785
|
+
dtype_str = schema[df_name]["columns"][col]["dtype"]
|
|
786
|
+
should_be_string = dtype_str == "pl.Utf8"
|
|
787
|
+
|
|
788
|
+
if should_be_string:
|
|
789
|
+
processed_values = []
|
|
790
|
+
for val in values:
|
|
791
|
+
if isinstance(val, bytes):
|
|
792
|
+
val = val.decode("utf-8")
|
|
793
|
+
processed_values.append(val)
|
|
794
|
+
data[col] = processed_values
|
|
795
|
+
# If not a string column, leave as original data type (will be cast by schema)
|
|
796
|
+
|
|
797
|
+
# Create DataFrame with Object columns handled properly
|
|
798
|
+
if object_columns:
|
|
799
|
+
logger.debug(f"Creating DataFrame with object columns: {object_columns}")
|
|
800
|
+
for col in object_columns:
|
|
801
|
+
if col in data:
|
|
802
|
+
logger.debug(f"Object column '{col}': length={len(data[col]) if data[col] is not None else 'None'}")
|
|
803
|
+
df = _create_dataframe_with_objects(data, object_columns)
|
|
804
|
+
else:
|
|
805
|
+
df = pl.DataFrame(data)
|
|
806
|
+
|
|
807
|
+
# Clean null values and apply schema
|
|
808
|
+
df = _clean_string_nulls(df)
|
|
809
|
+
df = _apply_schema_casting(df, schema, df_name)
|
|
810
|
+
df = _reorder_columns_by_schema(df, schema, df_name)
|
|
811
|
+
|
|
812
|
+
return df
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _save_study5_compressed(self, filename=None):
|
|
816
|
+
"""
|
|
817
|
+
Compressed save identical to _save_study5 but skips serialization of chrom and ms2_specs columns in features_df.
|
|
818
|
+
|
|
819
|
+
This version maintains full compatibility with _load_study5() while providing performance benefits
|
|
820
|
+
by skipping the serialization of heavy object columns (chrom and ms2_specs) in features_df.
|
|
821
|
+
"""
|
|
822
|
+
|
|
823
|
+
# if no extension is given, add .study5
|
|
824
|
+
if not filename.endswith(".study5"):
|
|
825
|
+
filename += ".study5"
|
|
826
|
+
|
|
827
|
+
self.logger.info(f"Compressed saving study to {filename}")
|
|
828
|
+
|
|
829
|
+
# delete existing file if it exists
|
|
830
|
+
if os.path.exists(filename):
|
|
831
|
+
os.remove(filename)
|
|
832
|
+
|
|
833
|
+
# Load schema for column ordering
|
|
834
|
+
schema_path = os.path.join(os.path.dirname(__file__), "study5_schema.json")
|
|
835
|
+
schema = _load_schema(schema_path)
|
|
836
|
+
if not schema:
|
|
837
|
+
self.logger.warning(f"Could not load schema from {schema_path}")
|
|
838
|
+
|
|
839
|
+
with h5py.File(filename, "w") as f:
|
|
840
|
+
# Count total DataFrames to save for progress tracking
|
|
841
|
+
dataframes_to_save = []
|
|
842
|
+
if self.samples_df is not None and not self.samples_df.is_empty():
|
|
843
|
+
dataframes_to_save.append(("samples", len(self.samples_df)))
|
|
844
|
+
if self.features_df is not None and not self.features_df.is_empty():
|
|
845
|
+
dataframes_to_save.append(("features", len(self.features_df)))
|
|
846
|
+
if self.consensus_df is not None and not self.consensus_df.is_empty():
|
|
847
|
+
dataframes_to_save.append(("consensus", len(self.consensus_df)))
|
|
848
|
+
if self.consensus_mapping_df is not None and not self.consensus_mapping_df.is_empty():
|
|
849
|
+
dataframes_to_save.append(("consensus_mapping", len(self.consensus_mapping_df)))
|
|
850
|
+
if self.consensus_ms2 is not None and not self.consensus_ms2.is_empty():
|
|
851
|
+
dataframes_to_save.append(("consensus_ms2", len(self.consensus_ms2)))
|
|
852
|
+
|
|
853
|
+
total_steps = len(dataframes_to_save) + 1 # +1 for metadata
|
|
854
|
+
|
|
855
|
+
# Show progress for large saves
|
|
856
|
+
tdqm_disable = self.log_level not in ["TRACE", "DEBUG", "INFO"] or total_steps < 2
|
|
857
|
+
|
|
858
|
+
with tqdm(
|
|
859
|
+
total=total_steps,
|
|
860
|
+
desc=f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {getattr(self, 'log_label', '')}Fast saving study",
|
|
861
|
+
disable=tdqm_disable,
|
|
862
|
+
) as pbar:
|
|
863
|
+
|
|
864
|
+
# Create groups for organization
|
|
865
|
+
metadata_group = f.create_group("metadata")
|
|
866
|
+
features_group = f.create_group("features")
|
|
867
|
+
consensus_group = f.create_group("consensus")
|
|
868
|
+
consensus_mapping_group = f.create_group("consensus_mapping")
|
|
869
|
+
consensus_ms2_group = f.create_group("consensus_ms2")
|
|
870
|
+
|
|
871
|
+
# Store metadata
|
|
872
|
+
metadata_group.attrs["format"] = "master-study-1"
|
|
873
|
+
metadata_group.attrs["folder"] = str(self.folder) if self.folder is not None else ""
|
|
874
|
+
metadata_group.attrs["label"] = str(self.label) if hasattr(self, "label") and self.label is not None else ""
|
|
875
|
+
|
|
876
|
+
# Store parameters as JSON
|
|
877
|
+
if hasattr(self, "parameters") and self.history is not None:
|
|
878
|
+
try:
|
|
879
|
+
parameters_json = json.dumps(self.history, indent=2)
|
|
880
|
+
metadata_group.create_dataset("parameters", data=parameters_json)
|
|
881
|
+
except (TypeError, ValueError) as e:
|
|
882
|
+
self.logger.warning(f"Failed to serialize history: {e}")
|
|
883
|
+
metadata_group.create_dataset("parameters", data="")
|
|
884
|
+
else:
|
|
885
|
+
metadata_group.create_dataset("parameters", data="")
|
|
886
|
+
|
|
887
|
+
pbar.update(1)
|
|
888
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {getattr(self, 'log_label', '')}Saving dataframes")
|
|
889
|
+
|
|
890
|
+
# Store samples_df - use optimized batch processing
|
|
891
|
+
if self.samples_df is not None and not self.samples_df.is_empty():
|
|
892
|
+
samples_group = f.create_group("samples")
|
|
893
|
+
self.logger.debug(f"Saving samples_df with {len(self.samples_df)} rows using optimized method")
|
|
894
|
+
_save_dataframe_optimized(self.samples_df, samples_group, schema, "samples_df", self.logger)
|
|
895
|
+
pbar.update(1)
|
|
896
|
+
|
|
897
|
+
# Store features_df - use fast method that skips chrom and ms2_specs columns
|
|
898
|
+
if self.features_df is not None and not self.features_df.is_empty():
|
|
899
|
+
self.logger.debug(f"Fast saving features_df with {len(self.features_df)} rows (skipping chrom and ms2_specs)")
|
|
900
|
+
_save_dataframe_optimized_fast(self.features_df, features_group, schema, "features_df", self.logger)
|
|
901
|
+
pbar.update(1)
|
|
902
|
+
|
|
903
|
+
# Store consensus_df - use optimized batch processing
|
|
904
|
+
if self.consensus_df is not None and not self.consensus_df.is_empty():
|
|
905
|
+
self.logger.debug(f"Saving consensus_df with {len(self.consensus_df)} rows using optimized method")
|
|
906
|
+
_save_dataframe_optimized(self.consensus_df, consensus_group, schema, "consensus_df", self.logger)
|
|
907
|
+
pbar.update(1)
|
|
908
|
+
|
|
909
|
+
# Store consensus_mapping_df - keep existing fast method
|
|
910
|
+
if self.consensus_mapping_df is not None and not self.consensus_mapping_df.is_empty():
|
|
911
|
+
consensus_mapping = self.consensus_mapping_df.clone()
|
|
912
|
+
self.logger.debug(f"Saving consensus_mapping_df with {len(consensus_mapping)} rows")
|
|
913
|
+
for col in consensus_mapping.columns:
|
|
914
|
+
try:
|
|
915
|
+
data = consensus_mapping[col].to_numpy()
|
|
916
|
+
# Use LZF compression for consensus mapping data
|
|
917
|
+
consensus_mapping_group.create_dataset(col, data=data, compression="lzf", shuffle=True)
|
|
918
|
+
except Exception as e:
|
|
919
|
+
self.logger.warning(f"Failed to save column '{col}' in consensus_mapping_df: {e}")
|
|
920
|
+
pbar.update(1)
|
|
921
|
+
|
|
922
|
+
# Store consensus_ms2 - use optimized batch processing
|
|
923
|
+
if self.consensus_ms2 is not None and not self.consensus_ms2.is_empty():
|
|
924
|
+
self.logger.debug(f"Saving consensus_ms2 with {len(self.consensus_ms2)} rows using optimized method")
|
|
925
|
+
_save_dataframe_optimized(self.consensus_ms2, consensus_ms2_group, schema, "consensus_ms2", self.logger)
|
|
926
|
+
pbar.update(1)
|
|
927
|
+
|
|
928
|
+
self.logger.info(f"Fast study saved successfully to {filename}")
|
|
929
|
+
self.logger.debug(f"Fast save completed for {filename}")
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _save_dataframe_optimized_fast(df, group, schema, df_name, logger, chunk_size=10000):
|
|
933
|
+
"""
|
|
934
|
+
Save DataFrame with optimized batch processing, but skip chrom and ms2_specs columns for features_df.
|
|
935
|
+
|
|
936
|
+
This function is identical to _save_dataframe_optimized but excludes heavy object columns
|
|
937
|
+
(chrom and ms2_specs) when saving features_df to improve performance.
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
df: Polars DataFrame to save
|
|
941
|
+
group: HDF5 group to save to
|
|
942
|
+
schema: Schema for column ordering
|
|
943
|
+
df_name: Name of the DataFrame for schema lookup
|
|
944
|
+
logger: Logger instance
|
|
945
|
+
chunk_size: Number of rows to process at once for memory efficiency
|
|
946
|
+
"""
|
|
947
|
+
if df is None or df.is_empty():
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
try:
|
|
951
|
+
# Reorder columns according to schema
|
|
952
|
+
df_ordered = _reorder_columns_by_schema(df.clone(), schema, df_name)
|
|
953
|
+
|
|
954
|
+
# Skip chrom and ms2_specs columns for features_df
|
|
955
|
+
if df_name == "features_df":
|
|
956
|
+
skip_columns = ["chrom", "ms2_specs"]
|
|
957
|
+
df_ordered = df_ordered.select([col for col in df_ordered.columns if col not in skip_columns])
|
|
958
|
+
logger.debug(f"Fast save: skipping columns {skip_columns} for {df_name}")
|
|
959
|
+
|
|
960
|
+
total_rows = len(df_ordered)
|
|
961
|
+
|
|
962
|
+
# Group columns by processing type for batch optimization
|
|
963
|
+
numeric_cols = []
|
|
964
|
+
string_cols = []
|
|
965
|
+
object_cols = []
|
|
966
|
+
|
|
967
|
+
for col in df_ordered.columns:
|
|
968
|
+
dtype = str(df_ordered[col].dtype).lower()
|
|
969
|
+
if dtype == "object":
|
|
970
|
+
object_cols.append(col)
|
|
971
|
+
elif dtype in ["string", "utf8"]:
|
|
972
|
+
string_cols.append(col)
|
|
973
|
+
else:
|
|
974
|
+
numeric_cols.append(col)
|
|
975
|
+
|
|
976
|
+
logger.debug(f"Saving {df_name}: {total_rows} rows, {len(numeric_cols)} numeric, {len(string_cols)} string, {len(object_cols)} object columns")
|
|
977
|
+
|
|
978
|
+
# Process numeric columns in batch (most efficient)
|
|
979
|
+
if numeric_cols:
|
|
980
|
+
for col in numeric_cols:
|
|
981
|
+
_save_numeric_column_fast(group, col, df_ordered[col], logger)
|
|
982
|
+
|
|
983
|
+
# Process string columns in batch
|
|
984
|
+
if string_cols:
|
|
985
|
+
for col in string_cols:
|
|
986
|
+
_save_string_column_fast(group, col, df_ordered[col], logger)
|
|
987
|
+
|
|
988
|
+
# Process object columns with optimized serialization
|
|
989
|
+
if object_cols:
|
|
990
|
+
_save_object_columns_optimized(group, df_ordered, object_cols, logger, chunk_size)
|
|
991
|
+
|
|
992
|
+
except Exception as e:
|
|
993
|
+
logger.error(f"Failed to save DataFrame {df_name}: {e}")
|
|
994
|
+
# Fallback to old method for safety
|
|
995
|
+
_save_dataframe_column_legacy(df, group, schema, df_name, logger)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _save_study5(self, filename=None):
|
|
999
|
+
"""
|
|
1000
|
+
Save the Study instance data to a .study5 HDF5 file with optimized schema-based format.
|
|
1001
|
+
|
|
1002
|
+
This method saves all Study DataFrames (samples_df, features_df, consensus_df,
|
|
1003
|
+
consensus_mapping_df, consensus_ms2) using the schema defined in study5_schema.json
|
|
1004
|
+
for proper Polars DataFrame type handling.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
filename (str, optional): Target file name. If None, uses default based on folder.
|
|
1008
|
+
|
|
1009
|
+
Stores:
|
|
1010
|
+
- metadata/format (str): Data format identifier ("master-study-1")
|
|
1011
|
+
- metadata/folder (str): Study default folder path
|
|
1012
|
+
- metadata/label (str): Study label
|
|
1013
|
+
- metadata/parameters (str): JSON-serialized parameters dictionary
|
|
1014
|
+
- samples/: samples_df DataFrame data
|
|
1015
|
+
- features/: features_df DataFrame data with Chromatogram and Spectrum objects
|
|
1016
|
+
- consensus/: consensus_df DataFrame data
|
|
1017
|
+
- consensus_mapping/: consensus_mapping_df DataFrame data
|
|
1018
|
+
- consensus_ms2/: consensus_ms2 DataFrame data with Spectrum objects
|
|
1019
|
+
|
|
1020
|
+
Notes:
|
|
1021
|
+
- Uses HDF5 format with compression for efficient storage.
|
|
1022
|
+
- Chromatogram objects are serialized as JSON for reconstruction.
|
|
1023
|
+
- MS2 scan lists and Spectrum objects are properly serialized.
|
|
1024
|
+
- Parameters dictionary (nested dicts) are JSON-serialized for storage.
|
|
1025
|
+
- Optimized for use with _load_study5() method.
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
# if no extension is given, add .study5
|
|
1029
|
+
if not filename.endswith(".study5"):
|
|
1030
|
+
filename += ".study5"
|
|
1031
|
+
|
|
1032
|
+
self.logger.info(f"Saving study to {filename}")
|
|
1033
|
+
|
|
1034
|
+
# delete existing file if it exists
|
|
1035
|
+
if os.path.exists(filename):
|
|
1036
|
+
os.remove(filename)
|
|
1037
|
+
|
|
1038
|
+
# Load schema for column ordering
|
|
1039
|
+
schema_path = os.path.join(os.path.dirname(__file__), "study5_schema.json")
|
|
1040
|
+
schema = _load_schema(schema_path)
|
|
1041
|
+
if not schema:
|
|
1042
|
+
self.logger.warning(f"Could not load schema from {schema_path}")
|
|
1043
|
+
|
|
1044
|
+
with h5py.File(filename, "w") as f:
|
|
1045
|
+
# Count total DataFrames to save for progress tracking
|
|
1046
|
+
dataframes_to_save = []
|
|
1047
|
+
if self.samples_df is not None and not self.samples_df.is_empty():
|
|
1048
|
+
dataframes_to_save.append(("samples", len(self.samples_df)))
|
|
1049
|
+
if self.features_df is not None and not self.features_df.is_empty():
|
|
1050
|
+
dataframes_to_save.append(("features", len(self.features_df)))
|
|
1051
|
+
if self.consensus_df is not None and not self.consensus_df.is_empty():
|
|
1052
|
+
dataframes_to_save.append(("consensus", len(self.consensus_df)))
|
|
1053
|
+
if self.consensus_mapping_df is not None and not self.consensus_mapping_df.is_empty():
|
|
1054
|
+
dataframes_to_save.append(("consensus_mapping", len(self.consensus_mapping_df)))
|
|
1055
|
+
if self.consensus_ms2 is not None and not self.consensus_ms2.is_empty():
|
|
1056
|
+
dataframes_to_save.append(("consensus_ms2", len(self.consensus_ms2)))
|
|
1057
|
+
|
|
1058
|
+
total_steps = len(dataframes_to_save) + 1 # +1 for metadata
|
|
1059
|
+
|
|
1060
|
+
# Show progress for large saves
|
|
1061
|
+
tdqm_disable = self.log_level not in ["TRACE", "DEBUG", "INFO"] or total_steps < 2
|
|
1062
|
+
|
|
1063
|
+
with tqdm(
|
|
1064
|
+
total=total_steps,
|
|
1065
|
+
desc=f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {getattr(self, 'log_label', '')}Saving study",
|
|
1066
|
+
disable=tdqm_disable,
|
|
1067
|
+
) as pbar:
|
|
1068
|
+
|
|
1069
|
+
# Create groups for organization
|
|
1070
|
+
metadata_group = f.create_group("metadata")
|
|
1071
|
+
features_group = f.create_group("features")
|
|
1072
|
+
consensus_group = f.create_group("consensus")
|
|
1073
|
+
consensus_mapping_group = f.create_group("consensus_mapping")
|
|
1074
|
+
consensus_ms2_group = f.create_group("consensus_ms2")
|
|
1075
|
+
|
|
1076
|
+
# Store metadata
|
|
1077
|
+
metadata_group.attrs["format"] = "master-study-1"
|
|
1078
|
+
metadata_group.attrs["folder"] = str(self.folder) if self.folder is not None else ""
|
|
1079
|
+
metadata_group.attrs["label"] = str(self.label) if hasattr(self, "label") and self.label is not None else ""
|
|
1080
|
+
|
|
1081
|
+
# Store parameters as JSON
|
|
1082
|
+
if hasattr(self, "parameters") and self.history is not None:
|
|
1083
|
+
try:
|
|
1084
|
+
parameters_json = json.dumps(self.history, indent=2)
|
|
1085
|
+
metadata_group.create_dataset("parameters", data=parameters_json)
|
|
1086
|
+
except (TypeError, ValueError) as e:
|
|
1087
|
+
self.logger.warning(f"Failed to serialize history: {e}")
|
|
1088
|
+
metadata_group.create_dataset("parameters", data="")
|
|
1089
|
+
else:
|
|
1090
|
+
metadata_group.create_dataset("parameters", data="")
|
|
1091
|
+
|
|
1092
|
+
pbar.update(1)
|
|
1093
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {getattr(self, 'log_label', '')}Saving dataframes")
|
|
1094
|
+
|
|
1095
|
+
# Store samples_df - use optimized batch processing
|
|
1096
|
+
if self.samples_df is not None and not self.samples_df.is_empty():
|
|
1097
|
+
samples_group = f.create_group("samples")
|
|
1098
|
+
self.logger.debug(f"Saving samples_df with {len(self.samples_df)} rows using optimized method")
|
|
1099
|
+
_save_dataframe_optimized(self.samples_df, samples_group, schema, "samples_df", self.logger)
|
|
1100
|
+
pbar.update(1)
|
|
1101
|
+
|
|
1102
|
+
# Store features_df - use optimized batch processing
|
|
1103
|
+
if self.features_df is not None and not self.features_df.is_empty():
|
|
1104
|
+
self.logger.debug(f"Saving features_df with {len(self.features_df)} rows using optimized method")
|
|
1105
|
+
_save_dataframe_optimized(self.features_df, features_group, schema, "features_df", self.logger)
|
|
1106
|
+
pbar.update(1)
|
|
1107
|
+
|
|
1108
|
+
# Store consensus_df - use optimized batch processing
|
|
1109
|
+
if self.consensus_df is not None and not self.consensus_df.is_empty():
|
|
1110
|
+
self.logger.debug(f"Saving consensus_df with {len(self.consensus_df)} rows using optimized method")
|
|
1111
|
+
_save_dataframe_optimized(self.consensus_df, consensus_group, schema, "consensus_df", self.logger)
|
|
1112
|
+
pbar.update(1)
|
|
1113
|
+
|
|
1114
|
+
# Store consensus_mapping_df - keep existing fast method
|
|
1115
|
+
if self.consensus_mapping_df is not None and not self.consensus_mapping_df.is_empty():
|
|
1116
|
+
consensus_mapping = self.consensus_mapping_df.clone()
|
|
1117
|
+
self.logger.debug(f"Saving consensus_mapping_df with {len(consensus_mapping)} rows")
|
|
1118
|
+
for col in consensus_mapping.columns:
|
|
1119
|
+
try:
|
|
1120
|
+
data = consensus_mapping[col].to_numpy()
|
|
1121
|
+
# Use LZF compression for consensus mapping data
|
|
1122
|
+
consensus_mapping_group.create_dataset(col, data=data, compression="lzf", shuffle=True)
|
|
1123
|
+
except Exception as e:
|
|
1124
|
+
self.logger.warning(f"Failed to save column '{col}' in consensus_mapping_df: {e}")
|
|
1125
|
+
pbar.update(1)
|
|
1126
|
+
|
|
1127
|
+
# Store consensus_ms2 - use optimized batch processing
|
|
1128
|
+
if self.consensus_ms2 is not None and not self.consensus_ms2.is_empty():
|
|
1129
|
+
self.logger.debug(f"Saving consensus_ms2 with {len(self.consensus_ms2)} rows using optimized method")
|
|
1130
|
+
_save_dataframe_optimized(self.consensus_ms2, consensus_ms2_group, schema, "consensus_ms2", self.logger)
|
|
1131
|
+
pbar.update(1)
|
|
1132
|
+
|
|
1133
|
+
self.logger.info(f"Study saved successfully to {filename}")
|
|
1134
|
+
self.logger.debug(f"Save completed for {filename}")
|
|
1135
|
+
self.logger.debug(f"Save completed for {filename}")
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _load_study5(self, filename=None):
|
|
1139
|
+
"""
|
|
1140
|
+
Load Study instance data from a .study5 HDF5 file.
|
|
1141
|
+
|
|
1142
|
+
Restores all Study DataFrames that were saved with _save_study5() method using the
|
|
1143
|
+
schema defined in study5_schema.json for proper Polars DataFrame reconstruction.
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
filename (str, optional): Path to the .study5 HDF5 file to load. If None, uses default.
|
|
1147
|
+
|
|
1148
|
+
Returns:
|
|
1149
|
+
None (modifies self in place)
|
|
1150
|
+
|
|
1151
|
+
Notes:
|
|
1152
|
+
- Restores DataFrames with proper schema typing from study5_schema.json
|
|
1153
|
+
- Handles Chromatogram and Spectrum object reconstruction
|
|
1154
|
+
- Properly handles MS2 scan lists and spectrum lists
|
|
1155
|
+
- Restores parameters dictionary from JSON serialization
|
|
1156
|
+
"""
|
|
1157
|
+
|
|
1158
|
+
self.logger.info(f"Loading study from {filename}")
|
|
1159
|
+
|
|
1160
|
+
# Handle default filename
|
|
1161
|
+
if filename is None:
|
|
1162
|
+
if self.folder is not None:
|
|
1163
|
+
filename = os.path.join(self.folder, "study.study5")
|
|
1164
|
+
else:
|
|
1165
|
+
self.logger.error("Either filename or folder must be provided")
|
|
1166
|
+
return
|
|
1167
|
+
|
|
1168
|
+
# Add .study5 extension if not provided
|
|
1169
|
+
if not filename.endswith(".study5"):
|
|
1170
|
+
filename += ".study5"
|
|
1171
|
+
|
|
1172
|
+
if not os.path.exists(filename):
|
|
1173
|
+
self.logger.error(f"File {filename} does not exist")
|
|
1174
|
+
return
|
|
1175
|
+
|
|
1176
|
+
# Load schema for proper DataFrame reconstruction
|
|
1177
|
+
schema_path = os.path.join(os.path.dirname(__file__), "study5_schema.json")
|
|
1178
|
+
schema = _load_schema(schema_path)
|
|
1179
|
+
if not schema:
|
|
1180
|
+
self.logger.warning(f"Schema file {schema_path} not found. Using default types.")
|
|
1181
|
+
|
|
1182
|
+
# Define loading steps for progress tracking
|
|
1183
|
+
loading_steps = [
|
|
1184
|
+
"metadata",
|
|
1185
|
+
"samples_df",
|
|
1186
|
+
"features_df",
|
|
1187
|
+
"consensus_df",
|
|
1188
|
+
"consensus_mapping_df",
|
|
1189
|
+
"consensus_ms2"
|
|
1190
|
+
]
|
|
1191
|
+
|
|
1192
|
+
# Check if progress bar should be disabled based on log level
|
|
1193
|
+
tdqm_disable = self.log_level not in ["TRACE", "DEBUG", "INFO"]
|
|
1194
|
+
|
|
1195
|
+
# Define loading steps for progress tracking
|
|
1196
|
+
loading_steps = [
|
|
1197
|
+
"metadata",
|
|
1198
|
+
"samples_df",
|
|
1199
|
+
"features_df",
|
|
1200
|
+
"consensus_df",
|
|
1201
|
+
"consensus_mapping_df",
|
|
1202
|
+
"consensus_ms2"
|
|
1203
|
+
]
|
|
1204
|
+
|
|
1205
|
+
# Check if progress bar should be disabled based on log level
|
|
1206
|
+
tdqm_disable = self.log_level not in ["TRACE", "DEBUG", "INFO"]
|
|
1207
|
+
|
|
1208
|
+
with h5py.File(filename, "r") as f:
|
|
1209
|
+
# Use progress bar to show loading progress
|
|
1210
|
+
with tqdm(
|
|
1211
|
+
total=len(loading_steps),
|
|
1212
|
+
desc=f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading study",
|
|
1213
|
+
disable=tdqm_disable,
|
|
1214
|
+
) as pbar:
|
|
1215
|
+
|
|
1216
|
+
# Load metadata
|
|
1217
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading metadata")
|
|
1218
|
+
if "metadata" in f:
|
|
1219
|
+
metadata = f["metadata"]
|
|
1220
|
+
self.folder = _decode_bytes_attr(metadata.attrs.get("folder", ""))
|
|
1221
|
+
if hasattr(self, "label"):
|
|
1222
|
+
self.label = _decode_bytes_attr(metadata.attrs.get("label", ""))
|
|
1223
|
+
|
|
1224
|
+
# Load parameters from JSON
|
|
1225
|
+
if "parameters" in metadata:
|
|
1226
|
+
try:
|
|
1227
|
+
parameters_data = metadata["parameters"][()]
|
|
1228
|
+
if isinstance(parameters_data, bytes):
|
|
1229
|
+
parameters_data = parameters_data.decode("utf-8")
|
|
1230
|
+
|
|
1231
|
+
if parameters_data and parameters_data != "":
|
|
1232
|
+
self.history = json.loads(parameters_data)
|
|
1233
|
+
else:
|
|
1234
|
+
self.history = {}
|
|
1235
|
+
except (json.JSONDecodeError, ValueError, TypeError) as e:
|
|
1236
|
+
self.logger.warning(f"Failed to deserialize parameters: {e}")
|
|
1237
|
+
self.history = {}
|
|
1238
|
+
else:
|
|
1239
|
+
self.history = {}
|
|
1240
|
+
|
|
1241
|
+
# Reconstruct self.parameters from loaded history
|
|
1242
|
+
from masster.study.defaults.study_def import study_defaults
|
|
1243
|
+
|
|
1244
|
+
# Always create a fresh study_defaults object to ensure we have all defaults
|
|
1245
|
+
self.parameters = study_defaults()
|
|
1246
|
+
|
|
1247
|
+
# Update parameters from loaded history if available
|
|
1248
|
+
if self.history and "study" in self.history:
|
|
1249
|
+
study_params = self.history["study"]
|
|
1250
|
+
if isinstance(study_params, dict):
|
|
1251
|
+
failed_params = self.parameters.set_from_dict(study_params, validate=False)
|
|
1252
|
+
if failed_params:
|
|
1253
|
+
self.logger.debug(f"Could not set study parameters: {failed_params}")
|
|
1254
|
+
else:
|
|
1255
|
+
self.logger.debug("Successfully updated parameters from loaded history")
|
|
1256
|
+
else:
|
|
1257
|
+
self.logger.debug("Study parameters in history are not a valid dictionary")
|
|
1258
|
+
else:
|
|
1259
|
+
self.logger.debug("No study parameters found in history, using defaults")
|
|
1260
|
+
|
|
1261
|
+
# Synchronize instance attributes with parameters (similar to __init__)
|
|
1262
|
+
# Note: folder and label are already loaded from metadata attributes above
|
|
1263
|
+
# but we ensure they match the parameters for consistency
|
|
1264
|
+
if hasattr(self.parameters, 'folder') and self.parameters.folder is not None:
|
|
1265
|
+
self.folder = self.parameters.folder
|
|
1266
|
+
if hasattr(self.parameters, 'label') and self.parameters.label is not None:
|
|
1267
|
+
self.label = self.parameters.label
|
|
1268
|
+
if hasattr(self.parameters, 'log_level'):
|
|
1269
|
+
self.log_level = self.parameters.log_level
|
|
1270
|
+
if hasattr(self.parameters, 'log_label'):
|
|
1271
|
+
self.log_label = self.parameters.log_label if self.parameters.log_label is not None else ""
|
|
1272
|
+
if hasattr(self.parameters, 'log_sink'):
|
|
1273
|
+
self.log_sink = self.parameters.log_sink
|
|
1274
|
+
pbar.update(1)
|
|
1275
|
+
|
|
1276
|
+
# Load samples_df
|
|
1277
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading samples")
|
|
1278
|
+
if "samples" in f and len(f["samples"].keys()) > 0:
|
|
1279
|
+
self.samples_df = _load_dataframe_from_group(f["samples"], schema, "samples_df", self.logger)
|
|
1280
|
+
else:
|
|
1281
|
+
# Initialize empty samples_df with the correct schema if no data exists
|
|
1282
|
+
self.logger.debug("No samples data found in study5 file. Initializing empty samples_df.")
|
|
1283
|
+
self.samples_df = pl.DataFrame(
|
|
1284
|
+
{
|
|
1285
|
+
"sample_uid": [],
|
|
1286
|
+
"sample_name": [],
|
|
1287
|
+
"sample_path": [],
|
|
1288
|
+
"sample_type": [],
|
|
1289
|
+
"size": [],
|
|
1290
|
+
"map_id": [],
|
|
1291
|
+
"file_source": [],
|
|
1292
|
+
"ms1": [],
|
|
1293
|
+
"ms2": [],
|
|
1294
|
+
},
|
|
1295
|
+
schema={
|
|
1296
|
+
"sample_uid": pl.Int64,
|
|
1297
|
+
"sample_name": pl.Utf8,
|
|
1298
|
+
"sample_path": pl.Utf8,
|
|
1299
|
+
"sample_type": pl.Utf8,
|
|
1300
|
+
"size": pl.Int64,
|
|
1301
|
+
"map_id": pl.Utf8,
|
|
1302
|
+
"file_source": pl.Utf8,
|
|
1303
|
+
"ms1": pl.Int64,
|
|
1304
|
+
"ms2": pl.Int64,
|
|
1305
|
+
},
|
|
1306
|
+
)
|
|
1307
|
+
pbar.update(1)
|
|
1308
|
+
# Load samples_df
|
|
1309
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading samples")
|
|
1310
|
+
if "samples" in f and len(f["samples"].keys()) > 0:
|
|
1311
|
+
self.samples_df = _load_dataframe_from_group(f["samples"], schema, "samples_df", self.logger)
|
|
1312
|
+
else:
|
|
1313
|
+
# Initialize empty samples_df with the correct schema if no data exists
|
|
1314
|
+
self.logger.debug("No samples data found in study5 file. Initializing empty samples_df.")
|
|
1315
|
+
self.samples_df = pl.DataFrame(
|
|
1316
|
+
{
|
|
1317
|
+
"sample_uid": [],
|
|
1318
|
+
"sample_name": [],
|
|
1319
|
+
"sample_path": [],
|
|
1320
|
+
"sample_type": [],
|
|
1321
|
+
"size": [],
|
|
1322
|
+
"map_id": [],
|
|
1323
|
+
"file_source": [],
|
|
1324
|
+
"ms1": [],
|
|
1325
|
+
"ms2": [],
|
|
1326
|
+
},
|
|
1327
|
+
schema={
|
|
1328
|
+
"sample_uid": pl.Int64,
|
|
1329
|
+
"sample_name": pl.Utf8,
|
|
1330
|
+
"sample_path": pl.Utf8,
|
|
1331
|
+
"sample_type": pl.Utf8,
|
|
1332
|
+
"size": pl.Int64,
|
|
1333
|
+
"map_id": pl.Utf8,
|
|
1334
|
+
"file_source": pl.Utf8,
|
|
1335
|
+
"ms1": pl.Int64,
|
|
1336
|
+
"ms2": pl.Int64,
|
|
1337
|
+
},
|
|
1338
|
+
)
|
|
1339
|
+
pbar.update(1)
|
|
1340
|
+
|
|
1341
|
+
# Load features_df
|
|
1342
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading features")
|
|
1343
|
+
if "features" in f and len(f["features"].keys()) > 0:
|
|
1344
|
+
object_columns = ["chrom", "ms2_scans", "ms2_specs"]
|
|
1345
|
+
self.features_df = _load_dataframe_from_group(f["features"], schema, "features_df", self.logger, object_columns)
|
|
1346
|
+
else:
|
|
1347
|
+
self.features_df = None
|
|
1348
|
+
pbar.update(1)
|
|
1349
|
+
|
|
1350
|
+
# Load consensus_df
|
|
1351
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading consensus")
|
|
1352
|
+
if "consensus" in f and len(f["consensus"].keys()) > 0:
|
|
1353
|
+
# Only include adducts in object_columns if it actually exists in the file
|
|
1354
|
+
object_columns = []
|
|
1355
|
+
if "adducts" in f["consensus"]:
|
|
1356
|
+
object_columns.append("adducts")
|
|
1357
|
+
|
|
1358
|
+
self.consensus_df = _load_dataframe_from_group(f["consensus"], schema, "consensus_df", self.logger, object_columns)
|
|
1359
|
+
|
|
1360
|
+
# Backward compatibility: If adducts column doesn't exist, initialize with empty lists
|
|
1361
|
+
if self.consensus_df is not None:
|
|
1362
|
+
if "adducts" not in self.consensus_df.columns or self.consensus_df["adducts"].dtype == pl.Null:
|
|
1363
|
+
self.logger.info("Adding missing 'adducts' column for backward compatibility")
|
|
1364
|
+
empty_adducts: list[list] = [[] for _ in range(len(self.consensus_df))]
|
|
1365
|
+
|
|
1366
|
+
# If column exists but is Null, drop it first
|
|
1367
|
+
if "adducts" in self.consensus_df.columns:
|
|
1368
|
+
self.consensus_df = self.consensus_df.drop("adducts")
|
|
1369
|
+
|
|
1370
|
+
self.consensus_df = self.consensus_df.with_columns([
|
|
1371
|
+
pl.Series("adducts", empty_adducts, dtype=pl.List(pl.Struct([
|
|
1372
|
+
pl.Field("adduct", pl.Utf8),
|
|
1373
|
+
pl.Field("count", pl.Int64),
|
|
1374
|
+
pl.Field("percentage", pl.Float64),
|
|
1375
|
+
pl.Field("mass", pl.Float64)
|
|
1376
|
+
])))
|
|
1377
|
+
])
|
|
1378
|
+
else:
|
|
1379
|
+
self.consensus_df = None
|
|
1380
|
+
pbar.update(1)
|
|
1381
|
+
|
|
1382
|
+
# Load consensus_mapping_df
|
|
1383
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading consensus mapping")
|
|
1384
|
+
if "consensus_mapping" in f and len(f["consensus_mapping"].keys()) > 0:
|
|
1385
|
+
self.consensus_mapping_df = _load_dataframe_from_group(f["consensus_mapping"], schema, "consensus_mapping_df", self.logger)
|
|
1386
|
+
else:
|
|
1387
|
+
self.consensus_mapping_df = None
|
|
1388
|
+
pbar.update(1)
|
|
1389
|
+
# Load consensus_mapping_df
|
|
1390
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading consensus mapping")
|
|
1391
|
+
if "consensus_mapping" in f and len(f["consensus_mapping"].keys()) > 0:
|
|
1392
|
+
self.consensus_mapping_df = _load_dataframe_from_group(f["consensus_mapping"], schema, "consensus_mapping_df", self.logger)
|
|
1393
|
+
else:
|
|
1394
|
+
self.consensus_mapping_df = None
|
|
1395
|
+
pbar.update(1)
|
|
1396
|
+
|
|
1397
|
+
# Load consensus_ms2
|
|
1398
|
+
pbar.set_description(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} | INFO | {self.log_label}Loading consensus MS2")
|
|
1399
|
+
if "consensus_ms2" in f and len(f["consensus_ms2"].keys()) > 0:
|
|
1400
|
+
object_columns = ["spec"]
|
|
1401
|
+
self.consensus_ms2 = _load_dataframe_from_group(f["consensus_ms2"], schema, "consensus_ms2", self.logger, object_columns)
|
|
1402
|
+
else:
|
|
1403
|
+
self.consensus_ms2 = None
|
|
1404
|
+
pbar.update(1)
|
|
1405
|
+
|
|
1406
|
+
self.logger.debug("Study loaded")
|