masster 0.4.9__py3-none-any.whl → 0.4.10__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/_version.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
 
4
- __version__ = "0.4.8"
4
+ __version__ = "0.4.10"
5
5
 
6
6
 
7
7
  def get_version():
@@ -0,0 +1,9 @@
1
+ """
2
+ Library module for masster.
3
+
4
+ This module provides the Lib class for managing compound libraries and feature annotation.
5
+ """
6
+
7
+ from .lib import Lib
8
+
9
+ __all__ = ["Lib"]
masster/lib/lib.py ADDED
@@ -0,0 +1,598 @@
1
+ """
2
+ lib.py
3
+
4
+ This module provides the Lib class for mass spectrometry compound library
5
+ management and feature annotation.
6
+
7
+ The Lib class supports annotation of sample.features_df and study.consensus_df
8
+ based on MS1 (rt, m/z, possibly isotopes) and MS2 data.
9
+
10
+ Key Features:
11
+ - **Lib Class**: Main class for managing compound libraries and annotations
12
+ - **Compound Libraries**: Load and manage compound databases with metadata
13
+ - **Adduct Calculations**: Handle various ionization adducts and charge states
14
+ - **Mass Calculations**: Precise mass calculations with adduct corrections
15
+ - **Target Matching**: Match detected features against compound libraries
16
+ - **Polarity Handling**: Support for positive and negative ionization modes
17
+ - **CSV Import**: Import compound data from CSV files with automatic adduct generation
18
+
19
+ Dependencies:
20
+ - `pyopenms`: For mass spectrometry algorithms and data structures
21
+ - `polars`: For efficient data manipulation and analysis
22
+ - `numpy`: For numerical computations and array operations
23
+ - `uuid`: For generating unique identifiers
24
+
25
+ Supported Adducts:
26
+ - Positive mode: [M+H]+, [M+Na]+, [M+K]+, [M+NH4]+, [M-H2O+H]+
27
+ - Negative mode: [M-H]-, [M+CH3COO]-, [M+HCOO]-, [M+Cl]-
28
+
29
+ Example Usage:
30
+ ```python
31
+ from masster.lib import Lib
32
+
33
+ # Create library instance
34
+ lib = Lib()
35
+
36
+ # Import compounds from CSV
37
+ lib.import_csv("compounds.csv", polarity="positive")
38
+
39
+ # Access library data
40
+ print(f"Loaded {len(lib.lib_df)} compounds")
41
+ print(lib.lib_df.head())
42
+
43
+ # Annotate sample features
44
+ annotations = lib.annotate_features(sample.features_df)
45
+ ```
46
+ """
47
+
48
+ import os
49
+ import uuid
50
+ from typing import Optional, Union, List, Dict, Any, TYPE_CHECKING
51
+ import warnings
52
+
53
+ import numpy as np
54
+ import polars as pl
55
+ import pyopenms as oms
56
+
57
+ if TYPE_CHECKING:
58
+ import pandas as pd
59
+
60
+
61
+ class Lib:
62
+ """
63
+ A class for managing compound libraries and feature annotation in mass spectrometry data.
64
+
65
+ The Lib class provides functionality to:
66
+ - Load compound libraries from CSV files
67
+ - Generate adduct variants for compounds
68
+ - Annotate MS1 features based on mass and retention time
69
+ - Support both positive and negative ionization modes
70
+ - Manage compound metadata (SMILES, InChI, formulas, etc.)
71
+
72
+ Attributes:
73
+ lib_df (pl.DataFrame): Polars DataFrame containing the library data with columns:
74
+ - lib_uid: Unique identifier for each library entry
75
+ - name: Compound name
76
+ - smiles: SMILES notation
77
+ - inchi: InChI identifier
78
+ - inchikey: InChI key
79
+ - formula: Molecular formula
80
+ - adduct: Adduct type
81
+ - m: Mass with adduct
82
+ - z: Charge state
83
+ - mz: Mass-to-charge ratio
84
+ - rt: Retention time (if available)
85
+
86
+ Example:
87
+ >>> lib = Lib()
88
+ >>> lib.import_csv("compounds.csv", polarity="positive")
89
+ >>> print(f"Loaded {len(lib.lib_df)} library entries")
90
+ """
91
+
92
+ # Define supported adducts and their properties
93
+ ADDUCT_DEFINITIONS = {
94
+ # Positive mode adducts
95
+ "[M+H]1+": {"delta_m": 1.007276, "delta_z": 1, "polarity": "positive"},
96
+ "[M+Na]1+": {"delta_m": 22.989218, "delta_z": 1, "polarity": "positive"},
97
+ "[M+K]1+": {"delta_m": 38.962383, "delta_z": 1, "polarity": "positive"},
98
+ "[M+NH4]1+": {"delta_m": 18.033823, "delta_z": 1, "polarity": "positive"},
99
+ "[M+H-H2O]1+": {"delta_m": -17.00329, "delta_z": 1, "polarity": "positive"},
100
+ "[M+2H]2+": {"delta_m": 2.014552, "delta_z": 2, "polarity": "positive"},
101
+
102
+ # Negative mode adducts
103
+ "[M-H]1-": {"delta_m": -1.007276, "delta_z": -1, "polarity": "negative"},
104
+ "[M+CH3COO]1-": {"delta_m": 59.013852, "delta_z": -1, "polarity": "negative"},
105
+ "[M+HCOO]1-": {"delta_m": 44.998203, "delta_z": -1, "polarity": "negative"},
106
+ "[M+Cl]1-": {"delta_m": 34.968853, "delta_z": -1, "polarity": "negative"},
107
+ "[M-2H]2-": {"delta_m": -2.014552, "delta_z": -2, "polarity": "negative"},
108
+ }
109
+
110
+ def __init__(self):
111
+ """Initialize an empty Lib instance."""
112
+ self.lib_df = None
113
+ self._initialize_empty_dataframe()
114
+
115
+ def _initialize_empty_dataframe(self):
116
+ """Initialize an empty DataFrame with the required schema."""
117
+ self.lib_df = pl.DataFrame({
118
+ "lib_uid": pl.Series([], dtype=pl.Int64),
119
+ "cmpd_uid": pl.Series([], dtype=pl.Int64),
120
+ "source_id": pl.Series([], dtype=pl.Utf8),
121
+ "name": pl.Series([], dtype=pl.Utf8),
122
+ "smiles": pl.Series([], dtype=pl.Utf8),
123
+ "inchi": pl.Series([], dtype=pl.Utf8),
124
+ "inchikey": pl.Series([], dtype=pl.Utf8),
125
+ "formula": pl.Series([], dtype=pl.Utf8),
126
+ "adduct": pl.Series([], dtype=pl.Utf8),
127
+ "m": pl.Series([], dtype=pl.Float64),
128
+ "z": pl.Series([], dtype=pl.Int8),
129
+ "mz": pl.Series([], dtype=pl.Float64),
130
+ "rt": pl.Series([], dtype=pl.Float64),
131
+ "db_id": pl.Series([], dtype=pl.Utf8),
132
+ "db": pl.Series([], dtype=pl.Utf8),
133
+ })
134
+
135
+ def _calculate_accurate_mass(self, formula: str) -> Optional[float]:
136
+ """
137
+ Calculate the accurate mass for a molecular formula using PyOpenMS.
138
+
139
+ Args:
140
+ formula: Molecular formula string
141
+
142
+ Returns:
143
+ Accurate mass as float, or None if calculation fails
144
+ """
145
+ try:
146
+ empirical_formula = oms.EmpiricalFormula(formula)
147
+ return empirical_formula.getMonoWeight()
148
+ except Exception as e:
149
+ warnings.warn(f"Error calculating accurate mass for formula {formula}: {e}")
150
+ return None
151
+
152
+ def _generate_adduct_variants(self,
153
+ compound_data: Dict[str, Any],
154
+ adducts: Optional[List[str]] = None,
155
+ polarity: Optional[str] = None,
156
+ lib_id_counter: Optional[int] = None) -> tuple[List[Dict[str, Any]], int]:
157
+ """
158
+ Generate adduct variants for a given compound.
159
+
160
+ Args:
161
+ compound_data: Dictionary containing compound information
162
+ adducts: List of specific adducts to generate. If None, uses all adducts for polarity
163
+ polarity: Ionization polarity ("positive", "negative", or None for both)
164
+ lib_id_counter: Counter for generating unique lib_uid values
165
+
166
+ Returns:
167
+ Tuple of (list of dictionaries representing adduct variants, updated counter)
168
+ """
169
+ variants = []
170
+ counter = lib_id_counter or 1
171
+
172
+ # Calculate base accurate mass
173
+ accurate_mass = self._calculate_accurate_mass(compound_data["formula"])
174
+ if accurate_mass is None:
175
+ return variants, counter
176
+
177
+ # Determine which adducts to use
178
+ if adducts is None:
179
+ if polarity is None:
180
+ # Use all adducts
181
+ selected_adducts = list(self.ADDUCT_DEFINITIONS.keys())
182
+ else:
183
+ # Filter by polarity
184
+ selected_adducts = [
185
+ adduct for adduct, props in self.ADDUCT_DEFINITIONS.items()
186
+ if props["polarity"] == polarity.lower()
187
+ ]
188
+ else:
189
+ selected_adducts = adducts
190
+
191
+ # Generate variants for each adduct
192
+ for adduct in selected_adducts:
193
+ if adduct not in self.ADDUCT_DEFINITIONS:
194
+ warnings.warn(f"Unknown adduct: {adduct}")
195
+ continue
196
+
197
+ adduct_props = self.ADDUCT_DEFINITIONS[adduct]
198
+
199
+ # Skip if polarity doesn't match
200
+ if polarity is not None and adduct_props["polarity"] != polarity.lower():
201
+ continue
202
+
203
+ # Calculate adducted mass and m/z
204
+ adducted_mass = accurate_mass + adduct_props["delta_m"]
205
+ charge = adduct_props["delta_z"]
206
+ mz = abs(adducted_mass / charge) if charge != 0 else adducted_mass
207
+
208
+ # Create variant entry
209
+ variant = {
210
+ "lib_uid": counter,
211
+ "cmpd_uid": compound_data.get("cmpd_uid", None),
212
+ "source_id": compound_data.get("source_id", None),
213
+ "name": compound_data.get("name", ""),
214
+ "smiles": compound_data.get("smiles", ""),
215
+ "inchi": compound_data.get("inchi", ""),
216
+ "inchikey": compound_data.get("inchikey", ""),
217
+ "formula": compound_data["formula"],
218
+ "adduct": adduct,
219
+ "m": adducted_mass,
220
+ "z": charge,
221
+ "mz": mz,
222
+ "rt": compound_data.get("rt", None),
223
+ "db_id": compound_data.get("db_id", None),
224
+ "db": compound_data.get("db", None),
225
+ }
226
+ variants.append(variant)
227
+ counter += 1
228
+
229
+ return variants, counter
230
+
231
+ def import_csv(self,
232
+ csvfile: str,
233
+ polarity: Optional[str] = None,
234
+ adducts: Optional[List[str]] = None) -> None:
235
+ """
236
+ Import compound library from a CSV file.
237
+
238
+ This method reads a CSV file and generates adduct variants for each compound.
239
+ Missing columns will be filled with appropriate default values.
240
+
241
+ Args:
242
+ csvfile: Path to the CSV file
243
+ polarity: Ionization polarity ("positive", "negative", or None for both)
244
+ adducts: Specific adducts to generate. If None, generates all for the polarity
245
+
246
+ Expected CSV columns (case-insensitive):
247
+ - Required: Formula (or formula)
248
+ - Optional: Name/name/Compound/compound, SMILES/smiles, InChI/inchi,
249
+ InChIKey/inchikey, RT/rt, RT2/rt2
250
+
251
+ Raises:
252
+ FileNotFoundError: If CSV file doesn't exist
253
+ ValueError: If required columns are missing
254
+ """
255
+ if not os.path.exists(csvfile):
256
+ raise FileNotFoundError(f"CSV file not found: {csvfile}")
257
+
258
+ # Read CSV file with robust error handling
259
+ try:
260
+ df = pl.read_csv(csvfile, truncate_ragged_lines=True, ignore_errors=True)
261
+ except Exception as e:
262
+ raise ValueError(f"Error reading CSV file: {e}") from e
263
+
264
+ # Find column mappings (case-insensitive)
265
+ column_mapping = self._map_csv_columns(df.columns)
266
+
267
+ # Validate required columns
268
+ if "formula" not in column_mapping:
269
+ raise ValueError("Required column 'Formula' (or 'formula') not found in CSV file")
270
+
271
+ # Process each compound
272
+ all_variants = []
273
+ cmpd_id_counter = 1
274
+ lib_id_counter = 1
275
+
276
+ for row in df.iter_rows(named=True):
277
+ # Extract compound data
278
+ # assign a compound-level uid so all adducts share the same cmpd_uid
279
+ compound_level_uid = cmpd_id_counter
280
+ cmpd_id_counter += 1
281
+
282
+ compound_data = {
283
+ "name": row.get(column_mapping.get("name", ""), ""),
284
+ "smiles": row.get(column_mapping.get("smiles", ""), ""),
285
+ "inchi": row.get(column_mapping.get("inchi", ""), ""),
286
+ "inchikey": row.get(column_mapping.get("inchikey", ""), ""),
287
+ "formula": row[column_mapping["formula"]],
288
+ "rt": self._safe_float_conversion(row.get(column_mapping.get("rt", ""), None)),
289
+ "db_id": row.get(column_mapping.get("db_id", ""), None),
290
+ "db": row.get(column_mapping.get("db", ""), None),
291
+ "cmpd_uid": compound_level_uid,
292
+ }
293
+
294
+ # Generate adduct variants
295
+ variants, lib_id_counter = self._generate_adduct_variants(
296
+ compound_data, adducts=adducts, polarity=polarity, lib_id_counter=lib_id_counter
297
+ )
298
+ all_variants.extend(variants)
299
+
300
+ # Handle RT2 column if present
301
+ if "rt2" in column_mapping:
302
+ rt2_value = self._safe_float_conversion(row.get(column_mapping["rt2"], None))
303
+ if rt2_value is not None:
304
+ # Create additional variants with RT2
305
+ compound_data_rt2 = compound_data.copy()
306
+ compound_data_rt2["rt"] = rt2_value
307
+ compound_data_rt2["name"] = compound_data["name"] + " II"
308
+
309
+ variants_rt2, lib_id_counter = self._generate_adduct_variants(
310
+ compound_data_rt2, adducts=adducts, polarity=polarity, lib_id_counter=lib_id_counter
311
+ )
312
+ all_variants.extend(variants_rt2)
313
+
314
+ # Convert to DataFrame and store
315
+ if all_variants:
316
+ new_lib_df = pl.DataFrame(all_variants)
317
+
318
+ # Combine with existing data if any
319
+ if self.lib_df is not None and len(self.lib_df) > 0:
320
+ self.lib_df = pl.concat([self.lib_df, new_lib_df])
321
+ else:
322
+ self.lib_df = new_lib_df
323
+
324
+ print(f"Successfully imported {len(all_variants)} library entries from {csvfile}")
325
+ else:
326
+ print(f"No valid compounds found in {csvfile}")
327
+
328
+ def _map_csv_columns(self, columns: List[str]) -> Dict[str, str]:
329
+ """
330
+ Map CSV column names to standardized internal names (case-insensitive).
331
+
332
+ Args:
333
+ columns: List of column names from CSV
334
+
335
+ Returns:
336
+ Dictionary mapping internal names to actual column names
337
+ """
338
+ mapping = {}
339
+ columns_lower = [col.lower() for col in columns]
340
+
341
+ # Name mapping
342
+ for name_variant in ["name", "compound"]:
343
+ if name_variant in columns_lower:
344
+ mapping["name"] = columns[columns_lower.index(name_variant)]
345
+ break
346
+
347
+ # Formula mapping
348
+ for formula_variant in ["formula"]:
349
+ if formula_variant in columns_lower:
350
+ mapping["formula"] = columns[columns_lower.index(formula_variant)]
351
+ break
352
+
353
+ # SMILES mapping
354
+ for smiles_variant in ["smiles"]:
355
+ if smiles_variant in columns_lower:
356
+ mapping["smiles"] = columns[columns_lower.index(smiles_variant)]
357
+ break
358
+
359
+ # InChI mapping
360
+ for inchi_variant in ["inchi"]:
361
+ if inchi_variant in columns_lower:
362
+ mapping["inchi"] = columns[columns_lower.index(inchi_variant)]
363
+ break
364
+
365
+ # InChIKey mapping
366
+ for inchikey_variant in ["inchikey", "inchi_key"]:
367
+ if inchikey_variant in columns_lower:
368
+ mapping["inchikey"] = columns[columns_lower.index(inchikey_variant)]
369
+ break
370
+
371
+ # RT mapping
372
+ for rt_variant in ["rt", "retention_time", "retentiontime"]:
373
+ if rt_variant in columns_lower:
374
+ mapping["rt"] = columns[columns_lower.index(rt_variant)]
375
+ break
376
+
377
+ # RT2 mapping
378
+ for rt2_variant in ["rt2", "retention_time2", "retentiontime2"]:
379
+ if rt2_variant in columns_lower:
380
+ mapping["rt2"] = columns[columns_lower.index(rt2_variant)]
381
+ break
382
+
383
+ # Database ID mapping
384
+ for db_id_variant in ["db_id", "database_id", "dbid"]:
385
+ if db_id_variant in columns_lower:
386
+ mapping["db_id"] = columns[columns_lower.index(db_id_variant)]
387
+ break
388
+
389
+ # Database mapping
390
+ for db_variant in ["db", "database"]:
391
+ if db_variant in columns_lower:
392
+ mapping["db"] = columns[columns_lower.index(db_variant)]
393
+ break
394
+
395
+ return mapping
396
+
397
+ def _safe_float_conversion(self, value: Any) -> Optional[float]:
398
+ """
399
+ Safely convert a value to float, returning None if conversion fails.
400
+
401
+ Args:
402
+ value: Value to convert
403
+
404
+ Returns:
405
+ Float value or None
406
+ """
407
+ if value is None or value == "":
408
+ return None
409
+ try:
410
+ return float(value)
411
+ except (ValueError, TypeError):
412
+ return None
413
+
414
+ def annotate_features(self,
415
+ features_df: Union[pl.DataFrame, "pd.DataFrame"],
416
+ mz_tolerance: float = 0.01,
417
+ rt_tolerance: Optional[float] = None) -> pl.DataFrame:
418
+ """
419
+ Annotate features based on library matches using m/z and retention time.
420
+
421
+ Args:
422
+ features_df: DataFrame containing features with 'mz' and optionally 'rt' columns
423
+ mz_tolerance: Mass tolerance in Da for matching
424
+ rt_tolerance: Retention time tolerance in minutes for matching (if None, RT not used)
425
+
426
+ Returns:
427
+ DataFrame with annotation results
428
+ """
429
+ if self.lib_df is None or len(self.lib_df) == 0:
430
+ raise ValueError("Library is empty. Import compounds first.")
431
+
432
+ # Convert pandas DataFrame to Polars if needed
433
+ if hasattr(features_df, 'to_pandas'): # It's already a Polars DataFrame
434
+ features_pl = features_df
435
+ elif hasattr(features_df, 'values'): # It's likely a pandas DataFrame
436
+ try:
437
+ import pandas as pd
438
+ if isinstance(features_df, pd.DataFrame):
439
+ features_pl = pl.from_pandas(features_df)
440
+ else:
441
+ features_pl = features_df
442
+ except ImportError:
443
+ features_pl = features_df
444
+ else:
445
+ features_pl = features_df
446
+
447
+ annotations = []
448
+
449
+ for feature_row in features_pl.iter_rows(named=True):
450
+ feature_mz = feature_row.get("mz")
451
+ feature_rt = feature_row.get("rt")
452
+
453
+ if feature_mz is None:
454
+ continue
455
+
456
+ # Find matching library entries
457
+ mz_matches = self.lib_df.filter(
458
+ (pl.col("mz") >= feature_mz - mz_tolerance) &
459
+ (pl.col("mz") <= feature_mz + mz_tolerance)
460
+ )
461
+
462
+ # Apply RT filter if both RT tolerance and feature RT are available
463
+ if rt_tolerance is not None and feature_rt is not None:
464
+ # Filter library entries that have RT values
465
+ rt_matches = mz_matches.filter(
466
+ pl.col("rt").is_not_null() &
467
+ (pl.col("rt") >= feature_rt - rt_tolerance) &
468
+ (pl.col("rt") <= feature_rt + rt_tolerance)
469
+ )
470
+ if len(rt_matches) > 0:
471
+ matches = rt_matches
472
+ else:
473
+ matches = mz_matches # Fall back to m/z-only matches
474
+ else:
475
+ matches = mz_matches
476
+
477
+ # Create annotation entries
478
+ for match_row in matches.iter_rows(named=True):
479
+ annotation = {
480
+ "feature_mz": feature_mz,
481
+ "feature_rt": feature_rt,
482
+ "lib_uid": match_row["lib_uid"],
483
+ "cmpd_uid": match_row.get("cmpd_uid"),
484
+ "source_id": match_row.get("source_id"),
485
+ "name": match_row["name"],
486
+ "formula": match_row["formula"],
487
+ "adduct": match_row["adduct"],
488
+ "smiles": match_row["smiles"],
489
+ "inchi": match_row["inchi"],
490
+ "inchikey": match_row["inchikey"],
491
+ "lib_mz": match_row["mz"],
492
+ "lib_rt": match_row["rt"],
493
+ "delta_mz": abs(feature_mz - match_row["mz"]),
494
+ "delta_rt": abs(feature_rt - match_row["rt"]) if feature_rt is not None and match_row["rt"] is not None else None,
495
+ }
496
+ annotations.append(annotation)
497
+
498
+ return pl.DataFrame(annotations) if annotations else pl.DataFrame()
499
+
500
+ def get_adducts_for_polarity(self, polarity: str) -> List[str]:
501
+ """
502
+ Get list of supported adducts for a given polarity.
503
+
504
+ Args:
505
+ polarity: "positive" or "negative"
506
+
507
+ Returns:
508
+ List of adduct names
509
+ """
510
+ return [
511
+ adduct for adduct, props in self.ADDUCT_DEFINITIONS.items()
512
+ if props["polarity"] == polarity.lower()
513
+ ]
514
+
515
+ def __len__(self) -> int:
516
+ """Return number of library entries."""
517
+ return len(self.lib_df) if self.lib_df is not None else 0
518
+
519
+ def _reload(self):
520
+ """
521
+ Reloads all masster modules to pick up any changes to their source code,
522
+ and updates the instance's class reference to the newly reloaded class version.
523
+ This ensures that the instance uses the latest implementation without restarting the interpreter.
524
+ """
525
+ import importlib
526
+ import sys
527
+
528
+ # Get the base module name (masster)
529
+ base_modname = self.__class__.__module__.split(".")[0]
530
+ current_module = self.__class__.__module__
531
+
532
+ # Dynamically find all lib submodules
533
+ lib_modules = []
534
+ lib_module_prefix = f"{base_modname}.lib."
535
+
536
+ # Get all currently loaded modules that are part of the lib package
537
+ for module_name in sys.modules:
538
+ if module_name.startswith(lib_module_prefix) and module_name != current_module:
539
+ lib_modules.append(module_name)
540
+
541
+ # Add core masster modules
542
+ core_modules = [
543
+ f"{base_modname}._version",
544
+ f"{base_modname}.chromatogram",
545
+ f"{base_modname}.spectrum",
546
+ f"{base_modname}.logger",
547
+ ]
548
+
549
+ '''# Add study submodules (for cross-dependencies)
550
+ study_modules = []
551
+ study_module_prefix = f"{base_modname}.study."
552
+ for module_name in sys.modules:
553
+ if module_name.startswith(study_module_prefix) and module_name != current_module:
554
+ study_modules.append(module_name)'''
555
+
556
+ '''# Add sample submodules (for cross-dependencies)
557
+ sample_modules = []
558
+ sample_module_prefix = f"{base_modname}.sample."
559
+ for module_name in sys.modules:
560
+ if module_name.startswith(sample_module_prefix) and module_name != current_module:
561
+ sample_modules.append(module_name)'''
562
+
563
+ all_modules_to_reload = core_modules + lib_modules # sample_modules + study_modules +
564
+
565
+ # Reload all discovered modules
566
+ for full_module_name in all_modules_to_reload:
567
+ try:
568
+ if full_module_name in sys.modules:
569
+ mod = sys.modules[full_module_name]
570
+ importlib.reload(mod)
571
+ # Note: Lib class doesn't have a logger by default, so we just print or use warnings
572
+ #print(f"Reloaded module: {full_module_name}")
573
+ except Exception as e:
574
+ print(f"Warning: Failed to reload module {full_module_name}: {e}")
575
+
576
+ # Finally, reload the current module (lib.py)
577
+ try:
578
+ mod = __import__(current_module, fromlist=[current_module.split(".")[0]])
579
+ importlib.reload(mod)
580
+
581
+ # Get the updated class reference from the reloaded module
582
+ new = getattr(mod, self.__class__.__name__)
583
+ # Update the class reference of the instance
584
+ self.__class__ = new
585
+
586
+ print("Lib module reload completed")
587
+ except Exception as e:
588
+ print(f"Error: Failed to reload current module {current_module}: {e}")
589
+
590
+ def __str__(self) -> str:
591
+ """String representation of the library."""
592
+ if self.lib_df is None or len(self.lib_df) == 0:
593
+ return "Empty Lib instance"
594
+
595
+ unique_compounds = self.lib_df.select("name").unique().height
596
+ unique_adducts = self.lib_df.select("adduct").unique().height
597
+
598
+ return f"Lib instance with {len(self)} entries ({unique_compounds} unique compounds, {unique_adducts} adduct types)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: masster
3
- Version: 0.4.9
3
+ Version: 0.4.10
4
4
  Summary: Mass spectrometry data analysis package
