LibViz 0.1.0__tar.gz
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.
- libviz-0.1.0/PKG-INFO +23 -0
- libviz-0.1.0/README.md +1 -0
- libviz-0.1.0/pyproject.toml +56 -0
- libviz-0.1.0/src/libviz/__init__.py +0 -0
- libviz-0.1.0/src/libviz/bridge/__init__.py +1 -0
- libviz-0.1.0/src/libviz/bridge/library_manager_bridge.py +149 -0
- libviz-0.1.0/src/libviz/core/__init__.py +1 -0
- libviz-0.1.0/src/libviz/core/db/__init__.py +1 -0
- libviz-0.1.0/src/libviz/core/db/extensions.py +3 -0
- libviz-0.1.0/src/libviz/core/db/migrations.py +27 -0
- libviz-0.1.0/src/libviz/core/db/models/__init__.py +5 -0
- libviz-0.1.0/src/libviz/core/db/models/adducts_type.py +56 -0
- libviz-0.1.0/src/libviz/core/db/models/compound.py +25 -0
- libviz-0.1.0/src/libviz/core/db/models/library_metadata.py +21 -0
- libviz-0.1.0/src/libviz/core/db/models/spectrum.py +89 -0
- libviz-0.1.0/src/libviz/core/db/models/spectrum_metadata.py +14 -0
- libviz-0.1.0/src/libviz/core/library.py +398 -0
- libviz-0.1.0/src/libviz/core/managers/__init__.py +1 -0
- libviz-0.1.0/src/libviz/core/managers/library_manager.py +93 -0
- libviz-0.1.0/src/libviz/core/readers/__init__.py +1 -0
- libviz-0.1.0/src/libviz/core/readers/msp_reader.py +211 -0
- libviz-0.1.0/src/libviz/core/utils/__init__.py +3 -0
- libviz-0.1.0/src/libviz/core/utils/library_io_utils.py +52 -0
- libviz-0.1.0/src/libviz/core/utils/logging_utils.py +25 -0
- libviz-0.1.0/src/libviz/core/utils/mass_calulation_utils.py +58 -0
- libviz-0.1.0/src/libviz/gui/Libviz/Pages/LibraryHomePage.qml +88 -0
- libviz-0.1.0/src/libviz/gui/Libviz/Pages/WelcomePage.qml +27 -0
- libviz-0.1.0/src/libviz/gui/Libviz/Pages/qmldir +4 -0
- libviz-0.1.0/src/libviz/gui/__init__.py +0 -0
- libviz-0.1.0/src/libviz/gui/libviz_resources.py +369 -0
- libviz-0.1.0/src/libviz/gui/main.qml +150 -0
- libviz-0.1.0/src/libviz/gui/resources.qrc +8 -0
- libviz-0.1.0/src/libviz/main.py +44 -0
libviz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: LibViz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: App to visualize MS library spectra
|
|
5
|
+
Author-Email: Matteo Miotto <miotsdata@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: ==3.13
|
|
8
|
+
Requires-Dist: sqlalchemy>=2.0.46
|
|
9
|
+
Requires-Dist: alembic>=1.18.4
|
|
10
|
+
Requires-Dist: pandas>=3.0.0
|
|
11
|
+
Requires-Dist: numpy>=2.4.2
|
|
12
|
+
Requires-Dist: pyteomics>=4.7.5
|
|
13
|
+
Requires-Dist: tqdm>=4.67.3
|
|
14
|
+
Requires-Dist: lxml>=6.0.2
|
|
15
|
+
Provides-Extra: gui
|
|
16
|
+
Requires-Dist: pyside6>=6.10.2; extra == "gui"
|
|
17
|
+
Requires-Dist: pyqt6>=6.10.2; extra == "gui"
|
|
18
|
+
Requires-Dist: PySide6-Essentials>=6.10.2; extra == "gui"
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: libviz[gui]; extra == "all"
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# LibViz
|
libviz-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# LibViz
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "LibViz"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "App to visualize MS library spectra"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Matteo Miotto", email = "miotsdata@gmail.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"sqlalchemy>=2.0.46",
|
|
10
|
+
"alembic>=1.18.4",
|
|
11
|
+
"pandas>=3.0.0",
|
|
12
|
+
"numpy>=2.4.2",
|
|
13
|
+
"pyteomics>=4.7.5",
|
|
14
|
+
"tqdm>=4.67.3",
|
|
15
|
+
"lxml>=6.0.2",
|
|
16
|
+
]
|
|
17
|
+
requires-python = "==3.13"
|
|
18
|
+
readme = "README.md"
|
|
19
|
+
|
|
20
|
+
[project.license]
|
|
21
|
+
text = "MIT"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
gui = [
|
|
25
|
+
"pyside6>=6.10.2",
|
|
26
|
+
"pyqt6>=6.10.2",
|
|
27
|
+
"PySide6-Essentials>=6.10.2",
|
|
28
|
+
]
|
|
29
|
+
all = [
|
|
30
|
+
"libviz[gui]",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = [
|
|
35
|
+
"pdm-backend",
|
|
36
|
+
]
|
|
37
|
+
build-backend = "pdm.backend"
|
|
38
|
+
|
|
39
|
+
[tool.pdm]
|
|
40
|
+
distribution = true
|
|
41
|
+
|
|
42
|
+
[tool.pdm.build]
|
|
43
|
+
excludes = [
|
|
44
|
+
"src/libviz/old",
|
|
45
|
+
"tests",
|
|
46
|
+
"**/.DS_Store",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=9.0.2",
|
|
52
|
+
"pylint>=4.0.4",
|
|
53
|
+
"pyside6-stubs>=6.7.3.0",
|
|
54
|
+
"hupper>=1.12.1",
|
|
55
|
+
"pyinstaller>=6.19.0",
|
|
56
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .library_manager_bridge import LibraryManagerBridge
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# libviz/bridge/library_manager_bridge.py
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QObject, Signal, Slot, Property, QThread, QUrl # pylint: disable=no-name-in-module
|
|
4
|
+
from libviz.core import LibraryManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _MspLoaderWorker(QObject):
|
|
8
|
+
"""Runs in a separate thread — no Qt UI calls allowed here."""
|
|
9
|
+
finished = Signal(str) # emits path on success
|
|
10
|
+
failed = Signal(str) # emits error message on failure
|
|
11
|
+
|
|
12
|
+
def __init__(self, manager: LibraryManager, msp_path: str):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self._manager = manager
|
|
15
|
+
self._msp_path = msp_path
|
|
16
|
+
|
|
17
|
+
@Slot()
|
|
18
|
+
def run(self):
|
|
19
|
+
try:
|
|
20
|
+
self._manager.load_library_from_msp(self._msp_path)
|
|
21
|
+
self.finished.emit(self._msp_path)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
self.failed.emit(str(e))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LibraryManagerBridge(QObject):
|
|
27
|
+
|
|
28
|
+
# --- Signals ---
|
|
29
|
+
libraryLoaded = Signal(str)
|
|
30
|
+
libraryUnloaded = Signal(str)
|
|
31
|
+
selectedLibraryChanged = Signal()
|
|
32
|
+
libraryCountChanged = Signal()
|
|
33
|
+
errorOccurred = Signal(str)
|
|
34
|
+
statusChanged = Signal(str)
|
|
35
|
+
|
|
36
|
+
def __init__(self, manager: LibraryManager, parent=None):
|
|
37
|
+
super().__init__(parent)
|
|
38
|
+
self._manager = manager
|
|
39
|
+
self._thread: QThread | None = None
|
|
40
|
+
self._worker: _MspLoaderWorker | None = None
|
|
41
|
+
|
|
42
|
+
# --- Internal helpers ---
|
|
43
|
+
|
|
44
|
+
def _set_status(self, message: str):
|
|
45
|
+
self.statusChanged.emit(message)
|
|
46
|
+
|
|
47
|
+
def _clear_status(self):
|
|
48
|
+
self.statusChanged.emit("")
|
|
49
|
+
|
|
50
|
+
# --- Slots ---
|
|
51
|
+
|
|
52
|
+
@Slot(str, str)
|
|
53
|
+
def createLibrary(self, name: str, path: str):
|
|
54
|
+
try:
|
|
55
|
+
self._set_status(f"Creating: {path}")
|
|
56
|
+
self._manager.create_library(name, path)
|
|
57
|
+
self.libraryCountChanged.emit()
|
|
58
|
+
self.libraryLoaded.emit(path)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self.errorOccurred.emit(str(e))
|
|
61
|
+
finally:
|
|
62
|
+
self._clear_status()
|
|
63
|
+
|
|
64
|
+
@Slot(str)
|
|
65
|
+
def loadLibraryFromMsp(self, msp_path: str):
|
|
66
|
+
"""Offloads the heavy parse to a QThread; status bar shown until done."""
|
|
67
|
+
if self._thread and self._thread.isRunning():
|
|
68
|
+
self.errorOccurred.emit("An MSP load is already in progress.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
msp_path = QUrl(msp_path).toLocalFile()
|
|
72
|
+
|
|
73
|
+
self._set_status(f"Loading: {msp_path}")
|
|
74
|
+
|
|
75
|
+
self._worker = _MspLoaderWorker(self._manager, msp_path)
|
|
76
|
+
self._thread = QThread(self)
|
|
77
|
+
self._worker.moveToThread(self._thread)
|
|
78
|
+
|
|
79
|
+
# Wire up lifecycle
|
|
80
|
+
self._thread.started.connect(self._worker.run)
|
|
81
|
+
self._worker.finished.connect(self._on_msp_loaded)
|
|
82
|
+
self._worker.failed.connect(self._on_msp_failed)
|
|
83
|
+
|
|
84
|
+
# Clean up thread and worker after work is done
|
|
85
|
+
self._worker.finished.connect(self._thread.quit)
|
|
86
|
+
self._worker.failed.connect(self._thread.quit)
|
|
87
|
+
self._thread.finished.connect(self._worker.deleteLater)
|
|
88
|
+
self._thread.finished.connect(self._thread.deleteLater)
|
|
89
|
+
|
|
90
|
+
self._thread.start()
|
|
91
|
+
|
|
92
|
+
def _on_msp_loaded(self, path: str):
|
|
93
|
+
self._clear_status()
|
|
94
|
+
self.libraryCountChanged.emit()
|
|
95
|
+
self.libraryLoaded.emit(path)
|
|
96
|
+
|
|
97
|
+
def _on_msp_failed(self, message: str):
|
|
98
|
+
self._clear_status()
|
|
99
|
+
self.errorOccurred.emit(message)
|
|
100
|
+
|
|
101
|
+
@Slot(str)
|
|
102
|
+
def loadLibraryFromDb(self, path: str):
|
|
103
|
+
try:
|
|
104
|
+
self._set_status(f"Loading: {path}")
|
|
105
|
+
self._manager.load_library_from_db(path)
|
|
106
|
+
self.libraryCountChanged.emit()
|
|
107
|
+
self.libraryLoaded.emit(path)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
self.errorOccurred.emit(str(e))
|
|
110
|
+
finally:
|
|
111
|
+
self._clear_status()
|
|
112
|
+
|
|
113
|
+
@Slot(str)
|
|
114
|
+
def saveAsDb(self, path: str):
|
|
115
|
+
try:
|
|
116
|
+
self._set_status(f"Saving: {path}")
|
|
117
|
+
self._manager.save_as_db(path)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
self.errorOccurred.emit(str(e))
|
|
120
|
+
finally:
|
|
121
|
+
self._clear_status()
|
|
122
|
+
|
|
123
|
+
@Slot(str)
|
|
124
|
+
def unloadLibrary(self, path: str):
|
|
125
|
+
try:
|
|
126
|
+
self._manager.unload_library(path)
|
|
127
|
+
self.libraryCountChanged.emit()
|
|
128
|
+
self.libraryUnloaded.emit(path)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.errorOccurred.emit(str(e))
|
|
131
|
+
|
|
132
|
+
@Slot(str)
|
|
133
|
+
def selectLibrary(self, path: str):
|
|
134
|
+
self._manager._selected_library = path
|
|
135
|
+
self.selectedLibraryChanged.emit()
|
|
136
|
+
|
|
137
|
+
# --- Properties ---
|
|
138
|
+
|
|
139
|
+
@Property(bool, notify=libraryCountChanged)
|
|
140
|
+
def hasLibraries(self) -> bool:
|
|
141
|
+
return self._manager.has_libraries
|
|
142
|
+
|
|
143
|
+
@Property(int, notify=libraryCountChanged)
|
|
144
|
+
def libraryCount(self) -> int:
|
|
145
|
+
return self._manager.library_count
|
|
146
|
+
|
|
147
|
+
@Property(str, notify=selectedLibraryChanged)
|
|
148
|
+
def selectedLibraryKey(self) -> str:
|
|
149
|
+
return self._manager.selected_library_key or ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .managers import LibraryManager
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .extensions import Base
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from alembic.config import Config
|
|
4
|
+
from alembic import command
|
|
5
|
+
|
|
6
|
+
def get_bundle_dir():
|
|
7
|
+
"""Get the path to the app directory, even if frozen by PyInstaller."""
|
|
8
|
+
if getattr(sys, 'frozen', False):
|
|
9
|
+
# Running in a bundle (.exe / .app)
|
|
10
|
+
return Path(sys._MEIPASS)
|
|
11
|
+
# Running in normal Python environment
|
|
12
|
+
return Path(__file__).parent.parent.parent.parent.parent
|
|
13
|
+
|
|
14
|
+
def upgrade_library_schema(db_path: Path):
|
|
15
|
+
bundle_dir = get_bundle_dir()
|
|
16
|
+
ini_path = bundle_dir / "alembic.ini"
|
|
17
|
+
|
|
18
|
+
alembic_cfg = Config(str(ini_path))
|
|
19
|
+
|
|
20
|
+
# CRITICAL: Tell Alembic where the versions folder is inside the bundle
|
|
21
|
+
# This overrides the 'script_location' in alembic.ini
|
|
22
|
+
script_location = bundle_dir / "migrations"
|
|
23
|
+
alembic_cfg.set_main_option("script_location", str(script_location))
|
|
24
|
+
|
|
25
|
+
alembic_cfg.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}")
|
|
26
|
+
|
|
27
|
+
command.upgrade(alembic_cfg, "head")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, Float
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from libviz.core.db import Base
|
|
5
|
+
|
|
6
|
+
PROTON = 1.007276466812
|
|
7
|
+
ELECTRON = 0.000548579909065 # optional, often ignored
|
|
8
|
+
|
|
9
|
+
COMMON_ADDUCTS = [
|
|
10
|
+
# Positive mode
|
|
11
|
+
{"formula": "[M+H]+", "charge": 1, "delta": PROTON, "multiplication_factor": 1},
|
|
12
|
+
{"formula": "[M+Na]+", "charge": 1, "delta": 22.989218, "multiplication_factor": 1},
|
|
13
|
+
{"formula": "[M+K]+", "charge": 1, "delta": 38.963158, "multiplication_factor": 1},
|
|
14
|
+
{"formula": "[M+NH4]+", "charge": 1, "delta": 18.033823, "multiplication_factor": 1},
|
|
15
|
+
|
|
16
|
+
{"formula": "[M+2H]2+", "charge": 2, "delta": 2 * PROTON, "multiplication_factor": 1},
|
|
17
|
+
|
|
18
|
+
# Negative mode
|
|
19
|
+
{"formula": "[M-H]-", "charge": -1, "delta": -PROTON, "multiplication_factor": 1},
|
|
20
|
+
{"formula": "[M+Cl]-", "charge": -1, "delta": 34.969402, "multiplication_factor": 1},
|
|
21
|
+
{"formula": "[M+FA-H]-", "charge": -1, "delta": 44.998201, "multiplication_factor": 1},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class AdductType(Base):
|
|
25
|
+
__tablename__ = "adduct_type"
|
|
26
|
+
|
|
27
|
+
id = Column(Integer, primary_key=True)
|
|
28
|
+
|
|
29
|
+
formula = Column(String, unique=True)
|
|
30
|
+
charge = Column(Integer, nullable=False, index=True)
|
|
31
|
+
delta_mass = Column(Float, index=True)
|
|
32
|
+
multiplication_factor = Column(Integer, default=1, nullable=False, index=True)
|
|
33
|
+
|
|
34
|
+
spectra = relationship("Spectrum", back_populates="adduct_type")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def populate_adduct_types(session):
|
|
38
|
+
|
|
39
|
+
for p in COMMON_ADDUCTS:
|
|
40
|
+
exists = session.query(AdductType)\
|
|
41
|
+
.filter_by(formula=p["formula"])\
|
|
42
|
+
.one_or_none()
|
|
43
|
+
|
|
44
|
+
if exists:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
session.add(
|
|
48
|
+
AdductType(
|
|
49
|
+
formula=p["formula"],
|
|
50
|
+
charge=p["charge"],
|
|
51
|
+
delta_mass=p["delta"],
|
|
52
|
+
multiplication_factor=p["multiplication_factor"]
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
session.commit()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, Float, DateTime
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from libviz.core.db import Base
|
|
6
|
+
|
|
7
|
+
class Compound(Base):
|
|
8
|
+
__tablename__ = "compound"
|
|
9
|
+
|
|
10
|
+
id = Column(Integer, primary_key=True)
|
|
11
|
+
|
|
12
|
+
name = Column(String(collation="NOCASE"), index=True)
|
|
13
|
+
formula = Column(String, index=True)
|
|
14
|
+
exact_mass = Column(Float, index=True)
|
|
15
|
+
inchikey = Column(String, unique=True, nullable=False)
|
|
16
|
+
smiles = Column(String, index=True, unique=True)
|
|
17
|
+
pubchem_id = Column(Integer, index=True, unique=True)
|
|
18
|
+
added_on = Column(
|
|
19
|
+
DateTime(timezone=True),
|
|
20
|
+
default=datetime.now,
|
|
21
|
+
nullable=False
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
spectra = relationship("Spectrum", back_populates="compound")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, DateTime
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from libviz.core.db import Base
|
|
5
|
+
|
|
6
|
+
class LibraryMetadata(Base):
|
|
7
|
+
__tablename__ = "library_metadata"
|
|
8
|
+
|
|
9
|
+
id = Column(Integer, primary_key=True)
|
|
10
|
+
name = Column(String, nullable=False)
|
|
11
|
+
added_on = Column(
|
|
12
|
+
DateTime(timezone=True),
|
|
13
|
+
default=datetime.now,
|
|
14
|
+
nullable=False
|
|
15
|
+
)
|
|
16
|
+
last_updated_on = Column(
|
|
17
|
+
DateTime(timezone=True),
|
|
18
|
+
default=datetime.now,
|
|
19
|
+
onupdate=datetime.now,
|
|
20
|
+
nullable=False
|
|
21
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import CheckConstraint, Column, Integer, String, Float, DateTime, ForeignKey, LargeBinary, UniqueConstraint
|
|
5
|
+
from sqlalchemy.orm import relationship
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from libviz.core.db import Base
|
|
9
|
+
|
|
10
|
+
class Spectrum(Base):
|
|
11
|
+
__tablename__ = "spectrum"
|
|
12
|
+
|
|
13
|
+
id = Column(Integer, primary_key=True)
|
|
14
|
+
compound_id = Column(Integer, ForeignKey("compound.id"), nullable=False, index=True)
|
|
15
|
+
precursor_mz = Column(Float, index=True, nullable=False)
|
|
16
|
+
adduct_type_id = Column(Integer, ForeignKey("adduct_type.id"), nullable=False, index=True)
|
|
17
|
+
polarity = Column(String, CheckConstraint("polarity IN ('POSITIVE', 'NEGATIVE')"), nullable=True, index=True)
|
|
18
|
+
collision_energy = Column(Float, index=True, nullable=False)
|
|
19
|
+
mz_array = Column(LargeBinary, nullable=False)
|
|
20
|
+
intensity_array = Column(LargeBinary, nullable=False)
|
|
21
|
+
added_on = Column(
|
|
22
|
+
DateTime(timezone=True),
|
|
23
|
+
default=datetime.now,
|
|
24
|
+
nullable=False
|
|
25
|
+
)
|
|
26
|
+
spectrum_hash = Column(String(64), nullable=False, index=True)
|
|
27
|
+
|
|
28
|
+
compound = relationship("Compound", back_populates="spectra")
|
|
29
|
+
adduct_type = relationship("AdductType", back_populates="spectra")
|
|
30
|
+
spectrum_metadata = relationship(
|
|
31
|
+
"SpectrumMetadata",
|
|
32
|
+
back_populates="spectrum",
|
|
33
|
+
cascade="all, delete-orphan"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__table_args__ = (
|
|
37
|
+
UniqueConstraint(
|
|
38
|
+
"compound_id",
|
|
39
|
+
"adduct_type_id",
|
|
40
|
+
"polarity",
|
|
41
|
+
"collision_energy",
|
|
42
|
+
"spectrum_hash",
|
|
43
|
+
name="uq_spectrum_identity"
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_arrays(
|
|
49
|
+
cls,
|
|
50
|
+
*,
|
|
51
|
+
compound_id: int,
|
|
52
|
+
precursor_mz: float,
|
|
53
|
+
adduct_type_id: int,
|
|
54
|
+
polarity: str,
|
|
55
|
+
collision_energy: float,
|
|
56
|
+
mz: np.ndarray,
|
|
57
|
+
intensity: np.ndarray,
|
|
58
|
+
):
|
|
59
|
+
|
|
60
|
+
mz = np.asarray(mz, dtype="<f4")
|
|
61
|
+
intensity = np.asarray(intensity, dtype="<f4")
|
|
62
|
+
order = np.argsort(mz)
|
|
63
|
+
mz = np.round(mz[order], 4)
|
|
64
|
+
intensity = np.round(intensity[order], 2)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
return cls(
|
|
68
|
+
compound_id=compound_id,
|
|
69
|
+
precursor_mz=precursor_mz,
|
|
70
|
+
adduct_type_id=adduct_type_id,
|
|
71
|
+
polarity=polarity,
|
|
72
|
+
collision_energy=collision_energy,
|
|
73
|
+
mz_array=mz.tobytes(),
|
|
74
|
+
intensity_array=intensity.tobytes(),
|
|
75
|
+
spectrum_hash=calculate_spectrum_hash(mz, intensity),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def mz_numpy(self) -> np.ndarray:
|
|
79
|
+
return np.frombuffer(self.mz_array, dtype="<f4")
|
|
80
|
+
|
|
81
|
+
def intensity_numpy(self) -> np.ndarray:
|
|
82
|
+
return np.frombuffer(self.intensity_array, dtype="<f4")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def calculate_spectrum_hash(mz: np.ndarray, intensity: np.ndarray) -> str:
|
|
86
|
+
h = hashlib.sha256()
|
|
87
|
+
h.update(np.asarray(mz, dtype="<f4").tobytes())
|
|
88
|
+
h.update(np.asarray(intensity, dtype="<f4").tobytes())
|
|
89
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, ForeignKey
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from libviz.core.db import Base
|
|
5
|
+
|
|
6
|
+
class SpectrumMetadata(Base):
|
|
7
|
+
__tablename__ = "spectrum_metadata"
|
|
8
|
+
|
|
9
|
+
id = Column(Integer, primary_key=True)
|
|
10
|
+
spectrum_id = Column(Integer, ForeignKey("spectrum.id"), nullable=False, index=True)
|
|
11
|
+
key = Column(String, nullable=False, index=True)
|
|
12
|
+
value = Column(String, nullable=False)
|
|
13
|
+
|
|
14
|
+
spectrum = relationship("Spectrum", back_populates="spectrum_metadata")
|