histoweave-spatial 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 (133) hide show
  1. histoweave/__init__.py +67 -0
  2. histoweave/__main__.py +8 -0
  3. histoweave/_math.py +186 -0
  4. histoweave/automl/__init__.py +36 -0
  5. histoweave/automl/compiler.py +710 -0
  6. histoweave/automl/report.py +290 -0
  7. histoweave/benchmark/__init__.py +481 -0
  8. histoweave/benchmark/active_calibration.py +401 -0
  9. histoweave/benchmark/causal.py +529 -0
  10. histoweave/benchmark/complexity.py +40 -0
  11. histoweave/benchmark/decision.py +674 -0
  12. histoweave/benchmark/digital_twin.py +260 -0
  13. histoweave/benchmark/digital_twin_report.py +217 -0
  14. histoweave/benchmark/donor_bootstrap.py +262 -0
  15. histoweave/benchmark/failure_boundary.py +990 -0
  16. histoweave/benchmark/failure_fingerprint.py +596 -0
  17. histoweave/benchmark/features.py +449 -0
  18. histoweave/benchmark/figure3.py +554 -0
  19. histoweave/benchmark/harness.py +482 -0
  20. histoweave/benchmark/independent_personalisation.py +996 -0
  21. histoweave/benchmark/isus.py +1195 -0
  22. histoweave/benchmark/k_selection.py +1073 -0
  23. histoweave/benchmark/landscape.py +592 -0
  24. histoweave/benchmark/landscape_io.py +436 -0
  25. histoweave/benchmark/multiple_testing.py +158 -0
  26. histoweave/benchmark/pareto.py +505 -0
  27. histoweave/benchmark/pareto_io.py +243 -0
  28. histoweave/benchmark/phenomenology_contracts.py +406 -0
  29. histoweave/benchmark/phenomenology_metrics.py +555 -0
  30. histoweave/benchmark/phenomenology_runner.py +580 -0
  31. histoweave/benchmark/phenomenology_statistics.py +293 -0
  32. histoweave/benchmark/phenomenology_suite.py +238 -0
  33. histoweave/benchmark/protocol_endpoints.py +1258 -0
  34. histoweave/benchmark/real_data.py +433 -0
  35. histoweave/benchmark/recommend.py +871 -0
  36. histoweave/benchmark/scaling.py +301 -0
  37. histoweave/benchmark/sota_pipeline.py +646 -0
  38. histoweave/benchmark/stats_review.py +382 -0
  39. histoweave/benchmark/study_grouped.py +322 -0
  40. histoweave/benchmark/task_contract.py +519 -0
  41. histoweave/benchmark/uncertainty.py +230 -0
  42. histoweave/cli.py +2664 -0
  43. histoweave/compiler/__init__.py +208 -0
  44. histoweave/compiler/catalog.py +28 -0
  45. histoweave/compiler/executor.py +173 -0
  46. histoweave/compiler/gaps.py +29 -0
  47. histoweave/compiler/llm.py +86 -0
  48. histoweave/compiler/prompts.py +78 -0
  49. histoweave/compiler/schema.py +320 -0
  50. histoweave/compiler/serialization.py +167 -0
  51. histoweave/compiler/templates.py +195 -0
  52. histoweave/compiler/validate.py +88 -0
  53. histoweave/data/__init__.py +7 -0
  54. histoweave/data/model.py +651 -0
  55. histoweave/datasets/__init__.py +94 -0
  56. histoweave/datasets/digital_twin.py +574 -0
  57. histoweave/datasets/domain_mappings.json +159 -0
  58. histoweave/datasets/histology.py +471 -0
  59. histoweave/datasets/pathology_domains.py +184 -0
  60. histoweave/datasets/phenomenology.py +625 -0
  61. histoweave/datasets/real.py +942 -0
  62. histoweave/datasets/scale_contract.py +150 -0
  63. histoweave/datasets/synthetic.py +739 -0
  64. histoweave/datasets/vendor.py +179 -0
  65. histoweave/decision.py +146 -0
  66. histoweave/federation/__init__.py +135 -0
  67. histoweave/federation/cli_fed.py +544 -0
  68. histoweave/federation/consensus.py +348 -0
  69. histoweave/federation/landscape_bridge.py +162 -0
  70. histoweave/federation/registry.py +290 -0
  71. histoweave/federation/schema.py +538 -0
  72. histoweave/federation/signing.py +355 -0
  73. histoweave/federation/store.py +264 -0
  74. histoweave/io/__init__.py +31 -0
  75. histoweave/io/_tenx.py +164 -0
  76. histoweave/io/base.py +36 -0
  77. histoweave/io/bundle.py +510 -0
  78. histoweave/io/readers.py +308 -0
  79. histoweave/logging.py +174 -0
  80. histoweave/plugins/__init__.py +59 -0
  81. histoweave/plugins/builtin/__init__.py +49 -0
  82. histoweave/plugins/builtin/_markers.py +111 -0
  83. histoweave/plugins/builtin/_r_base.py +169 -0
  84. histoweave/plugins/builtin/_sklearn_base.py +280 -0
  85. histoweave/plugins/builtin/_sota_common.py +113 -0
  86. histoweave/plugins/builtin/_validation.py +82 -0
  87. histoweave/plugins/builtin/annotate.py +51 -0
  88. histoweave/plugins/builtin/banksy.py +110 -0
  89. histoweave/plugins/builtin/banksy_py.py +145 -0
  90. histoweave/plugins/builtin/cell2location.py +241 -0
  91. histoweave/plugins/builtin/cellpose2.py +183 -0
  92. histoweave/plugins/builtin/celltypist.py +146 -0
  93. histoweave/plugins/builtin/deconv.py +71 -0
  94. histoweave/plugins/builtin/deep_learning.py +335 -0
  95. histoweave/plugins/builtin/domains.py +72 -0
  96. histoweave/plugins/builtin/extended_native.py +280 -0
  97. histoweave/plugins/builtin/ingestion.py +237 -0
  98. histoweave/plugins/builtin/integration.py +389 -0
  99. histoweave/plugins/builtin/liana_plus.py +133 -0
  100. histoweave/plugins/builtin/nnsvg.py +96 -0
  101. histoweave/plugins/builtin/normalize.py +71 -0
  102. histoweave/plugins/builtin/qc.py +100 -0
  103. histoweave/plugins/builtin/r_demo.py +74 -0
  104. histoweave/plugins/builtin/real_methods.py +8 -0
  105. histoweave/plugins/builtin/release_manifest.py +409 -0
  106. histoweave/plugins/builtin/research_context.py +734 -0
  107. histoweave/plugins/builtin/research_preprocessing.py +443 -0
  108. histoweave/plugins/builtin/research_spatial.py +506 -0
  109. histoweave/plugins/builtin/scanvi.py +185 -0
  110. histoweave/plugins/builtin/sctransform.py +110 -0
  111. histoweave/plugins/builtin/sklearn_clustering.py +268 -0
  112. histoweave/plugins/builtin/sota_domains.py +693 -0
  113. histoweave/plugins/builtin/spatial_graph.py +126 -0
  114. histoweave/plugins/builtin/spatial_svg.py +157 -0
  115. histoweave/plugins/builtin/spatialde.py +184 -0
  116. histoweave/plugins/builtin/virtual_st.py +497 -0
  117. histoweave/plugins/coverage.py +196 -0
  118. histoweave/plugins/experimental/__init__.py +1 -0
  119. histoweave/plugins/experimental/banksy_py.py +11 -0
  120. histoweave/plugins/interfaces.py +437 -0
  121. histoweave/plugins/registry.py +383 -0
  122. histoweave/report/__init__.py +14 -0
  123. histoweave/report/report.py +208 -0
  124. histoweave/report/svg.py +178 -0
  125. histoweave/report/templates/report.html.j2 +289 -0
  126. histoweave/report/vitessce_data.py +326 -0
  127. histoweave/workflow/__init__.py +23 -0
  128. histoweave/workflow/pipeline.py +310 -0
  129. histoweave_spatial-0.1.0.dist-info/METADATA +628 -0
  130. histoweave_spatial-0.1.0.dist-info/RECORD +133 -0
  131. histoweave_spatial-0.1.0.dist-info/WHEEL +4 -0
  132. histoweave_spatial-0.1.0.dist-info/entry_points.txt +5 -0
  133. histoweave_spatial-0.1.0.dist-info/licenses/LICENSE +29 -0