5
5
  Project-URL: homepage, https://github.com/zamboni-lab/masster
6
6
  Project-URL: repository, https://github.com/zamboni-lab/masster
@@ -1,5 +1,5 @@
1
1
  masster/__init__.py,sha256=8U4cIteNlYyHDrxWSbB_MsDKCX9tds07SJG8-vh8Oa8,738
2
- masster/_version.py,sha256=0yrEn9iWdV_opaqa2Sq9St0gGLqdVLUjTS0SKr3szC0,256
2
+ masster/_version.py,sha256=WP_nHA1hNbPXkmS3tyAKOINq6RpCiQCXwQl13e8Jsb4,257
3
3
  masster/chromatogram.py,sha256=iYpdv8C17zVnlWvOFgAn9ns2uFGiF-GgoYf5QVVAbHs,19319
4
4
  masster/logger.py,sha256=W50V_uh8RSYwGxDrDFhOuj5jpu2tKJyt_16lMw9kQwA,14755
5
5
  masster/spectrum.py,sha256=_upC_g2N9gwTaflXAugs9pSXpKUmzbIehofDordk7WI,47718
@@ -14,6 +14,8 @@ masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecR
14
14
  masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff,sha256=go5N9gAM1rn4PZAVaoCmdteY9f7YGEM9gyPdSmkQ8PE,1447936
