quantized-lab 0.8.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 (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
@@ -0,0 +1,54 @@
1
+ """Thin FFT spectral route. Wraps ``calc.spectral.fft_spectral`` for the ROI
2
+ gadget family's FFT mode (gap #34): a single-record magnitude/PSD/phase
3
+ spectrum of one region's rows. The "complex" output type never crosses the
4
+ wire (numpy complex isn't JSON-serializable — see CLAUDE.md's jsonencode
5
+ notes); only magnitude/psd/phase are exposed here. Validate -> call ->
6
+ serialize; no business logic here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ from fastapi import APIRouter, HTTPException
15
+ from pydantic import BaseModel
16
+
17
+ from quantized.calc.spectral import fft_spectral
18
+ from quantized.routes._payload import to_jsonable
19
+
20
+ router = APIRouter(prefix="/api/spectral", tags=["spectral"])
21
+
22
+ # "complex" is deliberately excluded (see module docstring).
23
+ _OUTPUT_TYPES = ("psd", "magnitude", "phase")
24
+
25
+
26
+ class FftRequest(BaseModel):
27
+ x: list[float]
28
+ y: list[float]
29
+ window: str = "hanning"
30
+ output_type: str = "magnitude"
31
+ sided: str = "one"
32
+ detrend: str = "mean"
33
+
34
+
35
+ @router.post("/fft")
36
+ def fft(req: FftRequest) -> dict[str, Any]:
37
+ """Single-record FFT spectrum (magnitude/psd/phase) of (x, y)."""
38
+ if req.output_type not in _OUTPUT_TYPES:
39
+ raise HTTPException(status_code=422, detail=f"unsupported output_type: {req.output_type}")
40
+ try:
41
+ result = fft_spectral(
42
+ np.asarray(req.x, dtype=float),
43
+ np.asarray(req.y, dtype=float),
44
+ window=req.window,
45
+ output_type=req.output_type,
46
+ sided=req.sided,
47
+ detrend=req.detrend,
48
+ )
49
+ except ValueError as exc:
50
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
51
+ # Drop the window-function array: same length as the input, internal detail
52
+ # the caller never plots (keeps the response small).
53
+ out = {k: v for k, v in result.items() if k != "window"}
54
+ return to_jsonable(out) # type: ignore[no-any-return]
@@ -0,0 +1,99 @@
1
+ """Thin routes for statistical-plot primitives (ORIGIN_GAP_PLAN #16).
2
+
3
+ All math lives in ``quantized.calc.statplots``; these adapters only validate,
4
+ call, and serialize. The same payloads feed the interactive stage and the
5
+ matplotlib export path.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ from fastapi import APIRouter, HTTPException
14
+ from pydantic import BaseModel
15
+
16
+ from quantized.calc.statplots import (
17
+ grouped_box_stats,
18
+ histogram,
19
+ qq_plot,
20
+ violin_kde,
21
+ )
22
+ from quantized.routes._payload import to_jsonable
23
+
24
+ router = APIRouter(prefix="/api/statplots", tags=["statplots"])
25
+
26
+
27
+ def _wrap(result: dict[str, Any]) -> dict[str, Any]:
28
+ return to_jsonable(result) # type: ignore[no-any-return]
29
+
30
+
31
+ class BoxRequest(BaseModel):
32
+ groups: list[list[float]]
33
+ labels: list[str] | None = None
34
+ whis: float | str = 1.5
35
+
36
+
37
+ @router.post("/box")
38
+ def box_route(req: BoxRequest) -> dict[str, Any]:
39
+ """Box/whisker stats for one or more groups (matplotlib-compatible)."""
40
+ try:
41
+ return _wrap(grouped_box_stats(req.groups, labels=req.labels, whis=req.whis))
42
+ except (ValueError, IndexError) as exc:
43
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
44
+
45
+
46
+ class ViolinRequest(BaseModel):
47
+ data: list[float]
48
+ bw_method: str | float = "scott"
49
+ n_points: int = 128
50
+ cut: float = 2.0
51
+
52
+
53
+ @router.post("/violin")
54
+ def violin_route(req: ViolinRequest) -> dict[str, Any]:
55
+ """Gaussian-KDE density for a violin plot."""
56
+ try:
57
+ return _wrap(
58
+ violin_kde(
59
+ np.asarray(req.data, dtype=float),
60
+ bw_method=req.bw_method, n_points=req.n_points, cut=req.cut,
61
+ )
62
+ )
63
+ except (ValueError, IndexError) as exc:
64
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
65
+
66
+
67
+ class QQRequest(BaseModel):
68
+ data: list[float]
69
+ dist: str = "norm"
70
+
71
+
72
+ @router.post("/qq")
73
+ def qq_route(req: QQRequest) -> dict[str, Any]:
74
+ """Quantile-quantile / probability-plot coordinates against a distribution."""
75
+ try:
76
+ return _wrap(qq_plot(np.asarray(req.data, dtype=float), dist=req.dist))
77
+ except (ValueError, IndexError) as exc:
78
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
79
+
80
+
81
+ class HistogramRequest(BaseModel):
82
+ data: list[float]
83
+ bins: str | int = "fd"
84
+ density: bool = False
85
+ fit: str | None = None
86
+
87
+
88
+ @router.post("/histogram")
89
+ def histogram_route(req: HistogramRequest) -> dict[str, Any]:
90
+ """Histogram with a data-driven bin rule and an optional fit overlay."""
91
+ try:
92
+ return _wrap(
93
+ histogram(
94
+ np.asarray(req.data, dtype=float),
95
+ bins=req.bins, density=req.density, fit=req.fit,
96
+ )
97
+ )
98
+ except (ValueError, IndexError) as exc:
99
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
@@ -0,0 +1,418 @@
1
+ """Thin statistics routes. Wraps ``calc.stats`` (no toolbox, golden vs MATLAB).
2
+
3
+ Each handler validates, converts to ndarray, calls the pure function, and
4
+ serializes the (array-bearing) result dict via ``to_jsonable``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel
14
+
15
+ from quantized.calc.stats import (
16
+ anova1,
17
+ descriptive_stats,
18
+ lin_regress,
19
+ pca_analysis,
20
+ t_test,
21
+ )
22
+ from quantized.calc.stats_dist import (
23
+ fit_distribution,
24
+ fit_distributions,
25
+ required_n,
26
+ t_test_power,
27
+ )
28
+ from quantized.calc.stats_multivar import (
29
+ correlation_matrix,
30
+ multiple_regression,
31
+ partial_correlation,
32
+ stepwise_regression,
33
+ )
34
+ from quantized.calc.stats_tests import (
35
+ anderson_darling,
36
+ friedman,
37
+ kruskal_wallis,
38
+ ks_normal,
39
+ ks_two_sample,
40
+ levene,
41
+ mann_whitney,
42
+ shapiro_wilk,
43
+ sign_test,
44
+ wilcoxon_signed_rank,
45
+ )
46
+ from quantized.routes._payload import to_jsonable
47
+
48
+ router = APIRouter(prefix="/api/stats", tags=["stats"])
49
+
50
+
51
+ class DescriptiveRequest(BaseModel):
52
+ x: list[float]
53
+
54
+
55
+ class RegressionRequest(BaseModel):
56
+ x: list[float]
57
+ y: list[float]
58
+ order: int = 1
59
+ alpha: float = 0.05
60
+
61
+
62
+ class TTestRequest(BaseModel):
63
+ x: list[float]
64
+ y: list[float] | None = None
65
+ mu: float = 0.0
66
+ paired: bool = False
67
+ alpha: float = 0.05
68
+ tail: str = "both"
69
+
70
+
71
+ class AnovaRequest(BaseModel):
72
+ groups: list[list[float]]
73
+ alpha: float = 0.05
74
+
75
+
76
+ class PCARequest(BaseModel):
77
+ data: list[list[float]]
78
+ center: bool = True
79
+ scale: bool = False
80
+ num_components: int = 0
81
+
82
+
83
+ class TwoSampleRequest(BaseModel):
84
+ x: list[float]
85
+ y: list[float]
86
+ alternative: str = "two-sided"
87
+
88
+
89
+ class PairedOrOneSampleRequest(BaseModel):
90
+ x: list[float]
91
+ y: list[float] | None = None
92
+ mu: float = 0.0
93
+ alternative: str = "two-sided"
94
+
95
+
96
+ class GroupsRequest(BaseModel):
97
+ groups: list[list[float]]
98
+
99
+
100
+ class LeveneRequest(BaseModel):
101
+ groups: list[list[float]]
102
+ center: str = "median"
103
+
104
+
105
+ class OneSampleRequest(BaseModel):
106
+ x: list[float]
107
+
108
+
109
+ class KSNormalRequest(BaseModel):
110
+ x: list[float]
111
+ loc: float | None = None
112
+ scale: float | None = None
113
+
114
+
115
+ class MultiRegressionRequest(BaseModel):
116
+ predictors: list[list[float]] # k same-length columns
117
+ y: list[float]
118
+ alpha: float = 0.05
119
+
120
+
121
+ class CorrelationRequest(BaseModel):
122
+ columns: list[list[float]]
123
+ method: str = "pearson"
124
+
125
+
126
+ class PartialCorrelationRequest(BaseModel):
127
+ columns: list[list[float]]
128
+
129
+
130
+ def _wrap(result: dict[str, Any]) -> dict[str, Any]:
131
+ return to_jsonable(result) # type: ignore[no-any-return]
132
+
133
+
134
+ @router.post("/descriptive")
135
+ def descriptive(req: DescriptiveRequest) -> dict[str, Any]:
136
+ """Descriptive statistics of a 1-D array (NaNs dropped)."""
137
+ try:
138
+ return _wrap(descriptive_stats(np.asarray(req.x, dtype=float)))
139
+ except (ValueError, IndexError) as exc:
140
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
141
+
142
+
143
+ @router.post("/regression")
144
+ def regression(req: RegressionRequest) -> dict[str, Any]:
145
+ """Polynomial least-squares regression with inference."""
146
+ try:
147
+ return _wrap(
148
+ lin_regress(
149
+ np.asarray(req.x, dtype=float),
150
+ np.asarray(req.y, dtype=float),
151
+ order=req.order,
152
+ alpha=req.alpha,
153
+ )
154
+ )
155
+ except (ValueError, IndexError) as exc:
156
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
157
+
158
+
159
+ @router.post("/ttest")
160
+ def ttest(req: TTestRequest) -> dict[str, Any]:
161
+ """Student's t-test (one-sample / paired / Welch two-sample)."""
162
+ try:
163
+ y = np.asarray(req.y, dtype=float) if req.y is not None else None
164
+ return _wrap(
165
+ t_test(
166
+ np.asarray(req.x, dtype=float),
167
+ y,
168
+ mu=req.mu,
169
+ paired=req.paired,
170
+ alpha=req.alpha,
171
+ tail=req.tail,
172
+ )
173
+ )
174
+ except (ValueError, IndexError) as exc:
175
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
176
+
177
+
178
+ @router.post("/anova")
179
+ def anova(req: AnovaRequest) -> dict[str, Any]:
180
+ """One-way ANOVA on a list of group vectors."""
181
+ try:
182
+ groups = [np.asarray(g, dtype=float) for g in req.groups]
183
+ return _wrap(anova1(groups, alpha=req.alpha))
184
+ except (ValueError, IndexError) as exc:
185
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
186
+
187
+
188
+ @router.post("/pca")
189
+ def pca(req: PCARequest) -> dict[str, Any]:
190
+ """Principal component analysis via SVD."""
191
+ try:
192
+ return _wrap(
193
+ pca_analysis(
194
+ np.asarray(req.data, dtype=float),
195
+ center=req.center,
196
+ scale=req.scale,
197
+ num_components=req.num_components,
198
+ )
199
+ )
200
+ except (ValueError, IndexError) as exc:
201
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
202
+
203
+
204
+ @router.post("/mann-whitney")
205
+ def mann_whitney_route(req: TwoSampleRequest) -> dict[str, Any]:
206
+ """Mann-Whitney U test (independent two-sample rank test)."""
207
+ try:
208
+ return _wrap(
209
+ mann_whitney(
210
+ np.asarray(req.x, dtype=float),
211
+ np.asarray(req.y, dtype=float),
212
+ alternative=req.alternative,
213
+ )
214
+ )
215
+ except (ValueError, IndexError) as exc:
216
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
217
+
218
+
219
+ @router.post("/wilcoxon")
220
+ def wilcoxon_route(req: PairedOrOneSampleRequest) -> dict[str, Any]:
221
+ """Wilcoxon signed-rank test (paired, or one-sample vs mu)."""
222
+ try:
223
+ y = np.asarray(req.y, dtype=float) if req.y is not None else None
224
+ return _wrap(
225
+ wilcoxon_signed_rank(
226
+ np.asarray(req.x, dtype=float), y, mu=req.mu, alternative=req.alternative
227
+ )
228
+ )
229
+ except (ValueError, IndexError) as exc:
230
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
231
+
232
+
233
+ @router.post("/kruskal")
234
+ def kruskal_route(req: GroupsRequest) -> dict[str, Any]:
235
+ """Kruskal-Wallis H test on a list of group vectors."""
236
+ try:
237
+ return _wrap(kruskal_wallis([np.asarray(g, dtype=float) for g in req.groups]))
238
+ except (ValueError, IndexError) as exc:
239
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
240
+
241
+
242
+ @router.post("/friedman")
243
+ def friedman_route(req: GroupsRequest) -> dict[str, Any]:
244
+ """Friedman test (k treatments x n blocks, equal lengths)."""
245
+ try:
246
+ return _wrap(friedman([np.asarray(g, dtype=float) for g in req.groups]))
247
+ except (ValueError, IndexError) as exc:
248
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
249
+
250
+
251
+ @router.post("/sign-test")
252
+ def sign_test_route(req: PairedOrOneSampleRequest) -> dict[str, Any]:
253
+ """Sign test (paired, or one-sample vs mu) via exact binomial."""
254
+ try:
255
+ y = np.asarray(req.y, dtype=float) if req.y is not None else None
256
+ return _wrap(
257
+ sign_test(np.asarray(req.x, dtype=float), y, mu=req.mu, alternative=req.alternative)
258
+ )
259
+ except (ValueError, IndexError) as exc:
260
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
261
+
262
+
263
+ @router.post("/shapiro")
264
+ def shapiro_route(req: OneSampleRequest) -> dict[str, Any]:
265
+ """Shapiro-Wilk normality test."""
266
+ try:
267
+ return _wrap(shapiro_wilk(np.asarray(req.x, dtype=float)))
268
+ except (ValueError, IndexError) as exc:
269
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
270
+
271
+
272
+ @router.post("/anderson")
273
+ def anderson_route(req: OneSampleRequest) -> dict[str, Any]:
274
+ """Anderson-Darling normality test (critical-value table, no p)."""
275
+ try:
276
+ return _wrap(anderson_darling(np.asarray(req.x, dtype=float)))
277
+ except (ValueError, IndexError) as exc:
278
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
279
+
280
+
281
+ @router.post("/levene")
282
+ def levene_route(req: LeveneRequest) -> dict[str, Any]:
283
+ """Levene / Brown-Forsythe equal-variance test."""
284
+ try:
285
+ return _wrap(
286
+ levene([np.asarray(g, dtype=float) for g in req.groups], center=req.center)
287
+ )
288
+ except (ValueError, IndexError) as exc:
289
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
290
+
291
+
292
+ @router.post("/ks-normal")
293
+ def ks_normal_route(req: KSNormalRequest) -> dict[str, Any]:
294
+ """One-sample KS test vs a normal distribution."""
295
+ try:
296
+ return _wrap(ks_normal(np.asarray(req.x, dtype=float), loc=req.loc, scale=req.scale))
297
+ except (ValueError, IndexError) as exc:
298
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
299
+
300
+
301
+ @router.post("/regression-multi")
302
+ def regression_multi(req: MultiRegressionRequest) -> dict[str, Any]:
303
+ """Multiple linear regression (intercept + k predictors) with inference."""
304
+ try:
305
+ return _wrap(
306
+ multiple_regression(
307
+ [np.asarray(c, dtype=float) for c in req.predictors],
308
+ np.asarray(req.y, dtype=float),
309
+ alpha=req.alpha,
310
+ )
311
+ )
312
+ except (ValueError, IndexError) as exc:
313
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
314
+
315
+
316
+ @router.post("/correlation")
317
+ def correlation(req: CorrelationRequest) -> dict[str, Any]:
318
+ """Pairwise Pearson/Spearman correlation matrix with p-values."""
319
+ try:
320
+ return _wrap(
321
+ correlation_matrix(
322
+ [np.asarray(c, dtype=float) for c in req.columns], method=req.method
323
+ )
324
+ )
325
+ except (ValueError, IndexError) as exc:
326
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
327
+
328
+
329
+ @router.post("/partial-correlation")
330
+ def partial_correlation_route(req: PartialCorrelationRequest) -> dict[str, Any]:
331
+ """Partial correlation of every pair controlling for all other columns."""
332
+ try:
333
+ return _wrap(partial_correlation([np.asarray(c, dtype=float) for c in req.columns]))
334
+ except (ValueError, IndexError) as exc:
335
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
336
+
337
+
338
+ @router.post("/ks-two-sample")
339
+ def ks_two_sample_route(req: TwoSampleRequest) -> dict[str, Any]:
340
+ """Two-sample KS test."""
341
+ try:
342
+ return _wrap(
343
+ ks_two_sample(
344
+ np.asarray(req.x, dtype=float),
345
+ np.asarray(req.y, dtype=float),
346
+ alternative=req.alternative,
347
+ )
348
+ )
349
+ except (ValueError, IndexError) as exc:
350
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
351
+
352
+
353
+ class FitDistRequest(BaseModel):
354
+ x: list[float]
355
+ dist: str | None = None # None = fit all families, rank by AIC
356
+
357
+
358
+ @router.post("/fit-distribution")
359
+ def fit_distribution_route(req: FitDistRequest) -> dict[str, Any]:
360
+ """MLE distribution fit(s) with KS GOF; dist=None ranks all families."""
361
+ try:
362
+ x = np.asarray(req.x, dtype=float)
363
+ if req.dist is None:
364
+ return _wrap(fit_distributions(x))
365
+ return _wrap(fit_distribution(x, req.dist))
366
+ except (ValueError, IndexError) as exc:
367
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
368
+
369
+
370
+ class PowerRequest(BaseModel):
371
+ effect_size: float
372
+ n: int | None = None # given -> power; omitted -> solve for n
373
+ power: float = 0.8
374
+ kind: str = "two-sample"
375
+ alpha: float = 0.05
376
+ tails: int = 2
377
+
378
+
379
+ @router.post("/power")
380
+ def power_route(req: PowerRequest) -> dict[str, Any]:
381
+ """t-test power at n, or the required n for a target power (exact nct)."""
382
+ try:
383
+ if req.n is not None:
384
+ return _wrap(
385
+ t_test_power(
386
+ req.effect_size, req.n, kind=req.kind, alpha=req.alpha, tails=req.tails
387
+ )
388
+ )
389
+ return _wrap(
390
+ required_n(
391
+ req.effect_size, req.power, kind=req.kind, alpha=req.alpha, tails=req.tails
392
+ )
393
+ )
394
+ except (ValueError, IndexError) as exc:
395
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
396
+
397
+
398
+ class StepwiseRequest(BaseModel):
399
+ predictors: list[list[float]]
400
+ y: list[float]
401
+ criterion: str = "aic"
402
+ direction: str = "forward"
403
+
404
+
405
+ @router.post("/stepwise")
406
+ def stepwise_route(req: StepwiseRequest) -> dict[str, Any]:
407
+ """Stepwise predictor selection (AIC/BIC) over multiple regression."""
408
+ try:
409
+ return _wrap(
410
+ stepwise_regression(
411
+ [np.asarray(c, dtype=float) for c in req.predictors],
412
+ np.asarray(req.y, dtype=float),
413
+ criterion=req.criterion,
414
+ direction=req.direction,
415
+ )
416
+ )
417
+ except (ValueError, IndexError) as exc:
418
+ raise HTTPException(status_code=422, detail=str(exc)) from exc