histoweave/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ """HistoWeave — orchestration & evaluation for reproducible spatial transcriptomics.
2
+
3
+ HistoWeave is *not* a new method zoo. It quantifies and reduces
4
+ **method × spatial-context selection uncertainty**: a unified data substrate,
5
+ containerized pipelines, typed plugins over existing R/Python methods, task-bound
6
+ benchmarks, and a recommendation engine that reports when strong defaults beat
7
+ personalisation.
8
+
9
+ Quick start
10
+ -----------
11
+ >>> import histoweave as ts
12
+ >>> data = ts.datasets.make_synthetic(seed=0) # tiny canonical dataset
13
+ >>> result = ts.run_pipeline(data) # ingest→QC→norm→domains→annotate
14
+ >>> ts.build_report(result, "report.html") # self-contained HTML report
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ __version__ = "0.1.0"
20
+ __version_info__ = (0, 1, 0, "final", 0)
21
+
22
+ from . import datasets
23
+ from .data import Provenance, SpatialTable
24
+ from .decision import (
25
+ DecisionAction,
26
+ DecisionCard,
27
+ DecisionEngine,
28
+ DecisionPolicy,
29
+ decide,
30
+ decide_from_bundle,
31
+ )
32
+ from .plugins import MethodCategory, MethodMaturity, get_method, list_methods, register
33
+ from .report import build_report
34
+ from .workflow import (
35
+ PipelineExecutionError,
36
+ PipelineStep,
37
+ PipelineStepError,
38
+ RunManifest,
39
+ default_pipeline,
40
+ run_pipeline,
41
+ )
42
+
43
+ __all__ = [
44
+ "__version__",
45
+ "__version_info__",
46
+ "SpatialTable",
47
+ "Provenance",
48
+ "DecisionAction",
49
+ "DecisionCard",
50
+ "DecisionEngine",
51
+ "DecisionPolicy",
52
+ "decide",
53
+ "decide_from_bundle",
54
+ "MethodCategory",
55
+ "MethodMaturity",
56
+ "register",
57
+ "get_method",
58
+ "list_methods",
59
+ "PipelineStep",
60
+ "PipelineStepError",
61
+ "PipelineExecutionError",
62
+ "RunManifest",
63
+ "run_pipeline",
64
+ "default_pipeline",
65
+ "build_report",
66
+ "datasets",
67
+ ]
histoweave/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Allow ``python -m histoweave`` to invoke the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
histoweave/_math.py ADDED
@@ -0,0 +1,186 @@
1
+ """Small, dependency-free numerical helpers.
2
+
3
+ These are deliberately minimal NumPy implementations so the scaffold runs with zero
4
+ heavy dependencies. In a real deployment these steps are delegated to the wrapped
5
+ methods (scikit-learn, scanpy, RAPIDS, ...); they are isolated here behind plain
6
+ functions so that substitution is trivial.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+
14
+ def zscore(X: np.ndarray, axis: int = 0, eps: float = 1e-8) -> np.ndarray:
15
+ """Standardize to zero mean / unit variance along ``axis``."""
16
+ mean = X.mean(axis=axis, keepdims=True)
17
+ std = X.std(axis=axis, keepdims=True)
18
+ return (X - mean) / (std + eps)
19
+
20
+
21
+ def pca(X: np.ndarray, n_components: int, random_state: int = 0) -> np.ndarray:
22
+ """Truncated PCA on mean-centred data. Returns the scores matrix.
23
+
24
+ Two equivalent routes, chosen by shape:
25
+
26
+ * When features outnumber observations (the spatial-omics norm — thousands of genes,
27
+ fewer spots), eigendecompose the small ``n x n`` Gram matrix. This yields the same
28
+ scores as the SVD (up to each component's arbitrary sign) without ever forming the
29
+ ``n x d`` right singular vectors that the plain SVD computes and we immediately
30
+ discard — the difference between seconds and a minute on a real Visium slide.
31
+ * Otherwise fall back to the economy SVD.
32
+
33
+ Both routes are deterministic for a given input.
34
+ """
35
+ n_components = int(min(n_components, min(X.shape)))
36
+ Xc = X - X.mean(axis=0, keepdims=True)
37
+ n_obs, n_features = Xc.shape
38
+
39
+ if n_features > n_obs:
40
+ gram = Xc @ Xc.T # (n_obs, n_obs) — small when genes >> spots
41
+ eigvals, eigvecs = np.linalg.eigh(gram) # ascending order
42
+ top = np.argsort(eigvals)[::-1][:n_components]
43
+ singular_values = np.sqrt(np.clip(eigvals[top], 0.0, None))
44
+ return eigvecs[:, top] * singular_values
45
+
46
+ U, S, _ = np.linalg.svd(Xc, full_matrices=False)
47
+ return U[:, :n_components] * S[:n_components]
48
+
49
+
50
+ def kmeans(
51
+ X: np.ndarray,
52
+ k: int,
53
+ n_iter: int = 100,
54
+ n_init: int = 4,
55
+ random_state: int = 0,
56
+ ) -> np.ndarray:
57
+ """Lloyd's algorithm with k-means++ seeding. Returns integer cluster labels.
58
+
59
+ Runs ``n_init`` restarts and keeps the lowest-inertia solution, so results are
60
+ deterministic for a fixed ``random_state``.
61
+ """
62
+ k = int(min(k, X.shape[0]))
63
+ best_labels: np.ndarray | None = None
64
+ best_inertia = np.inf
65
+ for init in range(n_init):
66
+ rng = np.random.default_rng(random_state + init)
67
+ centers = _kmeanspp_init(X, k, rng)
68
+ labels = np.zeros(X.shape[0], dtype=int)
69
+ for _ in range(n_iter):
70
+ dists = _sqdist(X, centers)
71
+ new_labels = dists.argmin(axis=1)
72
+ if np.array_equal(new_labels, labels):
73
+ labels = new_labels
74
+ break
75
+ labels = new_labels
76
+ for c in range(k):
77
+ members = X[labels == c]
78
+ if len(members):
79
+ centers[c] = members.mean(axis=0)
80
+ inertia = _sqdist(X, centers)[np.arange(X.shape[0]), labels].sum()
81
+ if inertia < best_inertia:
82
+ best_inertia = inertia
83
+ best_labels = labels
84
+ assert best_labels is not None
85
+ return best_labels
86
+
87
+
88
+ def _sqdist(X: np.ndarray, centers: np.ndarray) -> np.ndarray:
89
+ return ((X[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2)
90
+
91
+
92
+ def _kmeanspp_init(X: np.ndarray, k: int, rng: np.random.Generator) -> np.ndarray:
93
+ n = X.shape[0]
94
+ centers = np.empty((k, X.shape[1]), dtype=float)
95
+ centers[0] = X[rng.integers(n)]
96
+ closest = ((X - centers[0]) ** 2).sum(axis=1)
97
+ for c in range(1, k):
98
+ probs = closest / closest.sum() if closest.sum() > 0 else np.full(n, 1 / n)
99
+ centers[c] = X[rng.choice(n, p=probs)]
100
+ closest = np.minimum(closest, ((X - centers[c]) ** 2).sum(axis=1))
101
+ return centers
102
+
103
+
104
+ def knn_indices(coords: np.ndarray, k: int) -> np.ndarray:
105
+ """Indices of the ``k`` nearest neighbours (by Euclidean distance) per point.
106
+
107
+ Uses :class:`scipy.spatial.cKDTree` (``O(n log n)`` build / query) so spatial
108
+ neighbourhood steps remain usable beyond a few thousand observations. Falls
109
+ back to a dense distance matrix only when SciPy is unavailable or ``n`` is
110
+ tiny enough that the dense path is cheaper.
111
+ """
112
+ coords = np.asarray(coords, dtype=float)
113
+ if coords.ndim != 2:
114
+ raise ValueError(f"coords must be 2-D, got shape {coords.shape}")
115
+ n = int(coords.shape[0])
116
+ if n == 0:
117
+ return np.empty((0, 0), dtype=int)
118
+ k = int(min(max(k, 1), n))
119
+ if k == n and n <= 64:
120
+ # Dense path is fine for very small tables (and keeps unit tests simple).
121
+ d = np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis=2)
122
+ return np.argsort(d, axis=1, kind="stable")[:, :k]
123
+ try:
124
+ from scipy.spatial import cKDTree
125
+ except ImportError: # pragma: no cover - scipy is a core dependency
126
+ d = np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis=2)
127
+ return np.argsort(d, axis=1, kind="stable")[:, :k]
128
+
129
+ tree = cKDTree(coords)
130
+ # workers=-1 uses all cores when SciPy was built with OpenMP; safe no-op otherwise.
131
+ try:
132
+ _dist, indices = tree.query(coords, k=k, workers=-1)
133
+ except TypeError: # older SciPy without workers=
134
+ _dist, indices = tree.query(coords, k=k)
135
+ indices = np.asarray(indices, dtype=int)
136
+ if k == 1:
137
+ indices = indices.reshape(n, 1)
138
+ return indices
139
+
140
+
141
+ def neighborhood_mean(features: np.ndarray, coords: np.ndarray, k: int) -> np.ndarray:
142
+ """Average each point's features over its ``k`` spatial neighbours.
143
+
144
+ This is the ingredient that makes clustering *spatially aware* (the idea behind
145
+ BANKSY / neighbourhood-augmented domain detection).
146
+ """
147
+ idx = knn_indices(coords, k)
148
+ return features[idx].mean(axis=1)
149
+
150
+
151
+ def adjusted_rand_index(labels_true: np.ndarray, labels_pred: np.ndarray) -> float:
152
+ """Adjusted Rand Index between two labelings (clustering-agreement metric)."""
153
+ labels_true = np.asarray(labels_true)
154
+ labels_pred = np.asarray(labels_pred)
155
+ classes, class_idx = np.unique(labels_true, return_inverse=True)
156
+ clusters, cluster_idx = np.unique(labels_pred, return_inverse=True)
157
+ contingency = np.zeros((classes.size, clusters.size), dtype=np.int64)
158
+ np.add.at(contingency, (class_idx, cluster_idx), 1)
159
+
160
+ def comb2(x: np.ndarray | np.int64) -> np.ndarray | np.int64:
161
+ return x * (x - 1) // 2
162
+
163
+ sum_comb_c = comb2(contingency.sum(axis=1)).sum()
164
+ sum_comb_k = comb2(contingency.sum(axis=0)).sum()
165
+ sum_comb = comb2(contingency).sum()
166
+ n = labels_true.shape[0]
167
+ total = comb2(np.int64(n))
168
+ if total == 0:
169
+ return 1.0
170
+ expected = sum_comb_c * sum_comb_k / total
171
+ max_index = (sum_comb_c + sum_comb_k) / 2
172
+ if max_index == expected:
173
+ return 1.0
174
+ return float((sum_comb - expected) / (max_index - expected))
175
+
176
+
177
+ def proportions_rmsd(true: np.ndarray, pred: np.ndarray) -> float:
178
+ """Root-mean-square deviation between two proportion matrices (rows sum to 1).
179
+
180
+ Lower is better; 0 = identical, max = √2 (completely disjoint supports).
181
+ """
182
+ true_arr = np.asarray(true, dtype=float)
183
+ pred_arr = np.asarray(pred, dtype=float)
184
+ if true_arr.shape != pred_arr.shape:
185
+ raise ValueError(f"shape mismatch: true {true_arr.shape} vs pred {pred_arr.shape}")
186
+ return float(np.sqrt(np.mean((true_arr - pred_arr) ** 2)))
@@ -0,0 +1,36 @@
1
+ """Spatial AutoML compiler — landscape recommendation + multi-method execution.
2
+
3
+ Combines the natural-language compiler (:mod:`histoweave.compiler`) with the
4
+ landscape recommender (:class:`~histoweave.benchmark.recommend.MethodRecommender`)
5
+ into an automated loop:
6
+
7
+ 1. Extract target-free dataset features.
8
+ 2. Retrieve nearest reference datasets from a knowledge base.
9
+ 3. Run the recommended top-*k* methods on the user sample.
10
+ 4. Compare results with spatial-coherence / consensus proxies.
11
+ 5. Rank methods on a Pareto front and emit a full HTML report.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .compiler import (
17
+ AUTOML_SCHEMA_VERSION,
18
+ AutoMLResult,
19
+ MethodRunResult,
20
+ ParetoPoint,
21
+ compute_pareto_front,
22
+ run_spatial_automl,
23
+ write_automl_artifacts,
24
+ )
25
+ from .report import build_automl_report
26
+
27
+ __all__ = [
28
+ "AUTOML_SCHEMA_VERSION",
29
+ "AutoMLResult",
30
+ "MethodRunResult",
31
+ "ParetoPoint",
32
+ "build_automl_report",
33
+ "compute_pareto_front",
34
+ "run_spatial_automl",
35
+ "write_automl_artifacts",
36
+ ]