15
15
  masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff.scan,sha256=ahi1Y3UhAj9Bj4Q2MlbgPekNdkJvMOoMXVOoR6CeIxc,13881220
16
16
  masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff2,sha256=TFB0HW4Agkig6yht7FtgjUdbXax8jjKaHpSZSvuU5vs,3252224
17
+ masster/lib/__init__.py,sha256=TcePNx3SYZHz6763TL9Sg4gUNXaRWjlrOtyS6vsu-hg,178
18
+ masster/lib/lib.py,sha256=seAOqzat74iF6YnbNakU3rF5MN5t9WABIpcIPTvU1q8,24987
17
19
  masster/sample/__init__.py,sha256=HL0m1ept0PMAYUCQtDDnkdOS12IFl6oLAq4TZQz83uY,170
18
20
  masster/sample/adducts.py,sha256=jdtkkiMFeObRv3myluUx--IfpRsEq3-hPgkCb2VUxy4,32590
19
21
  masster/sample/h5.py,sha256=7NJeErlIHwC2Qh3nchusLZJWjBGue9zExAx08C89qhg,111889
@@ -58,8 +60,8 @@ masster/study/defaults/integrate_chrom_def.py,sha256=0MNIWGTjty-Zu-NTQsIweuj3UVq
58
60
  masster/study/defaults/integrate_def.py,sha256=Vf4SAzdBfnsSZ3IRaF0qZvWu3gMDPHdgPfMYoPKeWv8,7246
