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.
Files changed (71) hide show
  1. dfc_kit-1.0.0.dist-info/METADATA +184 -0
  2. dfc_kit-1.0.0.dist-info/RECORD +71 -0
  3. dfc_kit-1.0.0.dist-info/WHEEL +5 -0
  4. dfc_kit-1.0.0.dist-info/entry_points.txt +2 -0
  5. dfc_kit-1.0.0.dist-info/licenses/LICENSE +29 -0
  6. dfc_kit-1.0.0.dist-info/top_level.txt +1 -0
  7. dfckit/__init__.py +11 -0
  8. dfckit/_arrays.py +13 -0
  9. dfckit/_preprocessing.py +46 -0
  10. dfckit/_validation.py +87 -0
  11. dfckit/artifacts/__init__.py +36 -0
  12. dfckit/artifacts/_fields.py +67 -0
  13. dfckit/artifacts/_json.py +92 -0
  14. dfckit/artifacts/_numpy.py +55 -0
  15. dfckit/artifacts/models.py +624 -0
  16. dfckit/artifacts/state_alignment.py +106 -0
  17. dfckit/artifacts/state_results.py +353 -0
  18. dfckit/artifacts/state_scoring.py +509 -0
  19. dfckit/artifacts/state_stability.py +207 -0
  20. dfckit/cli.py +105 -0
  21. dfckit/commands/__init__.py +1 -0
  22. dfckit/commands/parser.py +354 -0
  23. dfckit/commands/reporting.py +146 -0
  24. dfckit/commands/source.py +245 -0
  25. dfckit/commands/stability.py +288 -0
  26. dfckit/commands/states.py +266 -0
  27. dfckit/connectivity/__init__.py +68 -0
  28. dfckit/connectivity/_edge_products.py +130 -0
  29. dfckit/connectivity/correlation.py +61 -0
  30. dfckit/connectivity/instantaneous.py +253 -0
  31. dfckit/connectivity/leida.py +204 -0
  32. dfckit/connectivity/lowrank.py +384 -0
  33. dfckit/connectivity/partition.py +291 -0
  34. dfckit/connectivity/windows.py +83 -0
  35. dfckit/data.py +229 -0
  36. dfckit/inference/__init__.py +55 -0
  37. dfckit/inference/endpoints.py +194 -0
  38. dfckit/inference/hc3.py +207 -0
  39. dfckit/inference/matching.py +300 -0
  40. dfckit/inference/multiple_testing.py +52 -0
  41. dfckit/inference/nbs.py +586 -0
  42. dfckit/inference/paired.py +157 -0
  43. dfckit/inference/state_metrics.py +277 -0
  44. dfckit/information/__init__.py +45 -0
  45. dfckit/information/_artifact.py +440 -0
  46. dfckit/information/estimators.py +536 -0
  47. dfckit/information/fixed.py +660 -0
  48. dfckit/information/summary.py +89 -0
  49. dfckit/io/__init__.py +23 -0
  50. dfckit/io/xcpd.py +470 -0
  51. dfckit/reference.py +350 -0
  52. dfckit/segments.py +44 -0
  53. dfckit/states/__init__.py +97 -0
  54. dfckit/states/alignment.py +364 -0
  55. dfckit/states/cap.py +62 -0
  56. dfckit/states/cross_validation.py +114 -0
  57. dfckit/states/data.py +387 -0
  58. dfckit/states/hmm.py +387 -0
  59. dfckit/states/interpretation.py +273 -0
  60. dfckit/states/kmeans.py +340 -0
  61. dfckit/states/metrics.py +94 -0
  62. dfckit/states/scoring.py +114 -0
  63. dfckit/states/selection.py +516 -0
  64. dfckit/states/stability.py +183 -0
  65. dfckit/states/streaming.py +859 -0
  66. dfckit/states/streaming_hmm.py +436 -0
  67. dfckit/storage/__init__.py +29 -0
  68. dfckit/storage/_statistics.py +80 -0
  69. dfckit/storage/builders.py +330 -0
  70. dfckit/storage/store.py +672 -0
  71. dfckit/storage/summary.py +140 -0
