dfc-kit 1.0.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.
- dfc_kit-1.0.0.dist-info/METADATA +184 -0
- dfc_kit-1.0.0.dist-info/RECORD +71 -0
- dfc_kit-1.0.0.dist-info/WHEEL +5 -0
- dfc_kit-1.0.0.dist-info/entry_points.txt +2 -0
- dfc_kit-1.0.0.dist-info/licenses/LICENSE +29 -0
- dfc_kit-1.0.0.dist-info/top_level.txt +1 -0
- dfckit/__init__.py +11 -0
- dfckit/_arrays.py +13 -0
- dfckit/_preprocessing.py +46 -0
- dfckit/_validation.py +87 -0
- dfckit/artifacts/__init__.py +36 -0
- dfckit/artifacts/_fields.py +67 -0
- dfckit/artifacts/_json.py +92 -0
- dfckit/artifacts/_numpy.py +55 -0
- dfckit/artifacts/models.py +624 -0
- dfckit/artifacts/state_alignment.py +106 -0
- dfckit/artifacts/state_results.py +353 -0
- dfckit/artifacts/state_scoring.py +509 -0
- dfckit/artifacts/state_stability.py +207 -0
- dfckit/cli.py +105 -0
- dfckit/commands/__init__.py +1 -0
- dfckit/commands/parser.py +354 -0
- dfckit/commands/reporting.py +146 -0
- dfckit/commands/source.py +245 -0
- dfckit/commands/stability.py +288 -0
- dfckit/commands/states.py +266 -0
- dfckit/connectivity/__init__.py +68 -0
- dfckit/connectivity/_edge_products.py +130 -0
- dfckit/connectivity/correlation.py +61 -0
- dfckit/connectivity/instantaneous.py +253 -0
- dfckit/connectivity/leida.py +204 -0
- dfckit/connectivity/lowrank.py +384 -0
- dfckit/connectivity/partition.py +291 -0
- dfckit/connectivity/windows.py +83 -0
- dfckit/data.py +229 -0
- dfckit/inference/__init__.py +55 -0
- dfckit/inference/endpoints.py +194 -0
- dfckit/inference/hc3.py +207 -0
- dfckit/inference/matching.py +300 -0
- dfckit/inference/multiple_testing.py +52 -0
- dfckit/inference/nbs.py +586 -0
- dfckit/inference/paired.py +157 -0
- dfckit/inference/state_metrics.py +277 -0
- dfckit/information/__init__.py +45 -0
- dfckit/information/_artifact.py +440 -0
- dfckit/information/estimators.py +536 -0
- dfckit/information/fixed.py +660 -0
- dfckit/information/summary.py +89 -0
- dfckit/io/__init__.py +23 -0
- dfckit/io/xcpd.py +470 -0
- dfckit/reference.py +350 -0
- dfckit/segments.py +44 -0
- dfckit/states/__init__.py +97 -0
- dfckit/states/alignment.py +364 -0
- dfckit/states/cap.py +62 -0
- dfckit/states/cross_validation.py +114 -0
- dfckit/states/data.py +387 -0
- dfckit/states/hmm.py +387 -0
- dfckit/states/interpretation.py +273 -0
- dfckit/states/kmeans.py +340 -0
- dfckit/states/metrics.py +94 -0
- dfckit/states/scoring.py +114 -0
- dfckit/states/selection.py +516 -0
- dfckit/states/stability.py +183 -0
- dfckit/states/streaming.py +859 -0
- dfckit/states/streaming_hmm.py +436 -0
- dfckit/storage/__init__.py +29 -0
- dfckit/storage/_statistics.py +80 -0
- dfckit/storage/builders.py +330 -0
- dfckit/storage/store.py +672 -0
- dfckit/storage/summary.py +140 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Command handlers for fitting, decoding, summarizing, and scoring state models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ..artifacts import (
|
|
8
|
+
StatePredictions,
|
|
9
|
+
load_fitted_model,
|
|
10
|
+
load_state_predictions,
|
|
11
|
+
save_fitted_model,
|
|
12
|
+
save_state_predictions,
|
|
13
|
+
write_state_metrics,
|
|
14
|
+
write_state_model_scores,
|
|
15
|
+
)
|
|
16
|
+
from ..artifacts.state_scoring import _state_model_specification
|
|
17
|
+
from ..states.hmm import GaussianHMMStateModel
|
|
18
|
+
from ..states.kmeans import KMeansStateModel
|
|
19
|
+
from ..states.metrics import summarize_state_assignments
|
|
20
|
+
from ..states.streaming import (
|
|
21
|
+
fit_kmeans_store_materialized,
|
|
22
|
+
fit_minibatch_kmeans_store,
|
|
23
|
+
predict_kmeans_store,
|
|
24
|
+
score_kmeans_store,
|
|
25
|
+
)
|
|
26
|
+
from ..states.streaming_hmm import (
|
|
27
|
+
fit_gaussian_hmm_store,
|
|
28
|
+
predict_gaussian_hmm_store,
|
|
29
|
+
score_gaussian_hmm_store,
|
|
30
|
+
)
|
|
31
|
+
from ..storage import FeatureStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fit_states(namespace: argparse.Namespace) -> dict[str, object]:
|
|
35
|
+
"""Fit one state model from a disk-backed feature store."""
|
|
36
|
+
store = FeatureStore.open(namespace.store)
|
|
37
|
+
if namespace.output.exists() or namespace.output.is_symlink():
|
|
38
|
+
raise FileExistsError(f"model artifact path already exists: {namespace.output}")
|
|
39
|
+
subjects = None if namespace.subject is None else tuple(namespace.subject)
|
|
40
|
+
if subjects is not None:
|
|
41
|
+
missing = sorted(set(subjects) - set(store.subjects))
|
|
42
|
+
if missing:
|
|
43
|
+
raise ValueError(f"requested fit subjects are absent from the store: {missing}")
|
|
44
|
+
|
|
45
|
+
if namespace.method == "kmeans":
|
|
46
|
+
if namespace.fitting_mode == "streaming":
|
|
47
|
+
if namespace.algorithm != "minibatch":
|
|
48
|
+
raise ValueError("streaming KMeans requires --algorithm minibatch")
|
|
49
|
+
fit = fit_minibatch_kmeans_store(
|
|
50
|
+
store,
|
|
51
|
+
n_states=namespace.n_states,
|
|
52
|
+
seed=namespace.seed,
|
|
53
|
+
n_init=10 if namespace.n_init is None else namespace.n_init,
|
|
54
|
+
max_iter=namespace.max_iter,
|
|
55
|
+
batch_size=namespace.batch_size,
|
|
56
|
+
standardize_features=namespace.standardize_features,
|
|
57
|
+
reassignment_ratio=namespace.reassignment_ratio,
|
|
58
|
+
init_sample_size=namespace.init_sample_size,
|
|
59
|
+
n_pca_components=namespace.n_pca_components,
|
|
60
|
+
pca_batch_size=namespace.pca_batch_size,
|
|
61
|
+
subjects=subjects,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
if namespace.init_sample_size is not None:
|
|
65
|
+
raise ValueError("materialized KMeans does not use --init-sample-size")
|
|
66
|
+
fit = fit_kmeans_store_materialized(
|
|
67
|
+
store,
|
|
68
|
+
n_states=namespace.n_states,
|
|
69
|
+
seed=namespace.seed,
|
|
70
|
+
n_init=10 if namespace.n_init is None else namespace.n_init,
|
|
71
|
+
max_iter=namespace.max_iter,
|
|
72
|
+
algorithm=namespace.algorithm,
|
|
73
|
+
batch_size=namespace.batch_size,
|
|
74
|
+
standardize_features=namespace.standardize_features,
|
|
75
|
+
reassignment_ratio=namespace.reassignment_ratio,
|
|
76
|
+
n_pca_components=namespace.n_pca_components,
|
|
77
|
+
subjects=subjects,
|
|
78
|
+
)
|
|
79
|
+
model = fit.model
|
|
80
|
+
fit_sequence_count = len(fit.assignments.sequences)
|
|
81
|
+
artifact = save_fitted_model(model, namespace.output)
|
|
82
|
+
return {
|
|
83
|
+
"model_kind": "kmeans-state",
|
|
84
|
+
"method": namespace.method,
|
|
85
|
+
"fitting_mode": namespace.fitting_mode,
|
|
86
|
+
"output": str(artifact),
|
|
87
|
+
"fit_subjects": list(model.fit_subjects),
|
|
88
|
+
"fit_sample_count": model.fit_sample_count,
|
|
89
|
+
"fit_sequence_count": fit_sequence_count,
|
|
90
|
+
"n_states": model.n_states,
|
|
91
|
+
"seed": model.seed,
|
|
92
|
+
"converged": None,
|
|
93
|
+
"log_likelihood": None,
|
|
94
|
+
"inertia": model.inertia,
|
|
95
|
+
"init_sample_size": model.init_sample_size,
|
|
96
|
+
"n_pca_components": model.n_pca_components,
|
|
97
|
+
"pca_batch_size": model.pca_batch_size,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if namespace.n_pca_components is None:
|
|
101
|
+
raise ValueError("--n-pca-components is required when --method hmm")
|
|
102
|
+
fit = fit_gaussian_hmm_store(
|
|
103
|
+
store,
|
|
104
|
+
n_states=namespace.n_states,
|
|
105
|
+
seed=namespace.seed,
|
|
106
|
+
n_pca_components=namespace.n_pca_components,
|
|
107
|
+
covariance_type=namespace.covariance_type,
|
|
108
|
+
n_init=1 if namespace.n_init is None else namespace.n_init,
|
|
109
|
+
n_iter=namespace.n_iter,
|
|
110
|
+
tol=namespace.tol,
|
|
111
|
+
pca_batch_size=namespace.pca_batch_size,
|
|
112
|
+
subjects=subjects,
|
|
113
|
+
minimum_sequence_length=namespace.minimum_sequence_length,
|
|
114
|
+
)
|
|
115
|
+
model = fit.model
|
|
116
|
+
artifact = save_fitted_model(model, namespace.output)
|
|
117
|
+
return {
|
|
118
|
+
"model_kind": "gaussian-hmm-state",
|
|
119
|
+
"method": namespace.method,
|
|
120
|
+
"output": str(artifact),
|
|
121
|
+
"fit_subjects": list(model.fit_subjects),
|
|
122
|
+
"fit_sample_count": model.fit_sample_count,
|
|
123
|
+
"fit_sequence_count": model.fit_sequence_count,
|
|
124
|
+
"n_states": model.n_states,
|
|
125
|
+
"seed": model.seed,
|
|
126
|
+
"converged": model.converged,
|
|
127
|
+
"log_likelihood": model.log_likelihood,
|
|
128
|
+
"n_pca_components": model.n_pca_components,
|
|
129
|
+
"omitted_short_sequence_count": model.omitted_short_sequence_count,
|
|
130
|
+
"pca_batch_size": model.pca_batch_size,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def selected_store_subjects(
|
|
135
|
+
store: FeatureStore,
|
|
136
|
+
requested: list[str] | None,
|
|
137
|
+
) -> tuple[str, ...] | None:
|
|
138
|
+
"""Validate and retain an optional store-subject selection."""
|
|
139
|
+
if requested is None:
|
|
140
|
+
return None
|
|
141
|
+
missing = sorted(set(requested) - set(store.subjects))
|
|
142
|
+
if missing:
|
|
143
|
+
raise ValueError(f"requested prediction subjects are absent from the store: {missing}")
|
|
144
|
+
return tuple(requested)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def predict_states(namespace: argparse.Namespace) -> dict[str, object]:
|
|
148
|
+
"""Decode a feature store with a fitted state model."""
|
|
149
|
+
store = FeatureStore.open(namespace.store)
|
|
150
|
+
model = load_fitted_model(namespace.model)
|
|
151
|
+
subjects = selected_store_subjects(store, namespace.subject)
|
|
152
|
+
if isinstance(model, KMeansStateModel):
|
|
153
|
+
assignments = predict_kmeans_store(
|
|
154
|
+
model,
|
|
155
|
+
store,
|
|
156
|
+
subjects=subjects,
|
|
157
|
+
allow_fit_subjects=namespace.allow_fit_subjects,
|
|
158
|
+
)
|
|
159
|
+
predictions = StatePredictions(
|
|
160
|
+
assignments=assignments,
|
|
161
|
+
model_kind="kmeans-state",
|
|
162
|
+
model_seed=model.seed,
|
|
163
|
+
)
|
|
164
|
+
elif isinstance(model, GaussianHMMStateModel):
|
|
165
|
+
result = predict_gaussian_hmm_store(
|
|
166
|
+
model,
|
|
167
|
+
store,
|
|
168
|
+
subjects=subjects,
|
|
169
|
+
allow_fit_subjects=namespace.allow_fit_subjects,
|
|
170
|
+
)
|
|
171
|
+
predictions = StatePredictions(
|
|
172
|
+
assignments=result.assignments,
|
|
173
|
+
model_kind="gaussian-hmm-state",
|
|
174
|
+
model_seed=model.seed,
|
|
175
|
+
posterior_probabilities=result.posterior_probabilities,
|
|
176
|
+
log_likelihood=result.log_likelihood,
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
raise TypeError("predict-states requires a KMeans or Gaussian HMM model artifact")
|
|
180
|
+
artifact = save_state_predictions(predictions, namespace.output)
|
|
181
|
+
return {
|
|
182
|
+
"model_kind": predictions.model_kind,
|
|
183
|
+
"output": str(artifact),
|
|
184
|
+
"subjects": list(predictions.subjects),
|
|
185
|
+
"n_sequences": predictions.n_sequences,
|
|
186
|
+
"n_samples": predictions.n_samples,
|
|
187
|
+
"allow_fit_subjects": bool(namespace.allow_fit_subjects),
|
|
188
|
+
"log_likelihood": predictions.log_likelihood,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def summarize_states(namespace: argparse.Namespace) -> dict[str, object]:
|
|
193
|
+
"""Write acquisition-level state metrics."""
|
|
194
|
+
predictions = load_state_predictions(namespace.predictions)
|
|
195
|
+
output = write_state_metrics(predictions, namespace.output)
|
|
196
|
+
metrics = summarize_state_assignments(predictions.assignments)
|
|
197
|
+
return {
|
|
198
|
+
"output": str(output),
|
|
199
|
+
"model_kind": predictions.model_kind,
|
|
200
|
+
"n_states": predictions.assignments.n_states,
|
|
201
|
+
"n_runs": len(metrics),
|
|
202
|
+
"n_sequences": predictions.n_sequences,
|
|
203
|
+
"n_samples": predictions.n_samples,
|
|
204
|
+
"subjects": list(predictions.subjects),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def score_states(namespace: argparse.Namespace) -> dict[str, object]:
|
|
209
|
+
"""Score a fitted state model on selected acquisitions."""
|
|
210
|
+
if namespace.output.exists() or namespace.output.is_symlink():
|
|
211
|
+
raise FileExistsError(f"state-model score output path already exists: {namespace.output}")
|
|
212
|
+
store = FeatureStore.open(namespace.store)
|
|
213
|
+
model = load_fitted_model(namespace.model)
|
|
214
|
+
subjects = selected_store_subjects(store, namespace.subject)
|
|
215
|
+
if isinstance(model, KMeansStateModel):
|
|
216
|
+
scores = score_kmeans_store(
|
|
217
|
+
model,
|
|
218
|
+
store,
|
|
219
|
+
subjects=subjects,
|
|
220
|
+
allow_fit_subjects=namespace.allow_fit_subjects,
|
|
221
|
+
)
|
|
222
|
+
model_kind = "kmeans-state"
|
|
223
|
+
minimum_sequence_length = 1
|
|
224
|
+
elif isinstance(model, GaussianHMMStateModel):
|
|
225
|
+
scores = score_gaussian_hmm_store(
|
|
226
|
+
model,
|
|
227
|
+
store,
|
|
228
|
+
subjects=subjects,
|
|
229
|
+
allow_fit_subjects=namespace.allow_fit_subjects,
|
|
230
|
+
)
|
|
231
|
+
model_kind = "gaussian-hmm-state"
|
|
232
|
+
minimum_sequence_length = model.minimum_sequence_length
|
|
233
|
+
else:
|
|
234
|
+
raise TypeError("score-states requires a KMeans or Gaussian HMM model artifact")
|
|
235
|
+
scored_subjects = tuple(dict.fromkeys(score.subject for score in scores))
|
|
236
|
+
selected_subject_set = set(scored_subjects)
|
|
237
|
+
omitted_short_sequence_count = sum(
|
|
238
|
+
identity[0] in selected_subject_set and count < minimum_sequence_length
|
|
239
|
+
for identity, count in store.sequence_sample_counts
|
|
240
|
+
)
|
|
241
|
+
output = write_state_model_scores(
|
|
242
|
+
scores,
|
|
243
|
+
namespace.output,
|
|
244
|
+
model_kind=model_kind,
|
|
245
|
+
model_seed=model.seed,
|
|
246
|
+
n_states=model.n_states,
|
|
247
|
+
fit_subjects=model.fit_subjects,
|
|
248
|
+
source_contract=store.source_contract,
|
|
249
|
+
sample_interval_seconds=store.sample_interval_seconds,
|
|
250
|
+
minimum_sequence_length=minimum_sequence_length,
|
|
251
|
+
omitted_short_sequence_count=omitted_short_sequence_count,
|
|
252
|
+
allow_fit_subjects=namespace.allow_fit_subjects,
|
|
253
|
+
model_specification=_state_model_specification(model),
|
|
254
|
+
)
|
|
255
|
+
return {
|
|
256
|
+
"output": str(output),
|
|
257
|
+
"model_kind": model_kind,
|
|
258
|
+
"n_states": model.n_states,
|
|
259
|
+
"subjects": list(scored_subjects),
|
|
260
|
+
"n_runs": len(scores),
|
|
261
|
+
"n_sequences": sum(score.n_sequences for score in scores),
|
|
262
|
+
"n_samples": sum(score.n_samples for score in scores),
|
|
263
|
+
"minimum_sequence_length": minimum_sequence_length,
|
|
264
|
+
"omitted_short_sequence_count": omitted_short_sequence_count,
|
|
265
|
+
"allow_fit_subjects": bool(namespace.allow_fit_subjects),
|
|
266
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Connectivity estimators and graph summaries of connectivity matrices."""
|
|
2
|
+
|
|
3
|
+
from .correlation import edge_index, fisher_z_edges, weighted_correlation
|
|
4
|
+
from .instantaneous import (
|
|
5
|
+
ETS,
|
|
6
|
+
MTD,
|
|
7
|
+
InstantaneousEdgeResult,
|
|
8
|
+
)
|
|
9
|
+
from .leida import (
|
|
10
|
+
LEiDA,
|
|
11
|
+
LEiDAResult,
|
|
12
|
+
)
|
|
13
|
+
from .lowrank import (
|
|
14
|
+
LowRankCovariance,
|
|
15
|
+
LowRankCovarianceResult,
|
|
16
|
+
StandardizedPCA,
|
|
17
|
+
bidirectional_heldout_r2,
|
|
18
|
+
effective_rank,
|
|
19
|
+
eigen_concentration,
|
|
20
|
+
fit_standardized_pca,
|
|
21
|
+
heldout_reconstruction_r2,
|
|
22
|
+
mean_projector_basis,
|
|
23
|
+
subspace_distance,
|
|
24
|
+
subspace_similarity,
|
|
25
|
+
)
|
|
26
|
+
from .partition import (
|
|
27
|
+
FixedPartitionGraph,
|
|
28
|
+
FixedPartitionGraphResult,
|
|
29
|
+
PartitionEdgeSummary,
|
|
30
|
+
achieved_density,
|
|
31
|
+
fixed_partition_modularity,
|
|
32
|
+
participation_coefficient,
|
|
33
|
+
partition_edge_summary,
|
|
34
|
+
positive_proportional_adjacency,
|
|
35
|
+
)
|
|
36
|
+
from .windows import SlidingWindowFC, WindowFCResult
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"ETS",
|
|
40
|
+
"MTD",
|
|
41
|
+
"FixedPartitionGraph",
|
|
42
|
+
"FixedPartitionGraphResult",
|
|
43
|
+
"InstantaneousEdgeResult",
|
|
44
|
+
"LEiDA",
|
|
45
|
+
"LEiDAResult",
|
|
46
|
+
"LowRankCovariance",
|
|
47
|
+
"LowRankCovarianceResult",
|
|
48
|
+
"PartitionEdgeSummary",
|
|
49
|
+
"SlidingWindowFC",
|
|
50
|
+
"StandardizedPCA",
|
|
51
|
+
"WindowFCResult",
|
|
52
|
+
"achieved_density",
|
|
53
|
+
"bidirectional_heldout_r2",
|
|
54
|
+
"edge_index",
|
|
55
|
+
"effective_rank",
|
|
56
|
+
"eigen_concentration",
|
|
57
|
+
"fisher_z_edges",
|
|
58
|
+
"fit_standardized_pca",
|
|
59
|
+
"fixed_partition_modularity",
|
|
60
|
+
"heldout_reconstruction_r2",
|
|
61
|
+
"mean_projector_basis",
|
|
62
|
+
"participation_coefficient",
|
|
63
|
+
"partition_edge_summary",
|
|
64
|
+
"positive_proportional_adjacency",
|
|
65
|
+
"subspace_distance",
|
|
66
|
+
"subspace_similarity",
|
|
67
|
+
"weighted_correlation",
|
|
68
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Shared instantaneous edge-product kernels.
|
|
2
|
+
|
|
3
|
+
ETS and MTD differ in how they construct their ROI samples. Once those
|
|
4
|
+
samples have been constructed, both methods use the same upper-triangular
|
|
5
|
+
outer-product operation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import ArrayLike, NDArray
|
|
14
|
+
|
|
15
|
+
from .._arrays import readonly_copy as _readonly
|
|
16
|
+
from .._validation import validated_roi_indices
|
|
17
|
+
from .correlation import edge_index
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validated_samples(samples: ArrayLike) -> NDArray[np.float64]:
|
|
21
|
+
values = np.asarray(samples, dtype=float)
|
|
22
|
+
if values.ndim != 2 or values.shape[1] < 2:
|
|
23
|
+
raise ValueError("samples must be a frames-by-ROI array")
|
|
24
|
+
if not np.isfinite(values).all():
|
|
25
|
+
raise ValueError("samples contain non-finite values")
|
|
26
|
+
return values
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def edge_products(samples: ArrayLike) -> NDArray[np.float64]:
|
|
30
|
+
"""Return all upper-triangular instantaneous ROI edge products.
|
|
31
|
+
|
|
32
|
+
The rows may be segment-standardized signals (ETS) or standardized
|
|
33
|
+
temporal derivatives (MTD); the kernel intentionally has no method-specific
|
|
34
|
+
temporal semantics.
|
|
35
|
+
"""
|
|
36
|
+
values = _validated_samples(samples)
|
|
37
|
+
left, right = edge_index(values.shape[1])
|
|
38
|
+
products = values[:, left] * values[:, right]
|
|
39
|
+
if not np.isfinite(products).all():
|
|
40
|
+
raise ValueError("edge products contain non-finite values")
|
|
41
|
+
return _readonly(products)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def edge_rss(samples: ArrayLike) -> NDArray[np.float64]:
|
|
45
|
+
"""Compute all-edge root-sum-square amplitude without materializing edges."""
|
|
46
|
+
values = _validated_samples(samples)
|
|
47
|
+
squared = values * values
|
|
48
|
+
squared_rss = ((squared.sum(axis=1) ** 2 - (squared * squared).sum(axis=1)) / 2).clip(min=0.0)
|
|
49
|
+
return _readonly(np.sqrt(squared_rss))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cross_block_products(
|
|
53
|
+
samples: ArrayLike,
|
|
54
|
+
left: Iterable[int],
|
|
55
|
+
right: Iterable[int],
|
|
56
|
+
) -> NDArray[np.float64]:
|
|
57
|
+
"""Return the mean product across two disjoint ROI blocks per row."""
|
|
58
|
+
values = _validated_samples(samples)
|
|
59
|
+
left_nodes = validated_roi_indices(left, n_rois=values.shape[1], label="left block", minimum=1)
|
|
60
|
+
right_nodes = validated_roi_indices(
|
|
61
|
+
right, n_rois=values.shape[1], label="right block", minimum=1
|
|
62
|
+
)
|
|
63
|
+
overlap = sorted(set(left_nodes.tolist()).intersection(right_nodes.tolist()))
|
|
64
|
+
if overlap:
|
|
65
|
+
raise ValueError(f"cross-block products require disjoint blocks; overlap={overlap}")
|
|
66
|
+
products = (values[:, left_nodes, None] * values[:, None, right_nodes]).mean(axis=(1, 2))
|
|
67
|
+
return _readonly(products)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def within_block_products(
|
|
71
|
+
samples: ArrayLike,
|
|
72
|
+
nodes: Iterable[int],
|
|
73
|
+
) -> NDArray[np.float64]:
|
|
74
|
+
"""Return the off-diagonal mean product within one ROI block per row."""
|
|
75
|
+
values = _validated_samples(samples)
|
|
76
|
+
selected = validated_roi_indices(nodes, n_rois=values.shape[1], label="within block", minimum=2)
|
|
77
|
+
left, right = np.triu_indices(len(selected), k=1)
|
|
78
|
+
products = (values[:, selected[left]] * values[:, selected[right]]).mean(axis=1)
|
|
79
|
+
return _readonly(products)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _edge_block_mask(
|
|
83
|
+
result: object,
|
|
84
|
+
left_nodes: NDArray[np.int64],
|
|
85
|
+
right_nodes: NDArray[np.int64],
|
|
86
|
+
) -> NDArray[np.bool_]:
|
|
87
|
+
edge_i = np.asarray(result.edge_i, dtype=np.int64)
|
|
88
|
+
edge_j = np.asarray(result.edge_j, dtype=np.int64)
|
|
89
|
+
return np.isin(edge_i, left_nodes) & np.isin(edge_j, right_nodes) | (
|
|
90
|
+
np.isin(edge_i, right_nodes) & np.isin(edge_j, left_nodes)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cross_block_edge_summary(
|
|
95
|
+
result: object,
|
|
96
|
+
left: Iterable[int],
|
|
97
|
+
right: Iterable[int],
|
|
98
|
+
) -> NDArray[np.float64]:
|
|
99
|
+
"""Summarize cross-block products from a common edge result."""
|
|
100
|
+
n_rois = len(result.roi_names)
|
|
101
|
+
left_nodes = validated_roi_indices(left, n_rois=n_rois, label="left block", minimum=1)
|
|
102
|
+
right_nodes = validated_roi_indices(right, n_rois=n_rois, label="right block", minimum=1)
|
|
103
|
+
overlap = sorted(set(left_nodes.tolist()).intersection(right_nodes.tolist()))
|
|
104
|
+
if overlap:
|
|
105
|
+
raise ValueError(f"cross-block products require disjoint blocks; overlap={overlap}")
|
|
106
|
+
if result.features is not None:
|
|
107
|
+
selected = _edge_block_mask(result, left_nodes, right_nodes)
|
|
108
|
+
features = np.asarray(result.features, dtype=float)
|
|
109
|
+
if not np.any(selected):
|
|
110
|
+
raise ValueError("cross-block product summary selected no edges")
|
|
111
|
+
return _readonly(features[:, selected].mean(axis=1))
|
|
112
|
+
raise ValueError("cross-block summary requires materialized edge features")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def within_block_edge_summary(
|
|
116
|
+
result: object,
|
|
117
|
+
nodes: Iterable[int],
|
|
118
|
+
) -> NDArray[np.float64]:
|
|
119
|
+
"""Summarize within-block products from a common edge result."""
|
|
120
|
+
n_rois = len(result.roi_names)
|
|
121
|
+
selected_nodes = validated_roi_indices(nodes, n_rois=n_rois, label="within block", minimum=2)
|
|
122
|
+
if result.features is not None:
|
|
123
|
+
edge_i = np.asarray(result.edge_i, dtype=np.int64)
|
|
124
|
+
edge_j = np.asarray(result.edge_j, dtype=np.int64)
|
|
125
|
+
selected = np.isin(edge_i, selected_nodes) & np.isin(edge_j, selected_nodes)
|
|
126
|
+
features = np.asarray(result.features, dtype=float)
|
|
127
|
+
if not np.any(selected):
|
|
128
|
+
raise ValueError("within-block product summary selected no edges")
|
|
129
|
+
return _readonly(features[:, selected].mean(axis=1))
|
|
130
|
+
raise ValueError("within-block summary requires materialized edge features")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Correlation matrices and edge-vector utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike, NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def edge_index(n_rois: int) -> tuple[NDArray[np.int64], NDArray[np.int64]]:
|
|
10
|
+
if n_rois < 2:
|
|
11
|
+
raise ValueError("at least two ROIs are required")
|
|
12
|
+
left, right = np.triu_indices(n_rois, k=1)
|
|
13
|
+
return left.astype(np.int64), right.astype(np.int64)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def weighted_correlation(
|
|
17
|
+
values: ArrayLike,
|
|
18
|
+
weights: ArrayLike | None = None,
|
|
19
|
+
) -> NDArray[np.float64]:
|
|
20
|
+
"""Compute a finite weighted Pearson correlation matrix."""
|
|
21
|
+
data = np.asarray(values, dtype=float)
|
|
22
|
+
if data.ndim != 2 or data.shape[0] < 2 or data.shape[1] < 2:
|
|
23
|
+
raise ValueError("values must have at least two frames and two ROIs")
|
|
24
|
+
if not np.isfinite(data).all():
|
|
25
|
+
raise ValueError("values contain non-finite samples")
|
|
26
|
+
|
|
27
|
+
if weights is None:
|
|
28
|
+
weight = np.ones(data.shape[0], dtype=float)
|
|
29
|
+
else:
|
|
30
|
+
weight = np.asarray(weights, dtype=float)
|
|
31
|
+
if weight.ndim != 1 or len(weight) != len(data):
|
|
32
|
+
raise ValueError("weights must be one-dimensional and match the frame count")
|
|
33
|
+
if not np.isfinite(weight).all() or np.any(weight < 0) or weight.sum() <= 0:
|
|
34
|
+
raise ValueError("weights must be finite, non-negative, and have positive sum")
|
|
35
|
+
|
|
36
|
+
mean = np.average(data, axis=0, weights=weight)
|
|
37
|
+
centered = data - mean
|
|
38
|
+
covariance = (centered * weight[:, None]).T @ centered / weight.sum()
|
|
39
|
+
variance = np.diag(covariance)
|
|
40
|
+
if np.any(~np.isfinite(variance)) or np.any(variance <= 1e-12):
|
|
41
|
+
raise ValueError("correlation is undefined for a constant or invalid ROI")
|
|
42
|
+
scale = np.sqrt(variance)
|
|
43
|
+
correlation = covariance / np.outer(scale, scale)
|
|
44
|
+
np.fill_diagonal(correlation, 1.0)
|
|
45
|
+
if not np.isfinite(correlation).all():
|
|
46
|
+
raise ValueError("correlation matrix contains non-finite values")
|
|
47
|
+
return np.clip(correlation, -1.0, 1.0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fisher_z_edges(
|
|
51
|
+
correlation: ArrayLike,
|
|
52
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64], NDArray[np.int64]]:
|
|
53
|
+
"""Return finite upper-triangular Fisher-z edges and their node indices."""
|
|
54
|
+
matrix = np.asarray(correlation, dtype=float)
|
|
55
|
+
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
|
|
56
|
+
raise ValueError("correlation must be a square matrix")
|
|
57
|
+
if not np.isfinite(matrix).all():
|
|
58
|
+
raise ValueError("correlation contains non-finite values")
|
|
59
|
+
left, right = edge_index(matrix.shape[0])
|
|
60
|
+
edges = np.arctanh(np.clip(matrix[left, right], -0.999999, 0.999999))
|
|
61
|
+
return edges, left, right
|