59
61
  masster/study/defaults/merge_def.py,sha256=EBsKE3hsAkTEzN9dpdRD5W3_suTKy_WZ_96rwS0uBuE,8572
60
62
  masster/study/defaults/study_def.py,sha256=h8dYbi9xv0sesCSQik49Z53IkskMmNtW6ixl7it5pL0,16033
61
- masster-0.4.9.dist-info/METADATA,sha256=ulvm1pAw0IpOdNsTvgm00orsh9G4Y2V5tRUhEmLxKes,44188
62
- masster-0.4.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
63
- masster-0.4.9.dist-info/entry_points.txt,sha256=ZHguQ_vPmdbpqq2uGtmEOLJfgP-DQ1T0c07Lxh30wc8,58
64
- masster-0.4.9.dist-info/licenses/LICENSE,sha256=bx5iLIKjgAdYQ7sISn7DsfHRKkoCUm1154sJJKhgqnU,35184
65
- masster-0.4.9.dist-info/RECORD,,
63
+ masster-0.4.10.dist-info/METADATA,sha256=xYmHFy0HHE1Ko8NTY_hM82xDuUK8OfsuJB8CkRQiHN4,44189
64
+ masster-0.4.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
65
+ masster-0.4.10.dist-info/entry_points.txt,sha256=ZHguQ_vPmdbpqq2uGtmEOLJfgP-DQ1T0c07Lxh30wc8,58
66
+ masster-0.4.10.dist-info/licenses/LICENSE,sha256=bx5iLIKjgAdYQ7sISn7DsfHRKkoCUm1154sJJKhgqnU,35184
67
+ masster-0.4.10.dist-info/RECORD,,