wavefactor 2.10.0__cp310-cp310-win_amd64.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.
- WaveFactor.cp310-win_amd64.pyd +0 -0
- wavefactor/__init__.py +41 -0
- wavefactor/data.py +249 -0
- wavefactor/engine.py +461 -0
- wavefactor/model.py +309 -0
- wavefactor/priors.py +168 -0
- wavefactor/results.py +298 -0
- wavefactor-2.10.0.dist-info/METADATA +110 -0
- wavefactor-2.10.0.dist-info/RECORD +15 -0
- wavefactor-2.10.0.dist-info/WHEEL +5 -0
- wavefactor-2.10.0.dist-info/licenses/LICENSE +674 -0
- wavefactor-2.10.0.dist-info/licenses/lib/eigen/COPYING.BSD +26 -0
- wavefactor-2.10.0.dist-info/licenses/lib/eigen/COPYING.MPL2 +373 -0
- wavefactor-2.10.0.dist-info/licenses/lib/pybind11/LICENSE +29 -0
- wavefactor-2.10.0.dist-info/top_level.txt +2 -0
|
Binary file
|
wavefactor/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WaveFactor: Wavelet-based Bayesian Sparse Factor Model For Spatial Transcriptomics
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_version() -> str:
|
|
9
|
+
"""Loads WaveFactor package version from repository root or package metadata."""
|
|
10
|
+
# 1. Dev mode: read language-neutral VERSION asset directly from repository root
|
|
11
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
12
|
+
repo_version_file = repo_root / "VERSION"
|
|
13
|
+
if repo_version_file.is_file() and (repo_root / "CMakeLists.txt").is_file():
|
|
14
|
+
return repo_version_file.read_text(encoding="utf-8").strip()
|
|
15
|
+
|
|
16
|
+
# 2. Installed mode: read from package metadata when distributed as a wheel
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import version
|
|
19
|
+
return version("wavefactor")
|
|
20
|
+
except Exception:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"WaveFactor version could not be determined. "
|
|
23
|
+
"Please ensure the package is installed or the VERSION file is present."
|
|
24
|
+
) from None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__version__ = _load_version()
|
|
28
|
+
|
|
29
|
+
from .priors import Priors
|
|
30
|
+
from .data import WaveFactorData, prepare_spatial_data
|
|
31
|
+
from .results import WaveFactorResult
|
|
32
|
+
from .model import WaveFactor
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"WaveFactor",
|
|
37
|
+
"WaveFactorResult",
|
|
38
|
+
"WaveFactorData",
|
|
39
|
+
"Priors",
|
|
40
|
+
"prepare_spatial_data",
|
|
41
|
+
]
|
wavefactor/data.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data ingestion, spatial rasterization, and 2D wavelet decomposition for WaveFactor.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Union, Tuple, Optional, Dict, Any, List
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pywt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_power_of_4(n: int) -> bool:
|
|
12
|
+
"""Return True if n is an exact power of 4."""
|
|
13
|
+
try:
|
|
14
|
+
n = int(n)
|
|
15
|
+
except (ValueError, TypeError):
|
|
16
|
+
return False
|
|
17
|
+
return n > 0 and (n & (n - 1)) == 0 and (n.bit_length() - 1) % 2 == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_L_shape(n_spots: int, n_resolutions: int, n_factors: int) -> List:
|
|
21
|
+
"""Generates empty nested list structure matching L shape."""
|
|
22
|
+
return [
|
|
23
|
+
[[np.array([0.0] * (int(n_spots / (4 ** (n_resolutions - 1)))))]]
|
|
24
|
+
+ [
|
|
25
|
+
[
|
|
26
|
+
np.array([0.0] * (int(n_spots / (4 ** (n_resolutions - i - 1)))))
|
|
27
|
+
for _ in range(3)
|
|
28
|
+
]
|
|
29
|
+
for i in range(n_resolutions - 1)
|
|
30
|
+
]
|
|
31
|
+
for _ in range(n_factors)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_Y_shape(n_spots: int, n_resolutions: int, n_features: int) -> List:
|
|
36
|
+
"""Generates empty nested list structure matching Y shape."""
|
|
37
|
+
return [
|
|
38
|
+
[[np.array([0.0] * (int(n_spots / (4 ** (n_resolutions - 1)))))]]
|
|
39
|
+
+ [
|
|
40
|
+
[
|
|
41
|
+
np.array([0.0] * (int(n_spots / (4 ** (n_resolutions - i - 1)))))
|
|
42
|
+
for _ in range(3)
|
|
43
|
+
]
|
|
44
|
+
for i in range(n_resolutions - 1)
|
|
45
|
+
]
|
|
46
|
+
for _ in range(n_features)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WaveFactorData:
|
|
51
|
+
"""
|
|
52
|
+
Container holding processed spatial transcriptomics matrices,
|
|
53
|
+
2D discrete wavelet decompositions (true_Y), dimensions, and spatial mappings.
|
|
54
|
+
|
|
55
|
+
Attributes
|
|
56
|
+
----------
|
|
57
|
+
true_Y : List
|
|
58
|
+
Nested DWT wavelet decomposition matrices for all features.
|
|
59
|
+
dimensions : Dict[str, Any]
|
|
60
|
+
Dictionary with keys 'n_spots', 'n_resolutions', 'n_factors', 'n_features'.
|
|
61
|
+
grid_side_length : int
|
|
62
|
+
Side length L of the square L x L spatial grid (e.g. 64 for a 64x64 grid).
|
|
63
|
+
grid_shape : Tuple[int, int]
|
|
64
|
+
2D grid shape (grid_side_length, grid_side_length).
|
|
65
|
+
spot_grid_coords : np.ndarray
|
|
66
|
+
Array of shape (N_spots, 2) holding Cartesian [grid_x, grid_y] coordinates:
|
|
67
|
+
- Column 0: grid_x (horizontal coordinate, column index in 0..L-1)
|
|
68
|
+
- Column 1: grid_y (vertical coordinate, row index in 0..L-1)
|
|
69
|
+
grid_x : np.ndarray
|
|
70
|
+
1D integer grid x-coordinates (columns in 0..L-1).
|
|
71
|
+
grid_y : np.ndarray
|
|
72
|
+
1D integer grid y-coordinates (rows in 0..L-1).
|
|
73
|
+
coords : np.ndarray, optional
|
|
74
|
+
Original raw spatial coordinates (N_spots, 2).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
true_Y: List,
|
|
80
|
+
dimensions: Dict[str, Any],
|
|
81
|
+
grid_side_length: int,
|
|
82
|
+
spot_grid_coords: Optional[np.ndarray] = None,
|
|
83
|
+
coords: Optional[np.ndarray] = None,
|
|
84
|
+
):
|
|
85
|
+
self.true_Y = true_Y
|
|
86
|
+
self.dimensions = dimensions
|
|
87
|
+
self.grid_side_length = int(grid_side_length)
|
|
88
|
+
self.spot_grid_coords = spot_grid_coords
|
|
89
|
+
self.coords = coords
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def grid_shape(self) -> Tuple[int, int]:
|
|
93
|
+
"""2D spatial grid shape (grid_side_length, grid_side_length)."""
|
|
94
|
+
return (self.grid_side_length, self.grid_side_length)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def grid_x(self) -> Optional[np.ndarray]:
|
|
98
|
+
"""1D integer grid x-coordinates (columns in 0..L-1)."""
|
|
99
|
+
return self.spot_grid_coords[:, 0] if self.spot_grid_coords is not None else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def grid_y(self) -> Optional[np.ndarray]:
|
|
103
|
+
"""1D integer grid y-coordinates (rows in 0..L-1)."""
|
|
104
|
+
return self.spot_grid_coords[:, 1] if self.spot_grid_coords is not None else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def prepare_spatial_data(
|
|
108
|
+
X: np.ndarray,
|
|
109
|
+
coords: np.ndarray,
|
|
110
|
+
n_factors: int = 10,
|
|
111
|
+
n_length_scales: int = 4,
|
|
112
|
+
) -> WaveFactorData:
|
|
113
|
+
"""
|
|
114
|
+
Preprocesses spatial transcriptomics input data, validates 2D spatial grid mappings,
|
|
115
|
+
and executes 2D DWT wavelet decomposition.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
X : np.ndarray
|
|
120
|
+
Gene expression matrix of shape (N_spots, N_features).
|
|
121
|
+
coords : np.ndarray
|
|
122
|
+
Spatial (x, y) integer grid coordinates for each spot, shape (N_spots, 2).
|
|
123
|
+
Must be a unique 1-to-1 mapping onto the [0, L-1] x [0, L-1] lattice,
|
|
124
|
+
where N_spots = L * L is an exact power of 4.
|
|
125
|
+
n_factors : int, default=10
|
|
126
|
+
Number of latent factors (K).
|
|
127
|
+
n_length_scales : int, default=4
|
|
128
|
+
Number of wavelet detail levels (D). Total resolutions R = D + 1.
|
|
129
|
+
Must satisfy 2^D <= L (i.e. D <= log2(L)).
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
WaveFactorData
|
|
134
|
+
Structured data container with wavelet decomposition and dimension constants.
|
|
135
|
+
"""
|
|
136
|
+
# 1. Parse and validate 2D expression matrix
|
|
137
|
+
expr_matrix = np.asarray(X, dtype=np.float64)
|
|
138
|
+
if expr_matrix.ndim != 2:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"X must be a 2D numpy array of shape (N_spots, N_features), got ndim={expr_matrix.ndim} with shape {expr_matrix.shape}."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
N_spots, N_features = expr_matrix.shape
|
|
144
|
+
|
|
145
|
+
# 2. Strict power-of-4 spot count verification
|
|
146
|
+
if not is_power_of_4(N_spots):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Number of spots (N={N_spots}) must be an exact power of 4 (e.g. 64, 256, 1024, 4096) "
|
|
149
|
+
"to form a complete 2D dyadic wavelet lattice."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
side_len = int(round(np.sqrt(N_spots)))
|
|
153
|
+
max_D = int(round(np.log2(side_len)))
|
|
154
|
+
|
|
155
|
+
# 3. Wavelet resolution bound check (2^D <= L)
|
|
156
|
+
if n_length_scales > max_D:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"n_length_scales (D={n_length_scales}) cannot exceed log2(grid_side_length)={max_D} "
|
|
159
|
+
f"for a grid of side length L={side_len} (N={N_spots} spots)."
|
|
160
|
+
)
|
|
161
|
+
if n_length_scales <= 0:
|
|
162
|
+
raise ValueError(f"n_length_scales must be a positive integer, got {n_length_scales}.")
|
|
163
|
+
|
|
164
|
+
# 4. Parse and strictly validate spatial coordinates
|
|
165
|
+
spot_coords = np.asarray(coords)
|
|
166
|
+
if spot_coords.ndim != 2 or spot_coords.shape != (N_spots, 2):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"coords must be a 2D array of shape (N_spots, 2) = ({N_spots}, 2), got shape {spot_coords.shape}."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Validate integer coordinates
|
|
172
|
+
if not (np.issubdtype(spot_coords.dtype, np.integer) or np.all(np.equal(np.mod(spot_coords, 1), 0))):
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"coords must contain integer grid indices on [0, {side_len - 1}] x [0, {side_len - 1}]."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
grid_x = spot_coords[:, 0].astype(int)
|
|
178
|
+
grid_y = spot_coords[:, 1].astype(int)
|
|
179
|
+
|
|
180
|
+
# Bounds check
|
|
181
|
+
if grid_x.min() < 0 or grid_x.max() >= side_len or grid_y.min() < 0 or grid_y.max() >= side_len:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"coords indices out of bounds: all x and y must be within [0, {side_len - 1}] "
|
|
184
|
+
f"for an inferred {side_len}x{side_len} grid."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Bijection check: every lattice cell must be covered exactly once
|
|
188
|
+
flat_indices = grid_y * side_len + grid_x
|
|
189
|
+
if len(np.unique(flat_indices)) != N_spots:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Input coords must define a unique 1-to-1 lattice mapping onto the {side_len}x{side_len} grid. "
|
|
192
|
+
"Found duplicate or missing grid cell coordinates."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
spot_grid_coords = np.column_stack([grid_x, grid_y])
|
|
196
|
+
|
|
197
|
+
# 5. Direct 1-to-1 lattice feature grid assignment
|
|
198
|
+
feature_grid = np.zeros((side_len, side_len, N_features), dtype=np.float64)
|
|
199
|
+
feature_grid[grid_y, grid_x, :] = expr_matrix
|
|
200
|
+
|
|
201
|
+
# Compute 2D DWT using Haar basis
|
|
202
|
+
true_Y = _compute_2d_dwt(feature_grid, n_length_scales, wavelet="haar")
|
|
203
|
+
total_grid_spots = side_len * side_len
|
|
204
|
+
dimensions = _build_dimensions_dict(
|
|
205
|
+
n_spots=total_grid_spots,
|
|
206
|
+
n_length_scales=n_length_scales,
|
|
207
|
+
n_factors=n_factors,
|
|
208
|
+
n_features=N_features,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return WaveFactorData(
|
|
212
|
+
true_Y=true_Y,
|
|
213
|
+
dimensions=dimensions,
|
|
214
|
+
grid_side_length=side_len,
|
|
215
|
+
spot_grid_coords=spot_grid_coords,
|
|
216
|
+
coords=spot_coords,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _compute_2d_dwt(feature_grid: np.ndarray, n_length_scales: int, wavelet: str = "haar") -> List:
|
|
221
|
+
"""Performs 2D DWT across all features and formats into true_Y nested structure."""
|
|
222
|
+
H, W, G = feature_grid.shape
|
|
223
|
+
flattened_wavelet_matrices = []
|
|
224
|
+
|
|
225
|
+
for g in range(G):
|
|
226
|
+
coeffs = pywt.wavedec2(feature_grid[:, :, g], wavelet, level=n_length_scales)
|
|
227
|
+
flattened_coeffs = []
|
|
228
|
+
flattened_coeffs.append((coeffs[0].flatten(),))
|
|
229
|
+
for res in coeffs[1:]:
|
|
230
|
+
flattened_coeffs.append(tuple(matrix.flatten() for matrix in res))
|
|
231
|
+
flattened_wavelet_matrices.append(flattened_coeffs)
|
|
232
|
+
|
|
233
|
+
return flattened_wavelet_matrices
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_dimensions_dict(n_spots: int, n_length_scales: int, n_factors: int, n_features: int) -> Dict[str, Any]:
|
|
237
|
+
"""Constructs dimensions dictionary matching C++ CAVI interface."""
|
|
238
|
+
n_resolutions = n_length_scales + 1
|
|
239
|
+
return {
|
|
240
|
+
"n_factors": n_factors,
|
|
241
|
+
"n_resolutions": n_resolutions,
|
|
242
|
+
"n_features": n_features,
|
|
243
|
+
"n_spots": n_spots,
|
|
244
|
+
"L_shape": get_L_shape(n_spots, n_resolutions, n_factors),
|
|
245
|
+
"Y_shape": get_Y_shape(n_spots, n_resolutions, n_features),
|
|
246
|
+
"ab_t_shape": (n_resolutions, n_factors),
|
|
247
|
+
"ab_tau_shape": (n_resolutions, n_features),
|
|
248
|
+
"F_shape": (n_factors, n_features),
|
|
249
|
+
}
|