@@ -0,0 +1,509 @@
1
+ """Strict JSON persistence for held-out state-model scores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from types import MappingProxyType
10
+
11
+ import numpy as np
12
+
13
+ from .._validation import (
14
+ validated_nonnegative_integer as _nonnegative_integer,
15
+ )
16
+ from .._validation import (
17
+ validated_positive_integer as _positive_integer,
18
+ )
19
+ from ..states.hmm import GaussianHMMStateModel
20
+ from ..states.kmeans import KMeansStateModel
21
+ from ..states.scoring import RunGaussianHMMScore, RunKMeansScore, RunStateModelScore
22
+ from ._json import load_json_object, write_json_atomic
23
+
24
+ FORMAT_NAME = "dfckit-state-model-scores"
25
+ FORMAT_VERSION = 3
26
+
27
+
28
+ def _state_model_specification(
29
+ model: KMeansStateModel | GaussianHMMStateModel,
30
+ ) -> dict[str, object]:
31
+ """Return the complete score-comparison specification of a fitted model."""
32
+ if isinstance(model, KMeansStateModel):
33
+ return {
34
+ "algorithm": model.algorithm,
35
+ "batch_size": model.batch_size,
36
+ "implementation": model.implementation,
37
+ "init_sample_size": model.init_sample_size,
38
+ "max_iter": model.max_iter,
39
+ "n_init": model.n_init,
40
+ "n_pca_components": model.n_pca_components,
41
+ "pca_batch_size": model.pca_batch_size,
42
+ "reassignment_ratio": model.reassignment_ratio,
43
+ "standardize_features": model.standardize_features,
44
+ }
45
+ if isinstance(model, GaussianHMMStateModel):
46
+ return {
47
+ "covariance_type": model.covariance_type,
48
+ "implementation": model.implementation,
49
+ "minimum_sequence_length": model.minimum_sequence_length,
50
+ "n_init": model.n_init,
51
+ "n_iter": model.n_iter,
52
+ "n_pca_components": model.n_pca_components,
53
+ "pca_batch_size": model.pca_batch_size,
54
+ "tol": model.tol,
55
+ }
56
+ raise TypeError("state-model specification requires KMeans or Gaussian HMM")
57
+
58
+
59
+ def _run_boundaries(
60
+ report: StateModelScoreReport,
61
+ ) -> tuple[tuple[object, ...], ...]:
62
+ """Return acquisition identities used to compare score collections."""
63
+ return tuple(
64
+ (
65
+ score.subject,
66
+ score.session,
67
+ score.acquisition_id,
68
+ score.n_samples,
69
+ score.n_sequences,
70
+ )
71
+ for score in report.scores
72
+ )
73
+
74
+ _KMEANS_SPECIFICATION_FIELDS = {
75
+ "algorithm",
76
+ "batch_size",
77
+ "implementation",
78
+ "init_sample_size",
79
+ "max_iter",
80
+ "n_init",
81
+ "n_pca_components",
82
+ "pca_batch_size",
83
+ "reassignment_ratio",
84
+ "standardize_features",
85
+ }
86
+ _HMM_SPECIFICATION_FIELDS = {
87
+ "covariance_type",
88
+ "implementation",
89
+ "minimum_sequence_length",
90
+ "n_init",
91
+ "n_iter",
92
+ "n_pca_components",
93
+ "pca_batch_size",
94
+ "tol",
95
+ }
96
+
97
+
98
+ def _sample_interval(value: float | None) -> float | None:
99
+ if value is None:
100
+ return None
101
+ output = float(value)
102
+ if not np.isfinite(output) or output <= 0.0:
103
+ raise ValueError("sample_interval_seconds must be finite and positive")
104
+ return output
105
+
106
+
107
+ def _model_specification(
108
+ value: Mapping[str, object],
109
+ model_kind: str,
110
+ ) -> dict[str, object]:
111
+ if not isinstance(value, Mapping):
112
+ raise TypeError("model_specification must be a mapping")
113
+ output = dict(value)
114
+ if model_kind == "kmeans-state":
115
+ output.setdefault("n_pca_components", None)
116
+ output.setdefault("pca_batch_size", None)
117
+ expected = (
118
+ _KMEANS_SPECIFICATION_FIELDS
119
+ if model_kind == "kmeans-state"
120
+ else _HMM_SPECIFICATION_FIELDS
121
+ )
122
+ if set(output) != expected:
123
+ raise ValueError(
124
+ "model_specification fields do not match model_kind; "
125
+ f"missing={sorted(expected - set(output))}, "
126
+ f"unexpected={sorted(set(output) - expected)}"
127
+ )
128
+ try:
129
+ json.dumps(output, allow_nan=False, sort_keys=True)
130
+ except (TypeError, ValueError) as error:
131
+ raise ValueError("model_specification must contain finite JSON values") from error
132
+ if not isinstance(output["implementation"], str) or not str(
133
+ output["implementation"]
134
+ ).strip():
135
+ raise ValueError("model_specification implementation must be non-empty")
136
+ if model_kind == "kmeans-state":
137
+ if output["algorithm"] not in {"lloyd", "minibatch"}:
138
+ raise ValueError("KMeans model_specification algorithm is invalid")
139
+ if not isinstance(output["standardize_features"], bool):
140
+ raise TypeError("KMeans model_specification standardize_features must be boolean")
141
+ for name in ("n_init", "max_iter"):
142
+ _positive_integer(output[name], f"model_specification {name}")
143
+ for name in (
144
+ "batch_size",
145
+ "init_sample_size",
146
+ "n_pca_components",
147
+ "pca_batch_size",
148
+ ):
149
+ if output[name] is not None:
150
+ _positive_integer(output[name], f"model_specification {name}")
151
+ ratio = output["reassignment_ratio"]
152
+ if ratio is not None and (
153
+ isinstance(ratio, bool)
154
+ or not isinstance(ratio, (int, float))
155
+ or not np.isfinite(ratio)
156
+ or not 0.0 <= ratio <= 1.0
157
+ ):
158
+ raise ValueError("KMeans model_specification reassignment_ratio is invalid")
159
+ else:
160
+ if output["covariance_type"] not in {"diag", "full"}:
161
+ raise ValueError("HMM model_specification covariance_type is invalid")
162
+ for name in ("n_init", "n_iter", "n_pca_components", "minimum_sequence_length"):
163
+ _positive_integer(output[name], f"model_specification {name}")
164
+ if output["pca_batch_size"] is not None:
165
+ _positive_integer(
166
+ output["pca_batch_size"],
167
+ "model_specification pca_batch_size",
168
+ )
169
+ tolerance = output["tol"]
170
+ if (
171
+ isinstance(tolerance, bool)
172
+ or not isinstance(tolerance, (int, float))
173
+ or not np.isfinite(tolerance)
174
+ or tolerance <= 0.0
175
+ ):
176
+ raise ValueError("HMM model_specification tol must be finite and positive")
177
+ return output
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class StateModelScoreReport:
182
+ """Validated in-memory representation of one score artifact."""
183
+
184
+ scores: tuple[RunStateModelScore, ...]
185
+ format_version: int
186
+ model_kind: str
187
+ model_seed: int
188
+ n_states: int
189
+ fit_subjects: tuple[str, ...]
190
+ source_contract: str
191
+ sample_interval_seconds: float | None
192
+ minimum_sequence_length: int
193
+ omitted_short_sequence_count: int
194
+ allow_fit_subjects: bool
195
+ model_specification: Mapping[str, object]
196
+
197
+ def __post_init__(self) -> None:
198
+ object.__setattr__(self, "scores", tuple(self.scores))
199
+ specification = _model_specification(self.model_specification, self.model_kind)
200
+ object.__setattr__(
201
+ self,
202
+ "model_specification",
203
+ MappingProxyType(specification),
204
+ )
205
+ canonical = _state_model_scores_payload(
206
+ self.scores,
207
+ model_kind=self.model_kind,
208
+ model_seed=self.model_seed,
209
+ n_states=self.n_states,
210
+ fit_subjects=self.fit_subjects,
211
+ source_contract=self.source_contract,
212
+ sample_interval_seconds=self.sample_interval_seconds,
213
+ minimum_sequence_length=self.minimum_sequence_length,
214
+ omitted_short_sequence_count=self.omitted_short_sequence_count,
215
+ allow_fit_subjects=self.allow_fit_subjects,
216
+ model_specification=self.model_specification,
217
+ )
218
+ if canonical["format_version"] != self.format_version:
219
+ raise ValueError("state-model score format version and provenance disagree")
220
+
221
+ @property
222
+ def subjects(self) -> tuple[str, ...]:
223
+ return tuple(dict.fromkeys(score.subject for score in self.scores))
224
+
225
+ @property
226
+ def n_runs(self) -> int:
227
+ return len(self.scores)
228
+
229
+ @property
230
+ def n_sequences(self) -> int:
231
+ return sum(score.n_sequences for score in self.scores)
232
+
233
+ @property
234
+ def n_samples(self) -> int:
235
+ return sum(score.n_samples for score in self.scores)
236
+
237
+
238
+ def _state_model_scores_payload(
239
+ scores: Sequence[RunStateModelScore],
240
+ *,
241
+ model_kind: str,
242
+ model_seed: int,
243
+ n_states: int,
244
+ fit_subjects: Sequence[str],
245
+ source_contract: str,
246
+ sample_interval_seconds: float | None,
247
+ minimum_sequence_length: int,
248
+ omitted_short_sequence_count: int,
249
+ allow_fit_subjects: bool,
250
+ model_specification: Mapping[str, object],
251
+ ) -> dict[str, object]:
252
+ """Build a finite held-out scoring payload."""
253
+ scores = tuple(scores)
254
+ if not scores:
255
+ raise ValueError("state-model scoring requires at least one run")
256
+ if model_kind not in {"kmeans-state", "gaussian-hmm-state"}:
257
+ raise ValueError("state-model scoring model_kind is invalid")
258
+ expected_type = RunKMeansScore if model_kind == "kmeans-state" else RunGaussianHMMScore
259
+ if any(not isinstance(score, expected_type) for score in scores):
260
+ raise TypeError("state-model score type does not match model_kind")
261
+ if not isinstance(allow_fit_subjects, (bool, np.bool_)):
262
+ raise TypeError("allow_fit_subjects must be boolean")
263
+ if not isinstance(source_contract, str) or not source_contract.strip():
264
+ raise ValueError("state-model scoring source_contract must be non-empty")
265
+ fit_subjects = tuple(fit_subjects)
266
+ if (
267
+ not fit_subjects
268
+ or any(not isinstance(subject, str) or not subject.strip() for subject in fit_subjects)
269
+ or len(set(fit_subjects)) != len(fit_subjects)
270
+ ):
271
+ raise ValueError("fit_subjects must contain unique non-empty labels")
272
+ identities = tuple(
273
+ (score.subject, score.session, score.acquisition_id) for score in scores
274
+ )
275
+ if len(set(identities)) != len(identities):
276
+ raise ValueError("state-model scores contain duplicate run identities")
277
+ score_subjects = tuple(dict.fromkeys(score.subject for score in scores))
278
+ overlap = sorted(set(fit_subjects).intersection(score_subjects))
279
+ if overlap and not allow_fit_subjects:
280
+ raise ValueError(
281
+ "state-model score fit and evaluation subjects overlap without permission: "
282
+ f"{overlap}"
283
+ )
284
+ specification = _model_specification(model_specification, model_kind)
285
+ if (
286
+ model_kind == "gaussian-hmm-state"
287
+ and specification["minimum_sequence_length"] != minimum_sequence_length
288
+ ):
289
+ raise ValueError(
290
+ "HMM model_specification minimum_sequence_length disagrees with scoring"
291
+ )
292
+
293
+ runs: list[dict[str, object]] = []
294
+ if model_kind == "kmeans-state":
295
+ total = sum(score.total_squared_distance for score in scores)
296
+ n_samples = sum(score.n_samples for score in scores)
297
+ metric_name = "mean_squared_distance"
298
+ direction = "lower is better on an identical held-out cohort"
299
+ summary = {
300
+ "total_squared_distance": total,
301
+ "mean_squared_distance": total / n_samples,
302
+ }
303
+ for score in scores:
304
+ assert isinstance(score, RunKMeansScore)
305
+ runs.append(
306
+ {
307
+ "subject": score.subject,
308
+ "session": score.session,
309
+ "acquisition_id": score.acquisition_id,
310
+ "n_samples": score.n_samples,
311
+ "n_sequences": score.n_sequences,
312
+ "total_squared_distance": score.total_squared_distance,
313
+ "mean_squared_distance": score.mean_squared_distance,
314
+ }
315
+ )
316
+ else:
317
+ total = sum(score.log_likelihood for score in scores)
318
+ n_samples = sum(score.n_samples for score in scores)
319
+ metric_name = "log_likelihood_per_sample"
320
+ direction = "higher is better on an identical held-out cohort"
321
+ summary = {
322
+ "log_likelihood": total,
323
+ "log_likelihood_per_sample": total / n_samples,
324
+ }
325
+ for score in scores:
326
+ assert isinstance(score, RunGaussianHMMScore)
327
+ runs.append(
328
+ {
329
+ "subject": score.subject,
330
+ "session": score.session,
331
+ "acquisition_id": score.acquisition_id,
332
+ "n_samples": score.n_samples,
333
+ "n_sequences": score.n_sequences,
334
+ "log_likelihood": score.log_likelihood,
335
+ "log_likelihood_per_sample": score.log_likelihood_per_sample,
336
+ }
337
+ )
338
+
339
+ payload = {
340
+ "format": FORMAT_NAME,
341
+ "format_version": FORMAT_VERSION,
342
+ "model_kind": model_kind,
343
+ "model_seed": _nonnegative_integer(model_seed, "model_seed"),
344
+ "n_states": _positive_integer(n_states, "n_states"),
345
+ "selection_metric": metric_name,
346
+ "selection_direction": direction,
347
+ "fit_subjects": list(fit_subjects),
348
+ "source_contract": source_contract,
349
+ "sample_interval_seconds": _sample_interval(sample_interval_seconds),
350
+ "minimum_sequence_length": _positive_integer(
351
+ minimum_sequence_length,
352
+ "minimum_sequence_length",
353
+ ),
354
+ "omitted_short_sequence_count": _nonnegative_integer(
355
+ omitted_short_sequence_count,
356
+ "omitted_short_sequence_count",
357
+ ),
358
+ "allow_fit_subjects": bool(allow_fit_subjects),
359
+ "n_runs": len(scores),
360
+ "n_sequences": sum(score.n_sequences for score in scores),
361
+ "n_samples": sum(score.n_samples for score in scores),
362
+ "subjects": list(score_subjects),
363
+ "summary": summary,
364
+ "runs": runs,
365
+ "model_specification": specification,
366
+ }
367
+ return payload
368
+
369
+
370
+ def load_state_model_scores(path: str | Path) -> StateModelScoreReport:
371
+ """Load and canonically validate a state-model score artifact."""
372
+ source = Path(path)
373
+ raw = load_json_object(source, context="state-model score artifact")
374
+ version = raw.get("format_version")
375
+ if (
376
+ raw.get("format") != FORMAT_NAME
377
+ or isinstance(version, bool)
378
+ or not isinstance(version, int)
379
+ or version != FORMAT_VERSION
380
+ ):
381
+ raise ValueError("unsupported state-model score artifact format or version")
382
+ expected_fields = {
383
+ "allow_fit_subjects",
384
+ "fit_subjects",
385
+ "format",
386
+ "format_version",
387
+ "minimum_sequence_length",
388
+ "model_kind",
389
+ "model_seed",
390
+ "model_specification",
391
+ "n_runs",
392
+ "n_samples",
393
+ "n_sequences",
394
+ "n_states",
395
+ "omitted_short_sequence_count",
396
+ "runs",
397
+ "sample_interval_seconds",
398
+ "selection_direction",
399
+ "selection_metric",
400
+ "source_contract",
401
+ "subjects",
402
+ "summary",
403
+ }
404
+ if set(raw) != expected_fields:
405
+ raise ValueError(
406
+ "state-model score artifact fields do not match its version; "
407
+ f"missing={sorted(expected_fields - set(raw))}, "
408
+ f"unexpected={sorted(set(raw) - expected_fields)}"
409
+ )
410
+ model_kind = raw["model_kind"]
411
+ if model_kind not in {"kmeans-state", "gaussian-hmm-state"}:
412
+ raise ValueError("state-model score artifact model_kind is invalid")
413
+ run_fields = {
414
+ "acquisition_id",
415
+ "mean_squared_distance",
416
+ "n_samples",
417
+ "n_sequences",
418
+ "session",
419
+ "subject",
420
+ "total_squared_distance",
421
+ } if model_kind == "kmeans-state" else {
422
+ "acquisition_id",
423
+ "log_likelihood",
424
+ "log_likelihood_per_sample",
425
+ "n_samples",
426
+ "n_sequences",
427
+ "session",
428
+ "subject",
429
+ }
430
+ records = raw["runs"]
431
+ if not isinstance(records, list) or not records:
432
+ raise ValueError("state-model score artifact runs must be a non-empty array")
433
+ scores: list[RunStateModelScore] = []
434
+ for record in records:
435
+ if not isinstance(record, dict) or set(record) != run_fields:
436
+ raise ValueError("state-model score run fields do not match model_kind")
437
+ common = {
438
+ "subject": record["subject"],
439
+ "session": record["session"],
440
+ "acquisition_id": record["acquisition_id"],
441
+ "n_samples": record["n_samples"],
442
+ "n_sequences": record["n_sequences"],
443
+ }
444
+ if model_kind == "kmeans-state":
445
+ score = RunKMeansScore(
446
+ **common,
447
+ total_squared_distance=record["total_squared_distance"],
448
+ mean_squared_distance=record["mean_squared_distance"],
449
+ )
450
+ else:
451
+ score = RunGaussianHMMScore(
452
+ **common,
453
+ log_likelihood=record["log_likelihood"],
454
+ log_likelihood_per_sample=record["log_likelihood_per_sample"],
455
+ )
456
+ scores.append(score)
457
+ fit_subjects = raw["fit_subjects"]
458
+ if not isinstance(fit_subjects, list):
459
+ raise TypeError("state-model score fit_subjects must be an array")
460
+ report = StateModelScoreReport(
461
+ scores=tuple(scores),
462
+ format_version=version,
463
+ model_kind=model_kind,
464
+ model_seed=raw["model_seed"],
465
+ n_states=raw["n_states"],
466
+ fit_subjects=tuple(fit_subjects),
467
+ source_contract=raw["source_contract"],
468
+ sample_interval_seconds=raw["sample_interval_seconds"],
469
+ minimum_sequence_length=raw["minimum_sequence_length"],
470
+ omitted_short_sequence_count=raw["omitted_short_sequence_count"],
471
+ allow_fit_subjects=raw["allow_fit_subjects"],
472
+ model_specification=raw["model_specification"],
473
+ )
474
+ canonical = _state_model_scores_payload(
475
+ report.scores,
476
+ model_kind=report.model_kind,
477
+ model_seed=report.model_seed,
478
+ n_states=report.n_states,
479
+ fit_subjects=report.fit_subjects,
480
+ source_contract=report.source_contract,
481
+ sample_interval_seconds=report.sample_interval_seconds,
482
+ minimum_sequence_length=report.minimum_sequence_length,
483
+ omitted_short_sequence_count=report.omitted_short_sequence_count,
484
+ allow_fit_subjects=report.allow_fit_subjects,
485
+ model_specification=report.model_specification,
486
+ )
487
+ if raw != canonical:
488
+ raise ValueError("state-model score artifact summary or metadata is inconsistent")
489
+ return report
490
+
491
+
492
+ def write_state_model_scores(
493
+ scores: Sequence[RunStateModelScore],
494
+ path: str | Path,
495
+ **metadata: object,
496
+ ) -> Path:
497
+ """Atomically write held-out state-model scores without JSON NaN values."""
498
+ target = Path(path)
499
+ payload = _state_model_scores_payload(scores, **metadata)
500
+ return write_json_atomic(target, payload)
501
+
502
+
503
+ __all__ = [
504
+ "FORMAT_NAME",
505
+ "FORMAT_VERSION",
506
+ "StateModelScoreReport",
507
+ "load_state_model_scores",
508
+ "write_state_model_scores",
509
+ ]