statgpu 0.1.0__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.
- statgpu/__init__.py +174 -0
- statgpu/_base.py +544 -0
- statgpu/_config.py +127 -0
- statgpu/anova/__init__.py +5 -0
- statgpu/anova/_oneway.py +194 -0
- statgpu/backends/__init__.py +83 -0
- statgpu/backends/_array_ops.py +529 -0
- statgpu/backends/_base.py +184 -0
- statgpu/backends/_cupy.py +453 -0
- statgpu/backends/_factory.py +65 -0
- statgpu/backends/_gpu_inference_cupy.py +214 -0
- statgpu/backends/_gpu_inference_torch.py +422 -0
- statgpu/backends/_numpy.py +324 -0
- statgpu/backends/_torch.py +685 -0
- statgpu/backends/_torch_safe.py +47 -0
- statgpu/backends/_utils.py +423 -0
- statgpu/core/__init__.py +10 -0
- statgpu/core/formula/__init__.py +33 -0
- statgpu/core/formula/_design.py +99 -0
- statgpu/core/formula/_parser.py +191 -0
- statgpu/core/formula/_terms.py +70 -0
- statgpu/core/formula/tests/__init__.py +0 -0
- statgpu/core/formula/tests/test_parser.py +194 -0
- statgpu/covariance/__init__.py +6 -0
- statgpu/covariance/_empirical.py +310 -0
- statgpu/covariance/_shrinkage.py +248 -0
- statgpu/cross_validation/__init__.py +31 -0
- statgpu/cross_validation/_base.py +410 -0
- statgpu/cross_validation/_engine.py +167 -0
- statgpu/diagnostics/__init__.py +7 -0
- statgpu/diagnostics/_regression_diagnostics.py +188 -0
- statgpu/feature_selection/__init__.py +24 -0
- statgpu/feature_selection/_knockoff.py +870 -0
- statgpu/feature_selection/_knockoff_utils.py +1003 -0
- statgpu/feature_selection/_stepwise.py +300 -0
- statgpu/glm_core/__init__.py +81 -0
- statgpu/glm_core/_base.py +202 -0
- statgpu/glm_core/_family.py +362 -0
- statgpu/glm_core/_fused.py +149 -0
- statgpu/glm_core/_gamma.py +111 -0
- statgpu/glm_core/_inverse_gaussian.py +62 -0
- statgpu/glm_core/_irls.py +561 -0
- statgpu/glm_core/_logistic.py +82 -0
- statgpu/glm_core/_negative_binomial.py +68 -0
- statgpu/glm_core/_poisson.py +60 -0
- statgpu/glm_core/_solver_legacy.py +100 -0
- statgpu/glm_core/_squared.py +53 -0
- statgpu/glm_core/_tweedie.py +74 -0
- statgpu/inference/__init__.py +239 -0
- statgpu/inference/_distributions_backend.py +2610 -0
- statgpu/inference/_multiple_testing.py +391 -0
- statgpu/inference/_resampling.py +1400 -0
- statgpu/inference/_results.py +265 -0
- statgpu/linear_model/__init__.py +75 -0
- statgpu/linear_model/_gaussian_inference.py +306 -0
- statgpu/linear_model/_glm_base.py +1261 -0
- statgpu/linear_model/_ordered_logit.py +52 -0
- statgpu/linear_model/_ordered_probit.py +50 -0
- statgpu/linear_model/_stats.py +170 -0
- statgpu/linear_model/cv/__init__.py +13 -0
- statgpu/linear_model/cv/_elasticnet_cv.py +892 -0
- statgpu/linear_model/cv/_lasso_cv.py +253 -0
- statgpu/linear_model/cv/_logistic_cv.py +895 -0
- statgpu/linear_model/cv/_ridge_cv.py +1160 -0
- statgpu/linear_model/legacy/__init__.py +1 -0
- statgpu/linear_model/legacy/_distributions_legacy_gpu.py +340 -0
- statgpu/linear_model/legacy/_elasticnet_legacy.py +936 -0
- statgpu/linear_model/legacy/_lasso_legacy.py +4876 -0
- statgpu/linear_model/legacy/_penalized_legacy.py +1174 -0
- statgpu/linear_model/legacy/_ridge_legacy.py +863 -0
- statgpu/linear_model/legacy/_solver_legacy.py +104 -0
- statgpu/linear_model/penalized/__init__.py +25 -0
- statgpu/linear_model/penalized/_base.py +437 -0
- statgpu/linear_model/penalized/_fit_mixin.py +1877 -0
- statgpu/linear_model/penalized/_inference_mixin.py +1179 -0
- statgpu/linear_model/penalized/_penalized_cv.py +2699 -0
- statgpu/linear_model/penalized/_penalized_gamma.py +86 -0
- statgpu/linear_model/penalized/_penalized_inverse_gaussian.py +62 -0
- statgpu/linear_model/penalized/_penalized_linear.py +236 -0
- statgpu/linear_model/penalized/_penalized_logistic.py +100 -0
- statgpu/linear_model/penalized/_penalized_negative_binomial.py +65 -0
- statgpu/linear_model/penalized/_penalized_poisson.py +62 -0
- statgpu/linear_model/penalized/_penalized_tweedie.py +65 -0
- statgpu/linear_model/penalized/_predict_mixin.py +182 -0
- statgpu/linear_model/wrappers/__init__.py +31 -0
- statgpu/linear_model/wrappers/_adaptive_lasso.py +63 -0
- statgpu/linear_model/wrappers/_elasticnet.py +75 -0
- statgpu/linear_model/wrappers/_gamma.py +67 -0
- statgpu/linear_model/wrappers/_inverse_gaussian.py +47 -0
- statgpu/linear_model/wrappers/_lasso.py +2124 -0
- statgpu/linear_model/wrappers/_linear.py +1127 -0
- statgpu/linear_model/wrappers/_logistic.py +1435 -0
- statgpu/linear_model/wrappers/_mcp.py +58 -0
- statgpu/linear_model/wrappers/_negative_binomial.py +58 -0
- statgpu/linear_model/wrappers/_poisson.py +48 -0
- statgpu/linear_model/wrappers/_ridge.py +166 -0
- statgpu/linear_model/wrappers/_scad.py +58 -0
- statgpu/linear_model/wrappers/_tweedie.py +57 -0
- statgpu/metrics/__init__.py +21 -0
- statgpu/metrics/_classification.py +591 -0
- statgpu/nonparametric/__init__.py +50 -0
- statgpu/nonparametric/kernel_methods/__init__.py +25 -0
- statgpu/nonparametric/kernel_methods/_kernels.py +246 -0
- statgpu/nonparametric/kernel_methods/_krr.py +234 -0
- statgpu/nonparametric/kernel_methods/_krr_cv.py +380 -0
- statgpu/nonparametric/kernel_smoothing/__init__.py +39 -0
- statgpu/nonparametric/kernel_smoothing/_bandwidth_selection.py +1083 -0
- statgpu/nonparametric/kernel_smoothing/_kde.py +761 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_common.py +348 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_regression.py +748 -0
- statgpu/nonparametric/splines/__init__.py +5 -0
- statgpu/nonparametric/splines/_bspline_basis.py +336 -0
- statgpu/nonparametric/splines/_penalized.py +349 -0
- statgpu/panel/__init__.py +19 -0
- statgpu/panel/_covariance.py +140 -0
- statgpu/panel/_fixed_effects.py +420 -0
- statgpu/panel/_random_effects.py +385 -0
- statgpu/panel/_utils.py +482 -0
- statgpu/penalties/__init__.py +139 -0
- statgpu/penalties/_adaptive_l1.py +313 -0
- statgpu/penalties/_base.py +261 -0
- statgpu/penalties/_categories.py +39 -0
- statgpu/penalties/_elasticnet.py +98 -0
- statgpu/penalties/_group_lasso.py +678 -0
- statgpu/penalties/_group_mcp.py +553 -0
- statgpu/penalties/_group_scad.py +605 -0
- statgpu/penalties/_l1.py +107 -0
- statgpu/penalties/_l2.py +77 -0
- statgpu/penalties/_mcp.py +237 -0
- statgpu/penalties/_scad.py +260 -0
- statgpu/semiparametric/__init__.py +5 -0
- statgpu/semiparametric/_gam.py +401 -0
- statgpu/solvers/__init__.py +24 -0
- statgpu/solvers/_admm.py +241 -0
- statgpu/solvers/_constants.py +15 -0
- statgpu/solvers/_convergence.py +6 -0
- statgpu/solvers/_fista.py +436 -0
- statgpu/solvers/_fista_bb.py +513 -0
- statgpu/solvers/_fista_lla.py +541 -0
- statgpu/solvers/_lbfgs.py +206 -0
- statgpu/solvers/_newton.py +149 -0
- statgpu/solvers/_utils.py +277 -0
- statgpu/survival/__init__.py +14 -0
- statgpu/survival/_cox.py +3974 -0
- statgpu/survival/_cox_breslow_triton_kernel.py +106 -0
- statgpu/survival/_cox_cv.py +1159 -0
- statgpu/survival/_cox_efron_cuda.py +1280 -0
- statgpu/survival/_cox_efron_triton.py +359 -0
- statgpu/unsupervised/__init__.py +29 -0
- statgpu/unsupervised/_agglomerative.py +307 -0
- statgpu/unsupervised/_dbscan.py +263 -0
- statgpu/unsupervised/_dbscan_cpu.pyx +125 -0
- statgpu/unsupervised/_gmm.py +332 -0
- statgpu/unsupervised/_incremental_pca.py +176 -0
- statgpu/unsupervised/_kmeans.py +261 -0
- statgpu/unsupervised/_minibatch_kmeans.py +299 -0
- statgpu/unsupervised/_minibatch_nmf.py +252 -0
- statgpu/unsupervised/_nmf.py +190 -0
- statgpu/unsupervised/_pca.py +189 -0
- statgpu/unsupervised/_truncated_svd.py +132 -0
- statgpu/unsupervised/_tsne.py +192 -0
- statgpu/unsupervised/_umap.py +224 -0
- statgpu/unsupervised/_utils.py +134 -0
- statgpu-0.1.0.dist-info/METADATA +245 -0
- statgpu-0.1.0.dist-info/RECORD +168 -0
- statgpu-0.1.0.dist-info/WHEEL +5 -0
- statgpu-0.1.0.dist-info/licenses/LICENSE +199 -0
- statgpu-0.1.0.dist-info/top_level.txt +1 -0
statgpu/_base.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for statgpu estimators.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = ["BaseEstimator"]
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Optional, Union, Any
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from statgpu._config import Device, get_device
|
|
14
|
+
from statgpu.backends import (
|
|
15
|
+
get_backend,
|
|
16
|
+
BackendBase,
|
|
17
|
+
_get_torch_device_str,
|
|
18
|
+
_cupy_to_torch_dlpack,
|
|
19
|
+
_torch_to_cupy_dlpack,
|
|
20
|
+
_numpy_to_torch_tensor,
|
|
21
|
+
_move_torch_tensor,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseEstimator(ABC):
|
|
26
|
+
"""
|
|
27
|
+
Base class for all statgpu estimators.
|
|
28
|
+
|
|
29
|
+
Provides common functionality for device management and input validation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
device: Union[str, Device] = Device.AUTO,
|
|
35
|
+
n_jobs: Optional[int] = None
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize base estimator.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
device : str or Device, default='auto'
|
|
43
|
+
Computation device: 'cpu', 'cuda', or 'auto'.
|
|
44
|
+
n_jobs : int, optional
|
|
45
|
+
Number of parallel jobs for CPU computation.
|
|
46
|
+
-1 means using all processors.
|
|
47
|
+
"""
|
|
48
|
+
self.device = device if isinstance(device, Device) else Device(device)
|
|
49
|
+
self.n_jobs = n_jobs
|
|
50
|
+
self._fitted = False
|
|
51
|
+
|
|
52
|
+
def _get_compute_device(self) -> Device:
|
|
53
|
+
"""Resolve device for actual computation."""
|
|
54
|
+
if self.device == Device.AUTO:
|
|
55
|
+
return get_device()
|
|
56
|
+
return self.device
|
|
57
|
+
|
|
58
|
+
def _get_backend(self, backend: str = "auto") -> BackendBase:
|
|
59
|
+
"""
|
|
60
|
+
Return the compute backend appropriate for this estimator's device.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
backend : {'auto', 'numpy', 'cupy', 'torch'}, default='auto'
|
|
65
|
+
Override which array library to use. When ``'auto'``, the backend
|
|
66
|
+
is chosen based on :attr:`device` and GPU availability.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
BackendBase
|
|
71
|
+
A backend instance whose :attr:`~BackendBase.xp` attribute is the
|
|
72
|
+
underlying array module (NumPy, CuPy, or PyTorch).
|
|
73
|
+
"""
|
|
74
|
+
compute_device = self._get_compute_device()
|
|
75
|
+
device_str = compute_device.value # 'cpu', 'cuda', or 'torch'
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
self.device != Device.AUTO
|
|
79
|
+
and compute_device == Device.CUDA
|
|
80
|
+
and backend == "auto"
|
|
81
|
+
):
|
|
82
|
+
cupy_backend = get_backend(backend="cupy", device="cuda")
|
|
83
|
+
if not cupy_backend.is_available():
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"device='cuda' requires a working CuPy CUDA backend. "
|
|
86
|
+
"Use device='auto' to allow automatic backend selection."
|
|
87
|
+
)
|
|
88
|
+
return cupy_backend
|
|
89
|
+
|
|
90
|
+
# Handle Device.TORCH explicitly - use torch backend
|
|
91
|
+
if compute_device == Device.TORCH:
|
|
92
|
+
try:
|
|
93
|
+
import torch
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"device='torch' requires PyTorch with CUDA support."
|
|
97
|
+
) from exc
|
|
98
|
+
if not torch.cuda.is_available():
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"device='torch' requires torch.cuda.is_available() to be True. "
|
|
101
|
+
"Use device='auto' or device='cpu' if CUDA is unavailable."
|
|
102
|
+
)
|
|
103
|
+
return get_backend(backend="torch", device="cuda")
|
|
104
|
+
|
|
105
|
+
return get_backend(backend=backend, device=device_str)
|
|
106
|
+
|
|
107
|
+
def _to_array(self, X, device: Optional[Device] = None, backend: Optional[str] = None) -> Any:
|
|
108
|
+
"""
|
|
109
|
+
Convert input to appropriate array type for device.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
X : array-like
|
|
114
|
+
Input data.
|
|
115
|
+
device : Device, optional
|
|
116
|
+
Target device. If None, uses self._get_compute_device().
|
|
117
|
+
backend : {'numpy', 'cupy', 'torch'}, optional
|
|
118
|
+
Explicit backend selection. If None, uses device-based default.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
array
|
|
123
|
+
NumPy array (CPU), CuPy array (GPU), or Torch tensor.
|
|
124
|
+
"""
|
|
125
|
+
target_device = device or self._get_compute_device()
|
|
126
|
+
|
|
127
|
+
# If backend is explicitly specified, use it
|
|
128
|
+
if backend == "torch":
|
|
129
|
+
torch_target = "cpu" if target_device == Device.CPU else "cuda"
|
|
130
|
+
return self._to_torch(X, device=torch_target)
|
|
131
|
+
elif backend == "cupy":
|
|
132
|
+
return self._to_cupy(X)
|
|
133
|
+
elif backend == "numpy":
|
|
134
|
+
if hasattr(X, "get"):
|
|
135
|
+
return X.get()
|
|
136
|
+
# Handle torch tensors that may be on CUDA — must move to CPU first
|
|
137
|
+
if hasattr(X, 'cpu') and hasattr(X, 'numpy'):
|
|
138
|
+
return X.detach().cpu().numpy()
|
|
139
|
+
return np.asarray(X)
|
|
140
|
+
|
|
141
|
+
# Otherwise, use device-based default
|
|
142
|
+
if target_device == Device.TORCH:
|
|
143
|
+
return self._to_torch(X, device="cuda")
|
|
144
|
+
|
|
145
|
+
if target_device == Device.CUDA:
|
|
146
|
+
# Strict CUDA means CuPy-backed arrays. Torch has its own explicit
|
|
147
|
+
# device mode and is not used as an implicit CUDA fallback.
|
|
148
|
+
try:
|
|
149
|
+
import cupy as cp
|
|
150
|
+
if isinstance(X, cp.ndarray):
|
|
151
|
+
return X
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Convert to numpy first for CPU paths or for non-CuPy inputs.
|
|
156
|
+
if hasattr(X, "get"): # CuPy-like array
|
|
157
|
+
X_np = X.get()
|
|
158
|
+
elif hasattr(X, "cpu"): # PyTorch tensor
|
|
159
|
+
X_cpu = X.detach().cpu() if hasattr(X, "detach") else X.cpu()
|
|
160
|
+
X_np = X_cpu.numpy() if hasattr(X_cpu, "numpy") else np.asarray(X_cpu)
|
|
161
|
+
else:
|
|
162
|
+
X_np = np.asarray(X)
|
|
163
|
+
|
|
164
|
+
if target_device == Device.CUDA:
|
|
165
|
+
try:
|
|
166
|
+
import cupy as cp
|
|
167
|
+
return cp.asarray(X_np)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
"device='cuda' requires a working CuPy CUDA backend; "
|
|
171
|
+
"no CPU/Torch fallback is performed for explicit CUDA."
|
|
172
|
+
) from exc
|
|
173
|
+
|
|
174
|
+
return X_np
|
|
175
|
+
|
|
176
|
+
def _to_torch(self, X, device=None):
|
|
177
|
+
"""Convert input to Torch tensor."""
|
|
178
|
+
import torch
|
|
179
|
+
|
|
180
|
+
if device == "cuda" and not torch.cuda.is_available():
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
"device='torch' requires torch.cuda.is_available() to be True; "
|
|
183
|
+
"no Torch CPU fallback is performed for explicit Torch GPU."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if isinstance(X, torch.Tensor):
|
|
187
|
+
return _move_torch_tensor(X, device=device) if device else X
|
|
188
|
+
|
|
189
|
+
if hasattr(X, "get"): # CuPy
|
|
190
|
+
tensor = _cupy_to_torch_dlpack(X, device=device)
|
|
191
|
+
if tensor is not None:
|
|
192
|
+
return tensor
|
|
193
|
+
X_np = X.get()
|
|
194
|
+
elif hasattr(X, "cpu"): # Tensor-like
|
|
195
|
+
X_cpu = X.detach().cpu() if hasattr(X, "detach") else X.cpu()
|
|
196
|
+
X_np = X_cpu.numpy() if hasattr(X_cpu, "numpy") else np.asarray(X_cpu)
|
|
197
|
+
else:
|
|
198
|
+
target_device = device or _get_torch_device_str()
|
|
199
|
+
return _numpy_to_torch_tensor(
|
|
200
|
+
X,
|
|
201
|
+
device=target_device,
|
|
202
|
+
pin_memory=str(target_device).startswith("cuda"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
target_device = device or _get_torch_device_str()
|
|
206
|
+
return _numpy_to_torch_tensor(
|
|
207
|
+
X_np,
|
|
208
|
+
device=target_device,
|
|
209
|
+
pin_memory=str(target_device).startswith("cuda"),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _to_cupy(self, X):
|
|
213
|
+
"""Convert input to CuPy array."""
|
|
214
|
+
import cupy as cp
|
|
215
|
+
|
|
216
|
+
if isinstance(X, cp.ndarray):
|
|
217
|
+
return X
|
|
218
|
+
|
|
219
|
+
if hasattr(X, "cpu"): # PyTorch
|
|
220
|
+
arr = _torch_to_cupy_dlpack(X)
|
|
221
|
+
if arr is not None:
|
|
222
|
+
return arr
|
|
223
|
+
X_np = X.detach().cpu().numpy()
|
|
224
|
+
elif hasattr(X, "get"): # CuPy (shouldn't happen, but handle it)
|
|
225
|
+
X_np = X.get()
|
|
226
|
+
else:
|
|
227
|
+
X_np = np.asarray(X)
|
|
228
|
+
|
|
229
|
+
return cp.asarray(X_np)
|
|
230
|
+
|
|
231
|
+
def _to_numpy(self, X) -> np.ndarray:
|
|
232
|
+
"""Convert array back to numpy."""
|
|
233
|
+
if hasattr(X, 'get'): # CuPy
|
|
234
|
+
return X.get()
|
|
235
|
+
elif hasattr(X, 'cpu'): # PyTorch
|
|
236
|
+
return X.detach().cpu().numpy()
|
|
237
|
+
return np.asarray(X)
|
|
238
|
+
|
|
239
|
+
def adjust_pvalues(
|
|
240
|
+
self,
|
|
241
|
+
pvalues=None,
|
|
242
|
+
method: str = "bh",
|
|
243
|
+
alpha: float = 0.05,
|
|
244
|
+
axis: Optional[int] = 0,
|
|
245
|
+
backend: str = "auto",
|
|
246
|
+
):
|
|
247
|
+
"""
|
|
248
|
+
Adjust p-values for multiple testing (FDR/FWER controls).
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
pvalues : array-like, optional
|
|
253
|
+
Raw p-values. If omitted, uses this estimator's ``_pvalues``.
|
|
254
|
+
method : str, default='bh'
|
|
255
|
+
Adjustment method: ``bh``, ``by``, ``holm``, ``bonferroni``
|
|
256
|
+
(aliases accepted).
|
|
257
|
+
alpha : float, default=0.05
|
|
258
|
+
Rejection threshold in (0, 1).
|
|
259
|
+
axis : int or None, default=0
|
|
260
|
+
Axis along which to adjust. ``None`` flattens all entries.
|
|
261
|
+
backend : {'auto', 'numpy', 'cupy'}, default='auto'
|
|
262
|
+
Compute backend. ``'auto'`` uses CuPy when estimator device is CUDA.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
dict
|
|
267
|
+
Contains ``pvalues``, ``pvalues_adjusted``, ``reject``,
|
|
268
|
+
``method``, ``alpha``, and ``axis``.
|
|
269
|
+
"""
|
|
270
|
+
from statgpu.inference import adjust_pvalues as _adjust_pvalues
|
|
271
|
+
|
|
272
|
+
source = pvalues
|
|
273
|
+
if source is None:
|
|
274
|
+
source = getattr(self, "_pvalues", None)
|
|
275
|
+
if source is None:
|
|
276
|
+
raise RuntimeError(
|
|
277
|
+
"No p-values available. Fit with inference enabled or pass pvalues explicitly."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
backend_name = str(backend).strip().lower()
|
|
281
|
+
if backend_name == "auto" and self._get_compute_device() == Device.CUDA:
|
|
282
|
+
backend_name = "cupy"
|
|
283
|
+
|
|
284
|
+
if backend_name == "cupy":
|
|
285
|
+
pvals = self._to_array(source, Device.CUDA)
|
|
286
|
+
else:
|
|
287
|
+
pvals = self._to_numpy(source)
|
|
288
|
+
|
|
289
|
+
reject, pvals_adj = _adjust_pvalues(
|
|
290
|
+
pvals,
|
|
291
|
+
method=method,
|
|
292
|
+
alpha=alpha,
|
|
293
|
+
axis=axis,
|
|
294
|
+
backend=backend_name,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"method": method,
|
|
299
|
+
"alpha": float(alpha),
|
|
300
|
+
"axis": axis,
|
|
301
|
+
"backend": backend_name,
|
|
302
|
+
"pvalues": pvals,
|
|
303
|
+
"pvalues_adjusted": pvals_adj,
|
|
304
|
+
"reject": reject,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def combine_pvalues(
|
|
308
|
+
self,
|
|
309
|
+
pvalues=None,
|
|
310
|
+
method: str = "fisher",
|
|
311
|
+
weights=None,
|
|
312
|
+
axis: Optional[int] = None,
|
|
313
|
+
backend: str = "auto",
|
|
314
|
+
):
|
|
315
|
+
"""
|
|
316
|
+
Combine p-values into a global p-value.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
pvalues : array-like, optional
|
|
321
|
+
Raw p-values. If omitted, uses this estimator's ``_pvalues``.
|
|
322
|
+
method : str, default='fisher'
|
|
323
|
+
Combination method: ``fisher`` or ``cauchy`` (aliases accepted).
|
|
324
|
+
weights : array-like, optional
|
|
325
|
+
Optional non-negative weights for cauchy combination.
|
|
326
|
+
axis : int or None, default=None
|
|
327
|
+
Axis along which to combine p-values. ``None`` flattens input.
|
|
328
|
+
backend : {'auto', 'numpy', 'cupy'}, default='auto'
|
|
329
|
+
Compute backend. ``'auto'`` uses CuPy when estimator device is CUDA.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
dict
|
|
334
|
+
Contains ``pvalues``, ``statistic``, ``pvalue``,
|
|
335
|
+
``method``, ``axis``, and ``backend``.
|
|
336
|
+
"""
|
|
337
|
+
from statgpu.inference import combine_pvalues as _combine_pvalues
|
|
338
|
+
|
|
339
|
+
source = pvalues
|
|
340
|
+
if source is None:
|
|
341
|
+
source = getattr(self, "_pvalues", None)
|
|
342
|
+
if source is None:
|
|
343
|
+
raise RuntimeError(
|
|
344
|
+
"No p-values available. Fit with inference enabled or pass pvalues explicitly."
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
backend_name = str(backend).strip().lower()
|
|
348
|
+
if backend_name == "auto" and self._get_compute_device() == Device.CUDA:
|
|
349
|
+
backend_name = "cupy"
|
|
350
|
+
|
|
351
|
+
if backend_name == "cupy":
|
|
352
|
+
pvals = self._to_array(source, Device.CUDA)
|
|
353
|
+
w_cast = None if weights is None else self._to_array(weights, Device.CUDA)
|
|
354
|
+
elif backend_name == "numpy":
|
|
355
|
+
pvals = self._to_numpy(source)
|
|
356
|
+
w_cast = None if weights is None else self._to_numpy(weights)
|
|
357
|
+
else:
|
|
358
|
+
pvals = source
|
|
359
|
+
w_cast = weights
|
|
360
|
+
|
|
361
|
+
statistic, pvalue = _combine_pvalues(
|
|
362
|
+
pvals,
|
|
363
|
+
method=method,
|
|
364
|
+
weights=w_cast,
|
|
365
|
+
axis=axis,
|
|
366
|
+
backend=backend_name,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"method": method,
|
|
371
|
+
"axis": axis,
|
|
372
|
+
"backend": backend_name,
|
|
373
|
+
"pvalues": pvals,
|
|
374
|
+
"weights": w_cast,
|
|
375
|
+
"statistic": statistic,
|
|
376
|
+
"pvalue": pvalue,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def bootstrap_statistic(
|
|
380
|
+
self,
|
|
381
|
+
statistic,
|
|
382
|
+
*arrays,
|
|
383
|
+
n_resamples: int = 200,
|
|
384
|
+
strategy: str = "iid",
|
|
385
|
+
strata=None,
|
|
386
|
+
clusters=None,
|
|
387
|
+
block_size: Optional[int] = None,
|
|
388
|
+
confidence_level: float = 0.95,
|
|
389
|
+
random_state: Optional[int] = None,
|
|
390
|
+
statistic_name: str = "statistic",
|
|
391
|
+
backend: str = "auto",
|
|
392
|
+
):
|
|
393
|
+
"""
|
|
394
|
+
Run unified bootstrap engine from model context.
|
|
395
|
+
|
|
396
|
+
This is a thin wrapper over ``statgpu.inference.bootstrap_statistic``.
|
|
397
|
+
"""
|
|
398
|
+
from statgpu.inference import bootstrap_statistic as _bootstrap_statistic
|
|
399
|
+
|
|
400
|
+
arrays_use = arrays
|
|
401
|
+
if len(arrays_use) == 0:
|
|
402
|
+
X_cache = getattr(self, "_X_design", None)
|
|
403
|
+
y_cache = getattr(self, "_y", None)
|
|
404
|
+
if X_cache is None or y_cache is None:
|
|
405
|
+
raise RuntimeError(
|
|
406
|
+
"No cached training arrays available. Pass arrays explicitly or fit first."
|
|
407
|
+
)
|
|
408
|
+
arrays_use = (X_cache, y_cache)
|
|
409
|
+
|
|
410
|
+
backend_name = str(backend).strip().lower()
|
|
411
|
+
if backend_name == "auto" and self._get_compute_device() == Device.CUDA:
|
|
412
|
+
backend_name = "cupy"
|
|
413
|
+
|
|
414
|
+
if backend_name == "cupy":
|
|
415
|
+
arrays_cast = tuple(self._to_array(a, Device.CUDA) for a in arrays_use)
|
|
416
|
+
strata_cast = None if strata is None else self._to_array(strata, Device.CUDA)
|
|
417
|
+
clusters_cast = None if clusters is None else self._to_array(clusters, Device.CUDA)
|
|
418
|
+
elif backend_name == "numpy":
|
|
419
|
+
arrays_cast = tuple(self._to_numpy(a) for a in arrays_use)
|
|
420
|
+
strata_cast = None if strata is None else self._to_numpy(strata)
|
|
421
|
+
clusters_cast = None if clusters is None else self._to_numpy(clusters)
|
|
422
|
+
else:
|
|
423
|
+
arrays_cast = arrays_use
|
|
424
|
+
strata_cast = strata
|
|
425
|
+
clusters_cast = clusters
|
|
426
|
+
|
|
427
|
+
return _bootstrap_statistic(
|
|
428
|
+
statistic,
|
|
429
|
+
*arrays_cast,
|
|
430
|
+
n_resamples=n_resamples,
|
|
431
|
+
strategy=strategy,
|
|
432
|
+
strata=strata_cast,
|
|
433
|
+
clusters=clusters_cast,
|
|
434
|
+
block_size=block_size,
|
|
435
|
+
confidence_level=confidence_level,
|
|
436
|
+
random_state=random_state,
|
|
437
|
+
statistic_name=statistic_name,
|
|
438
|
+
backend=backend_name,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def permutation_test(
|
|
442
|
+
self,
|
|
443
|
+
statistic,
|
|
444
|
+
X,
|
|
445
|
+
y,
|
|
446
|
+
n_resamples: int = 1000,
|
|
447
|
+
strategy: str = "iid",
|
|
448
|
+
strata=None,
|
|
449
|
+
groups=None,
|
|
450
|
+
alternative: str = "two-sided",
|
|
451
|
+
random_state: Optional[int] = None,
|
|
452
|
+
statistic_name: str = "statistic",
|
|
453
|
+
backend: str = "auto",
|
|
454
|
+
):
|
|
455
|
+
"""
|
|
456
|
+
Run unified permutation test engine from model context.
|
|
457
|
+
|
|
458
|
+
This is a thin wrapper over ``statgpu.inference.permutation_test``.
|
|
459
|
+
"""
|
|
460
|
+
from statgpu.inference import permutation_test as _permutation_test
|
|
461
|
+
|
|
462
|
+
backend_name = str(backend).strip().lower()
|
|
463
|
+
if backend_name == "auto" and self._get_compute_device() == Device.CUDA:
|
|
464
|
+
backend_name = "cupy"
|
|
465
|
+
|
|
466
|
+
if backend_name == "cupy":
|
|
467
|
+
X_cast = self._to_array(X, Device.CUDA)
|
|
468
|
+
y_cast = self._to_array(y, Device.CUDA)
|
|
469
|
+
strata_cast = None if strata is None else self._to_array(strata, Device.CUDA)
|
|
470
|
+
groups_cast = None if groups is None else self._to_array(groups, Device.CUDA)
|
|
471
|
+
elif backend_name == "numpy":
|
|
472
|
+
X_cast = self._to_numpy(X)
|
|
473
|
+
y_cast = self._to_numpy(y)
|
|
474
|
+
strata_cast = None if strata is None else self._to_numpy(strata)
|
|
475
|
+
groups_cast = None if groups is None else self._to_numpy(groups)
|
|
476
|
+
else:
|
|
477
|
+
X_cast = X
|
|
478
|
+
y_cast = y
|
|
479
|
+
strata_cast = strata
|
|
480
|
+
groups_cast = groups
|
|
481
|
+
|
|
482
|
+
return _permutation_test(
|
|
483
|
+
statistic,
|
|
484
|
+
X_cast,
|
|
485
|
+
y_cast,
|
|
486
|
+
n_resamples=n_resamples,
|
|
487
|
+
strategy=strategy,
|
|
488
|
+
strata=strata_cast,
|
|
489
|
+
groups=groups_cast,
|
|
490
|
+
alternative=alternative,
|
|
491
|
+
random_state=random_state,
|
|
492
|
+
statistic_name=statistic_name,
|
|
493
|
+
backend=backend_name,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
@abstractmethod
|
|
497
|
+
def fit(self, X, y=None, **fit_params):
|
|
498
|
+
"""Fit the estimator."""
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
@abstractmethod
|
|
502
|
+
def predict(self, X):
|
|
503
|
+
"""Make predictions."""
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
def _check_is_fitted(self):
|
|
507
|
+
"""Check if estimator has been fitted."""
|
|
508
|
+
if not self._fitted:
|
|
509
|
+
raise RuntimeError(
|
|
510
|
+
f"This {self.__class__.__name__} instance is not fitted yet. "
|
|
511
|
+
"Call 'fit' before using this method."
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def get_params(self, deep=True):
|
|
515
|
+
"""Get parameters for this estimator.
|
|
516
|
+
|
|
517
|
+
Only returns parameters accepted by this class's own ``__init__``,
|
|
518
|
+
not parent class parameters. This matches sklearn's contract where
|
|
519
|
+
``clone(est).__init__(**est.get_params())`` must work.
|
|
520
|
+
"""
|
|
521
|
+
import inspect
|
|
522
|
+
params = {}
|
|
523
|
+
# Only look at the most specific __init__ (this class, not parents)
|
|
524
|
+
try:
|
|
525
|
+
sig = inspect.signature(type(self).__init__)
|
|
526
|
+
except (ValueError, TypeError):
|
|
527
|
+
return params
|
|
528
|
+
for name in sig.parameters:
|
|
529
|
+
if name == "self":
|
|
530
|
+
continue
|
|
531
|
+
if hasattr(self, name):
|
|
532
|
+
params[name] = getattr(self, name)
|
|
533
|
+
elif hasattr(self, f'_{name}'):
|
|
534
|
+
params[name] = getattr(self, f'_{name}')
|
|
535
|
+
return params
|
|
536
|
+
|
|
537
|
+
def set_params(self, **params):
|
|
538
|
+
"""Set parameters for this estimator."""
|
|
539
|
+
for key, value in params.items():
|
|
540
|
+
if key == 'device':
|
|
541
|
+
self.device = Device(value) if isinstance(value, str) else value
|
|
542
|
+
else:
|
|
543
|
+
setattr(self, key, value)
|
|
544
|
+
return self
|
statgpu/_config.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device configuration and GPU detection.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Device(Enum):
|
|
12
|
+
"""Device types for computation."""
|
|
13
|
+
CPU = "cpu"
|
|
14
|
+
CUDA = "cuda"
|
|
15
|
+
TORCH = "torch"
|
|
16
|
+
AUTO = "auto"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _DeviceManager:
|
|
20
|
+
"""Internal device state manager."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._current_device = Device.AUTO
|
|
24
|
+
self._cupy_available = None
|
|
25
|
+
self._torch_available = None
|
|
26
|
+
self._cuda_available = None
|
|
27
|
+
|
|
28
|
+
def _check_cupy(self) -> bool:
|
|
29
|
+
"""Check if CuPy is available and working."""
|
|
30
|
+
if self._cupy_available is None:
|
|
31
|
+
try:
|
|
32
|
+
import cupy as cp
|
|
33
|
+
# Test actual CUDA functionality
|
|
34
|
+
cp.cuda.Device(0).use()
|
|
35
|
+
self._cupy_available = True
|
|
36
|
+
except Exception:
|
|
37
|
+
self._cupy_available = False
|
|
38
|
+
return self._cupy_available
|
|
39
|
+
|
|
40
|
+
def _check_torch(self) -> bool:
|
|
41
|
+
"""Check if PyTorch CUDA is available."""
|
|
42
|
+
if self._torch_available is None:
|
|
43
|
+
try:
|
|
44
|
+
import torch
|
|
45
|
+
self._torch_available = torch.cuda.is_available()
|
|
46
|
+
except Exception:
|
|
47
|
+
self._torch_available = False
|
|
48
|
+
return self._torch_available
|
|
49
|
+
|
|
50
|
+
def cuda_available(self) -> bool:
|
|
51
|
+
"""Check if any CUDA backend is available (CuPy or Torch)."""
|
|
52
|
+
if self._cuda_available is None:
|
|
53
|
+
self._cuda_available = self._check_cupy() or self._check_torch()
|
|
54
|
+
return self._cuda_available
|
|
55
|
+
|
|
56
|
+
def get_device(self) -> Device:
|
|
57
|
+
"""Get current device setting."""
|
|
58
|
+
if self._current_device == Device.AUTO:
|
|
59
|
+
if self.cuda_available():
|
|
60
|
+
# Prefer CuPy if both are available for backward compatibility
|
|
61
|
+
return Device.CUDA if self._check_cupy() else Device.TORCH
|
|
62
|
+
return Device.CPU
|
|
63
|
+
return self._current_device
|
|
64
|
+
|
|
65
|
+
def set_device(self, device: Union[str, Device]) -> None:
|
|
66
|
+
"""Set device for computation."""
|
|
67
|
+
if isinstance(device, str):
|
|
68
|
+
device = Device(device.lower())
|
|
69
|
+
|
|
70
|
+
if device in (Device.CUDA, Device.TORCH) and not self.cuda_available():
|
|
71
|
+
warnings.warn(
|
|
72
|
+
"CUDA requested but not available. statgpu keeps the explicit "
|
|
73
|
+
"device setting and model execution will raise unless a matching "
|
|
74
|
+
"GPU backend is installed; use device='auto' for automatic CPU selection.",
|
|
75
|
+
RuntimeWarning
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self._current_device = device
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Global device manager instance
|
|
82
|
+
_device_manager = _DeviceManager()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_device() -> Device:
|
|
86
|
+
"""
|
|
87
|
+
Get the current computation device.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
Device
|
|
92
|
+
Current resolved device. If the configured device is ``'auto'``, this
|
|
93
|
+
resolves to CuPy CUDA when available, then Torch CUDA, then CPU.
|
|
94
|
+
"""
|
|
95
|
+
return _device_manager.get_device()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def set_device(device: Union[str, Device]) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Set the computation device.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
device : str or Device
|
|
105
|
+
Device to use: ``'cpu'``, ``'cuda'``, ``'torch'``, or ``'auto'``.
|
|
106
|
+
``'cuda'`` and ``'torch'`` are explicit GPU requests and are kept as
|
|
107
|
+
configured; model execution raises if the matching backend is not
|
|
108
|
+
available. ``'auto'`` chooses CuPy CUDA when available, then Torch CUDA,
|
|
109
|
+
then CPU.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
>>> import statgpu as sg
|
|
114
|
+
>>> sg.set_device('cuda') # Force CuPy CUDA
|
|
115
|
+
>>> sg.set_device('torch') # Force Torch CUDA
|
|
116
|
+
>>> sg.set_device('auto') # Auto-detect
|
|
117
|
+
"""
|
|
118
|
+
_device_manager.set_device(device)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cuda_available() -> bool:
|
|
122
|
+
"""Check if CUDA is available."""
|
|
123
|
+
return _device_manager.cuda_available()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Convenience imports
|
|
127
|
+
__all__ = ['Device', 'get_device', 'set_device', 'cuda_available']
|