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.
Files changed (168) hide show
  1. statgpu/__init__.py +174 -0
  2. statgpu/_base.py +544 -0
  3. statgpu/_config.py +127 -0
  4. statgpu/anova/__init__.py +5 -0
  5. statgpu/anova/_oneway.py +194 -0
  6. statgpu/backends/__init__.py +83 -0
  7. statgpu/backends/_array_ops.py +529 -0
  8. statgpu/backends/_base.py +184 -0
  9. statgpu/backends/_cupy.py +453 -0
  10. statgpu/backends/_factory.py +65 -0
  11. statgpu/backends/_gpu_inference_cupy.py +214 -0
  12. statgpu/backends/_gpu_inference_torch.py +422 -0
  13. statgpu/backends/_numpy.py +324 -0
  14. statgpu/backends/_torch.py +685 -0
  15. statgpu/backends/_torch_safe.py +47 -0
  16. statgpu/backends/_utils.py +423 -0
  17. statgpu/core/__init__.py +10 -0
  18. statgpu/core/formula/__init__.py +33 -0
  19. statgpu/core/formula/_design.py +99 -0
  20. statgpu/core/formula/_parser.py +191 -0
  21. statgpu/core/formula/_terms.py +70 -0
  22. statgpu/core/formula/tests/__init__.py +0 -0
  23. statgpu/core/formula/tests/test_parser.py +194 -0
  24. statgpu/covariance/__init__.py +6 -0
  25. statgpu/covariance/_empirical.py +310 -0
  26. statgpu/covariance/_shrinkage.py +248 -0
  27. statgpu/cross_validation/__init__.py +31 -0
  28. statgpu/cross_validation/_base.py +410 -0
  29. statgpu/cross_validation/_engine.py +167 -0
  30. statgpu/diagnostics/__init__.py +7 -0
  31. statgpu/diagnostics/_regression_diagnostics.py +188 -0
  32. statgpu/feature_selection/__init__.py +24 -0
  33. statgpu/feature_selection/_knockoff.py +870 -0
  34. statgpu/feature_selection/_knockoff_utils.py +1003 -0
  35. statgpu/feature_selection/_stepwise.py +300 -0
  36. statgpu/glm_core/__init__.py +81 -0
  37. statgpu/glm_core/_base.py +202 -0
  38. statgpu/glm_core/_family.py +362 -0
  39. statgpu/glm_core/_fused.py +149 -0
  40. statgpu/glm_core/_gamma.py +111 -0
  41. statgpu/glm_core/_inverse_gaussian.py +62 -0
  42. statgpu/glm_core/_irls.py +561 -0
  43. statgpu/glm_core/_logistic.py +82 -0
  44. statgpu/glm_core/_negative_binomial.py +68 -0
  45. statgpu/glm_core/_poisson.py +60 -0
  46. statgpu/glm_core/_solver_legacy.py +100 -0
  47. statgpu/glm_core/_squared.py +53 -0
  48. statgpu/glm_core/_tweedie.py +74 -0
  49. statgpu/inference/__init__.py +239 -0
  50. statgpu/inference/_distributions_backend.py +2610 -0
  51. statgpu/inference/_multiple_testing.py +391 -0
  52. statgpu/inference/_resampling.py +1400 -0
  53. statgpu/inference/_results.py +265 -0
  54. statgpu/linear_model/__init__.py +75 -0
  55. statgpu/linear_model/_gaussian_inference.py +306 -0
  56. statgpu/linear_model/_glm_base.py +1261 -0
  57. statgpu/linear_model/_ordered_logit.py +52 -0
  58. statgpu/linear_model/_ordered_probit.py +50 -0
  59. statgpu/linear_model/_stats.py +170 -0
  60. statgpu/linear_model/cv/__init__.py +13 -0
  61. statgpu/linear_model/cv/_elasticnet_cv.py +892 -0
  62. statgpu/linear_model/cv/_lasso_cv.py +253 -0
  63. statgpu/linear_model/cv/_logistic_cv.py +895 -0
  64. statgpu/linear_model/cv/_ridge_cv.py +1160 -0
  65. statgpu/linear_model/legacy/__init__.py +1 -0
  66. statgpu/linear_model/legacy/_distributions_legacy_gpu.py +340 -0
  67. statgpu/linear_model/legacy/_elasticnet_legacy.py +936 -0
  68. statgpu/linear_model/legacy/_lasso_legacy.py +4876 -0
  69. statgpu/linear_model/legacy/_penalized_legacy.py +1174 -0
  70. statgpu/linear_model/legacy/_ridge_legacy.py +863 -0
  71. statgpu/linear_model/legacy/_solver_legacy.py +104 -0
  72. statgpu/linear_model/penalized/__init__.py +25 -0
  73. statgpu/linear_model/penalized/_base.py +437 -0
  74. statgpu/linear_model/penalized/_fit_mixin.py +1877 -0
  75. statgpu/linear_model/penalized/_inference_mixin.py +1179 -0
  76. statgpu/linear_model/penalized/_penalized_cv.py +2699 -0
  77. statgpu/linear_model/penalized/_penalized_gamma.py +86 -0
  78. statgpu/linear_model/penalized/_penalized_inverse_gaussian.py +62 -0
  79. statgpu/linear_model/penalized/_penalized_linear.py +236 -0
  80. statgpu/linear_model/penalized/_penalized_logistic.py +100 -0
  81. statgpu/linear_model/penalized/_penalized_negative_binomial.py +65 -0
  82. statgpu/linear_model/penalized/_penalized_poisson.py +62 -0
  83. statgpu/linear_model/penalized/_penalized_tweedie.py +65 -0
  84. statgpu/linear_model/penalized/_predict_mixin.py +182 -0
  85. statgpu/linear_model/wrappers/__init__.py +31 -0
  86. statgpu/linear_model/wrappers/_adaptive_lasso.py +63 -0
  87. statgpu/linear_model/wrappers/_elasticnet.py +75 -0
  88. statgpu/linear_model/wrappers/_gamma.py +67 -0
  89. statgpu/linear_model/wrappers/_inverse_gaussian.py +47 -0
  90. statgpu/linear_model/wrappers/_lasso.py +2124 -0
  91. statgpu/linear_model/wrappers/_linear.py +1127 -0
  92. statgpu/linear_model/wrappers/_logistic.py +1435 -0
  93. statgpu/linear_model/wrappers/_mcp.py +58 -0
  94. statgpu/linear_model/wrappers/_negative_binomial.py +58 -0
  95. statgpu/linear_model/wrappers/_poisson.py +48 -0
  96. statgpu/linear_model/wrappers/_ridge.py +166 -0
  97. statgpu/linear_model/wrappers/_scad.py +58 -0
  98. statgpu/linear_model/wrappers/_tweedie.py +57 -0
  99. statgpu/metrics/__init__.py +21 -0
  100. statgpu/metrics/_classification.py +591 -0
  101. statgpu/nonparametric/__init__.py +50 -0
  102. statgpu/nonparametric/kernel_methods/__init__.py +25 -0
  103. statgpu/nonparametric/kernel_methods/_kernels.py +246 -0
  104. statgpu/nonparametric/kernel_methods/_krr.py +234 -0
  105. statgpu/nonparametric/kernel_methods/_krr_cv.py +380 -0
  106. statgpu/nonparametric/kernel_smoothing/__init__.py +39 -0
  107. statgpu/nonparametric/kernel_smoothing/_bandwidth_selection.py +1083 -0
  108. statgpu/nonparametric/kernel_smoothing/_kde.py +761 -0
  109. statgpu/nonparametric/kernel_smoothing/_kernel_common.py +348 -0
  110. statgpu/nonparametric/kernel_smoothing/_kernel_regression.py +748 -0
  111. statgpu/nonparametric/splines/__init__.py +5 -0
  112. statgpu/nonparametric/splines/_bspline_basis.py +336 -0
  113. statgpu/nonparametric/splines/_penalized.py +349 -0
  114. statgpu/panel/__init__.py +19 -0
  115. statgpu/panel/_covariance.py +140 -0
  116. statgpu/panel/_fixed_effects.py +420 -0
  117. statgpu/panel/_random_effects.py +385 -0
  118. statgpu/panel/_utils.py +482 -0
  119. statgpu/penalties/__init__.py +139 -0
  120. statgpu/penalties/_adaptive_l1.py +313 -0
  121. statgpu/penalties/_base.py +261 -0
  122. statgpu/penalties/_categories.py +39 -0
  123. statgpu/penalties/_elasticnet.py +98 -0
  124. statgpu/penalties/_group_lasso.py +678 -0
  125. statgpu/penalties/_group_mcp.py +553 -0
  126. statgpu/penalties/_group_scad.py +605 -0
  127. statgpu/penalties/_l1.py +107 -0
  128. statgpu/penalties/_l2.py +77 -0
  129. statgpu/penalties/_mcp.py +237 -0
  130. statgpu/penalties/_scad.py +260 -0
  131. statgpu/semiparametric/__init__.py +5 -0
  132. statgpu/semiparametric/_gam.py +401 -0
  133. statgpu/solvers/__init__.py +24 -0
  134. statgpu/solvers/_admm.py +241 -0
  135. statgpu/solvers/_constants.py +15 -0
  136. statgpu/solvers/_convergence.py +6 -0
  137. statgpu/solvers/_fista.py +436 -0
  138. statgpu/solvers/_fista_bb.py +513 -0
  139. statgpu/solvers/_fista_lla.py +541 -0
  140. statgpu/solvers/_lbfgs.py +206 -0
  141. statgpu/solvers/_newton.py +149 -0
  142. statgpu/solvers/_utils.py +277 -0
  143. statgpu/survival/__init__.py +14 -0
  144. statgpu/survival/_cox.py +3974 -0
  145. statgpu/survival/_cox_breslow_triton_kernel.py +106 -0
  146. statgpu/survival/_cox_cv.py +1159 -0
  147. statgpu/survival/_cox_efron_cuda.py +1280 -0
  148. statgpu/survival/_cox_efron_triton.py +359 -0
  149. statgpu/unsupervised/__init__.py +29 -0
  150. statgpu/unsupervised/_agglomerative.py +307 -0
  151. statgpu/unsupervised/_dbscan.py +263 -0
  152. statgpu/unsupervised/_dbscan_cpu.pyx +125 -0
  153. statgpu/unsupervised/_gmm.py +332 -0
  154. statgpu/unsupervised/_incremental_pca.py +176 -0
  155. statgpu/unsupervised/_kmeans.py +261 -0
  156. statgpu/unsupervised/_minibatch_kmeans.py +299 -0
  157. statgpu/unsupervised/_minibatch_nmf.py +252 -0
  158. statgpu/unsupervised/_nmf.py +190 -0
  159. statgpu/unsupervised/_pca.py +189 -0
  160. statgpu/unsupervised/_truncated_svd.py +132 -0
  161. statgpu/unsupervised/_tsne.py +192 -0
  162. statgpu/unsupervised/_umap.py +224 -0
  163. statgpu/unsupervised/_utils.py +134 -0
  164. statgpu-0.1.0.dist-info/METADATA +245 -0
  165. statgpu-0.1.0.dist-info/RECORD +168 -0
  166. statgpu-0.1.0.dist-info/WHEEL +5 -0
  167. statgpu-0.1.0.dist-info/licenses/LICENSE +199 -0
  168. 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']
@@ -0,0 +1,5 @@
1
+ """One-way ANOVA with GPU acceleration."""
2
+
3
+ from ._oneway import f_oneway, AnovaResult
4
+
5
+ __all__ = ['f_oneway', 'AnovaResult']