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,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: dfc-kit
3
+ Version: 1.0.0
4
+ Summary: Censor-aware tools for dynamic functional connectivity analysis
5
+ License-Expression: BSD-3-Clause
6
+ Project-URL: Homepage, https://github.com/yidao9518/dfc-kit
7
+ Project-URL: Repository, https://github.com/yidao9518/dfc-kit
8
+ Project-URL: Issues, https://github.com/yidao9518/dfc-kit/issues
9
+ Keywords: functional connectivity,dynamic functional connectivity,fMRI,neuroimaging,XCP-D
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy>=1.23
22
+ Provides-Extra: states
23
+ Requires-Dist: scikit-learn>=1.2; extra == "states"
24
+ Provides-Extra: phase
25
+ Requires-Dist: scipy>=1.10; extra == "phase"
26
+ Provides-Extra: inference
27
+ Requires-Dist: scipy>=1.10; extra == "inference"
28
+ Provides-Extra: hmm
29
+ Requires-Dist: hmmlearn>=0.3; extra == "hmm"
30
+ Requires-Dist: scikit-learn>=1.2; extra == "hmm"
31
+ Provides-Extra: information
32
+ Requires-Dist: scipy>=1.10; extra == "information"
33
+ Provides-Extra: all
34
+ Requires-Dist: hmmlearn>=0.3; extra == "all"
35
+ Requires-Dist: scikit-learn>=1.2; extra == "all"
36
+ Requires-Dist: scipy>=1.10; extra == "all"
37
+ Provides-Extra: dev
38
+ Requires-Dist: build>=1.2; extra == "dev"
39
+ Requires-Dist: pytest>=8; extra == "dev"
40
+ Requires-Dist: ruff>=0.6; extra == "dev"
41
+ Requires-Dist: twine>=5; extra == "dev"
42
+ Provides-Extra: docs
43
+ Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
44
+ Dynamic: license-file
45
+
46
+ # dfc-kit
47
+
48
+ `dfc-kit` is an open-source Python toolkit for dynamic functional connectivity
49
+ analysis of XCP-D parcellated derivatives. It provides composable estimators,
50
+ state models, network summaries, statistical inference, and command-line
51
+ workflows for reproducible neuroimaging analysis.
52
+
53
+ ```text
54
+ BIDS -> fMRIPrep -> XCP-D -> dfc-kit
55
+ ```
56
+
57
+ ## Why dfc-kit
58
+
59
+ `dfc-kit` provides a unified workflow for estimating time-varying functional
60
+ connectivity, identifying recurring brain states, and testing paired or
61
+ between-group differences. Sliding-window FC, instantaneous edges generated
62
+ from ETS or MTD samples, LEiDA, CAP, KMeans, and Gaussian HMM analyses share
63
+ the same data structures and output conventions, making it easier to compare
64
+ methods without rebuilding data loading, state summaries, and statistical
65
+ inference for every analysis.
66
+
67
+ The toolkit supports both direct in-memory analysis and chunked feature stores
68
+ for larger datasets, with matching Python and command-line interfaces.
69
+
70
+ ## Features
71
+
72
+ - **Input and topology:** XCP-D discovery and validation, multi-atlas ROI
73
+ loading, acquisition identity, and censor-bounded sequences. Censored time
74
+ points retain their original frame indices, and temporal operations are
75
+ evaluated separately within contiguous retained segments.
76
+ - **Connectivity:** weighted sliding-window FC, instantaneous ETS/MTD edges,
77
+ LEiDA, low-rank covariance geometry, and fixed-length MI/CMI.
78
+ - **Connectivity and state analysis:** partition-based graph metrics, CAP, KMeans,
79
+ Gaussian HMMs, state alignment, occupancy/dwell/transition summaries, and
80
+ selection of the number of states using held-out participants.
81
+ - **Inference:** paired sign-flips, bootstrap intervals, HC3 models,
82
+ declared-family FDR, generic paired endpoint inference, paired NBS, and
83
+ within-subject motion matching.
84
+ - **Large-dataset workflows:** chunked, memory-mapped FeatureStores and
85
+ batch-wise fitting for MiniBatch KMeans and Incremental PCA.
86
+ - **Portable results:** models and held-out predictions stored as JSON and
87
+ NumPy arrays with explicit feature, subject, and parameter metadata.
88
+
89
+ The [method inventory](docs/method_inventory.md) maps each method family to its
90
+ public API and guide. Public data, connectivity, state, reference, and
91
+ inference objects are covered by the package test suite and documented
92
+ contracts.
93
+
94
+ ## Scope
95
+
96
+ The supported input boundary is XCP-D output. `dfc-kit` does not reimplement
97
+ fMRIPrep-to-XCP-D denoising, filtering, censoring, interpolation, or
98
+ parcellation. Callers provide ROI definitions, cohort labels, clinical
99
+ variables, and manuscript-specific analyses around the library's numerical
100
+ interfaces. The array API is also available for equivalently preprocessed ROI
101
+ time series that are not stored as XCP-D derivatives.
102
+
103
+ ## Installation
104
+
105
+ ```bash
106
+ python -m pip install dfc-kit
107
+ ```
108
+
109
+ Install only the optional method families required by an analysis:
110
+
111
+ ```bash
112
+ python -m pip install 'dfc-kit[phase,states,hmm,information,inference]'
113
+ ```
114
+
115
+ Python 3.10 or newer is required. See [Getting started](docs/getting_started.md)
116
+ for development installation and dependency details.
117
+
118
+ ## Quick start
119
+
120
+ ```python
121
+ from dfckit.connectivity import SlidingWindowFC
122
+ from dfckit.io import load_xcpd_run
123
+
124
+ loaded = load_xcpd_run(
125
+ "/path/to/xcp_d",
126
+ subject="sub-001",
127
+ session="01",
128
+ task="rest",
129
+ atlases=("Schaefer200",),
130
+ space="MNI152NLin2009cAsym",
131
+ minimum_coverage=0.5,
132
+ tr=0.8,
133
+ )
134
+
135
+ result = SlidingWindowFC(length=60, step=10, taper="hamming").transform(loaded.run)
136
+ print(result.features.shape)
137
+ print(result.start_frames, result.end_frames, result.segment_ids)
138
+ ```
139
+
140
+ The result contains Fisher-z upper-triangle edges and the original-frame bounds
141
+ of every valid window. For a complete path from XCP-D discovery through state
142
+ fitting, see the [XCP-D-to-state tutorial](docs/tutorial_xcpd_to_states.md).
143
+
144
+ ## Command line
145
+
146
+ The `dfc-kit` command exposes XCP-D inspection, FeatureStore construction,
147
+ state fitting, held-out prediction, scoring, alignment, and state-count
148
+ validation. Start with:
149
+
150
+ ```bash
151
+ dfc-kit --help
152
+ dfc-kit inspect-xcpd --help
153
+ dfc-kit build-store --help
154
+ dfc-kit fixed-information --help
155
+ dfc-kit describe-states --help
156
+ dfc-kit infer-state-metrics --help
157
+ dfc-kit summarize-store --help
158
+ dfc-kit summarize-information --help
159
+ dfc-kit infer-paired-endpoints --help
160
+ ```
161
+
162
+ See [Command-line workflows](docs/cli.md) for complete examples and arguments,
163
+ including fixed-length MI/CMI artifacts and frozen-window replay.
164
+
165
+ ## Documentation
166
+
167
+ - [Documentation home](docs/index.md)
168
+ - [XCP-D input contract](docs/xcpd_input.md)
169
+ - [Connectivity methods](docs/correlation.md)
170
+ - [State models and validation](docs/states.md)
171
+ - [API map](docs/api.md)
172
+ - [Release process](docs/release.md)
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ python -m pip install -e '.[all,dev,docs]'
178
+ python -m unittest discover
179
+ ruff check src tests
180
+ mkdocs build --strict
181
+ ```
182
+
183
+ `dfc-kit` is distributed under the BSD-3-Clause license. See `LICENSE` and
184
+ `CITATION.cff` for licensing and citation information.
@@ -0,0 +1,71 @@
1
+ dfc_kit-1.0.0.dist-info/licenses/LICENSE,sha256=kxmKO_Pwzh4hpRqvLrgakw0GknW6MZT-GpyESzUsOJg,1528
2
+ dfckit/__init__.py,sha256=KCGdV9ixKTvXjebYTUVobRTFCNQX0ceaZ9_Wc9LmaQk,283
3
+ dfckit/_arrays.py,sha256=8Cg2NH2ETnZlgIE7O8U-9jZJi8vU2_QgRMS5eEX2Z3Y,355
4
+ dfckit/_preprocessing.py,sha256=11WZ0GPg3lwKbWg0rPKsFPAPj3_IvVs0JmM-227FtoM,1702
5
+ dfckit/_validation.py,sha256=2_1bcCoLE6gPTiISdAQjD-lpTb84uz7jblc-364v9go,3637
6
+ dfckit/cli.py,sha256=MbEkgE8bcKch-DlLR7bAtU9ahhjVt6rPqdICAQRZxy0,3426
7
+ dfckit/data.py,sha256=DF-_bFXuOhjHe1_038I7tDLegenLchgXyaaA4ihLwxA,9403
8
+ dfckit/reference.py,sha256=RN_8VlEfxmqhUGnMyAfAXV_8XJ7aCUlqf4PELKwa_80,14121
9
+ dfckit/segments.py,sha256=3pNszqoKN-bznIMDEza1Of6-Phxji4NhWiWPDTjuvLw,1716
10
+ dfckit/artifacts/__init__.py,sha256=Rq0X9IeqpCznXrXyyWJGtgWVD-uAaklWJoKwQq7XVag,900
11
+ dfckit/artifacts/_fields.py,sha256=LlY-wFsyZFR4zAH6uujkedo1rGZgw02H03TSFkaJdVI,2224
12
+ dfckit/artifacts/_json.py,sha256=UnbH-8lTkGcXSkKMNrJNUeXOJY8uoi0q0GFXTrd5r-0,2986
13
+ dfckit/artifacts/_numpy.py,sha256=YwkQFnAuMgvwsRY8p5eiEJVpIPsB_5EwE0ngCNA955o,2127
14
+ dfckit/artifacts/models.py,sha256=yVLUCjxu-_4NCCTE5dToBfRs8BNRoaAe5_o9s39FnWc,26410
15
+ dfckit/artifacts/state_alignment.py,sha256=Me-WIDw5iCkSfbtHw46zs08E3Uk9fVqHqJAD-1638sE,4420
16
+ dfckit/artifacts/state_results.py,sha256=f6X16wXzy7mMS_Ou3hWPzXIDZ3T13qZiaRuWbgntBKY,14626
17
+ dfckit/artifacts/state_scoring.py,sha256=QvHMlOecgB60ozxbUR5f1hLj5DVtnv_Xr-cqhBHzO6k,18960
18
+ dfckit/artifacts/state_stability.py,sha256=ugRF2MN6D4IW_Ii4DpcHEcJD9XQDwOQMrLbRhY3TeOM,7547
19
+ dfckit/commands/__init__.py,sha256=mQOkYH18Zfzd8QYg_2uBFrNRR3vlnGlBVFAHZfxigZc,59
20
+ dfckit/commands/parser.py,sha256=Dycs33QvUcFcjSpJmPJ42nKuEZMLjX38jAN2BhdG9C0,14181
21
+ dfckit/commands/reporting.py,sha256=fGU2V3X1AlTittGwMsU6Xw_r5EHNRlQRwRM8CRquRf4,5623
22
+ dfckit/commands/source.py,sha256=S5GdjU34Xr7uMdLhlmHxv_rqAEcXVHDl5pu7hKsOPhw,8966
23
+ dfckit/commands/stability.py,sha256=DYM-BNCp9XXBTCNP5B7QkfxVT_sG4v_VV279Wp2DPJ8,12019
24
+ dfckit/commands/states.py,sha256=WAzEKYcjpP18Y7npO_FCM95rALRgYcq8YpB5wSCp5dM,10751
25
+ dfckit/connectivity/__init__.py,sha256=EjylhFWC-fhf1GqTbcle00y8gTUfVfs6ymqEoXWXHwI,1658
26
+ dfckit/connectivity/_edge_products.py,sha256=0U7DbJpPl8XnWEyPa6waCpKW1zks79uts8xAr-k0NgQ,5338
27
+ dfckit/connectivity/correlation.py,sha256=zqYopgGauG7PJXxOaUrNVAbHzhIidTpc5dkCGiRGj2I,2579
28
+ dfckit/connectivity/instantaneous.py,sha256=w5nTpYATYbDkPdGwGhda6SOtJCO95F-COUdYsfcHqcs,10725
29
+ dfckit/connectivity/leida.py,sha256=Ycf11G0hSPQLQyygAfgcETCawL2YBfokXmbljNLVkgc,7710
30
+ dfckit/connectivity/lowrank.py,sha256=Enhc4nnbkuBN84JE6fG5mSsgkAStVIMCpytz4X94pQ0,16668
31
+ dfckit/connectivity/partition.py,sha256=dFjUxHXQlSiVhQIq7jFzXsir8n-oRiYJDXwgXocJJLM,11612
32
+ dfckit/connectivity/windows.py,sha256=U-dBREBxgzTA-l9AT1I7_UOHjIlZnAPubErLfWnNYJk,2856
33
+ dfckit/inference/__init__.py,sha256=CCoa3ooCYLdsGgMAlM2pS_EEX5qnpySA7XtTmffZeGM,1298
34
+ dfckit/inference/endpoints.py,sha256=3LtFjfU3j5vOrcSSj_cQpOQQ6TG4GbYkiMd68__9O9c,7107
35
+ dfckit/inference/hc3.py,sha256=F4VrNQXGx9qe53wnK25hvgZWt88EzBLxaZz7abkMZRk,7409
36
+ dfckit/inference/matching.py,sha256=MZ55KNCW4dePI_eSzWHWL3espe3hi0iitZpHiMXeRMM,12336
37
+ dfckit/inference/multiple_testing.py,sha256=Ij2jTYJ3kSjFYkUOcluLdCMzlQ1CETqjMLTjYYldCWE,1773
38
+ dfckit/inference/nbs.py,sha256=sMRyXw6iv_kqxEMq8eeDOXnst3rv3E8XKZ7e2oQl2zc,22006
39
+ dfckit/inference/paired.py,sha256=jIWb31gOjGCI4sS-hhPIDcoHm9bx0mt4zUSaoA11mOM,5562
40
+ dfckit/inference/state_metrics.py,sha256=spcBU3SCBfB3KDwZ2fSvFq6vROLJcK1R3xIHxXtUnTA,10442
41
+ dfckit/information/__init__.py,sha256=6W8BHMnjowNI2DYrx2UFU5MHuv6cOtwxQGU4dVUweqM,1071
42
+ dfckit/information/_artifact.py,sha256=zPVwzjfU13sjePdagD8_pqH_xSZUXNZitz1uh8b02G0,19984
43
+ dfckit/information/estimators.py,sha256=F-mz9-A1zKBjyLO5FszJ0xLvrBCUNggIT6uer264HjY,19677
44
+ dfckit/information/fixed.py,sha256=F_mL5rb8KcYau9ImlEpYZRvQ8C-N4f0sgkEXcOFlsXc,25612
45
+ dfckit/information/summary.py,sha256=pr0Hg-erAXVqlb-p8M-Aj3Fstjp5s_4WWRYKPGB9G-o,2911
46
+ dfckit/io/__init__.py,sha256=PQEtD6_nPXyTAFY4NWEyntU2T_koSddGvpfU3SGAU7I,422
47
+ dfckit/io/xcpd.py,sha256=Mu7VA7szewDYK0B6ZC80MLYDOTGiHP1ZRsHemx4CaoU,17499
48
+ dfckit/states/__init__.py,sha256=1o_OvbJHnJ7XjIUUfo6JBBxu6xDxS4Mbvc90WclB-9k,2664
49
+ dfckit/states/alignment.py,sha256=JBoUMWdj0k2SyFw-Pvgovivm4Xf1L8P9m82ogjywIQg,16617
50
+ dfckit/states/cap.py,sha256=Xa2Le_en_zhX-YQQbeD4TbO0UQ_S2UlkfGCFqnet9Ok,2251
51
+ dfckit/states/cross_validation.py,sha256=EHTyHflanmrc0GHT4WcyB2ANEs7C_gz3tfXRNeg2FcI,4516
52
+ dfckit/states/data.py,sha256=GNJ-9dX4_x2ZehlUaDoYMFXsS5oiFXRRL8LR6hxgZbs,17331
53
+ dfckit/states/hmm.py,sha256=GefBHehuHVMf-eUUWZFU459ps-YFqjCTdWw2WIlyDTE,15392
54
+ dfckit/states/interpretation.py,sha256=7I3_qX4epopk7kTArKyajvzXOGQgL7sWBt2KpqhMpHo,9983
55
+ dfckit/states/kmeans.py,sha256=5x35H8KpnL-NQTORjU6n3l0JL3NfWumf49LlkMSRav8,14218
56
+ dfckit/states/metrics.py,sha256=t45phWMTFK0rXfN28SRS-oj3f8SINVHKYnUn2W_2SMU,3726
57
+ dfckit/states/scoring.py,sha256=uG1FRrskrSpsRv8OP37d1h5DUV4oY6Pub3ssU9jox4o,4201
58
+ dfckit/states/selection.py,sha256=AQA4DmKta4Jjea2oSKq_SuBmQJlgzv-Cx75shIqdv5Y,21829
59
+ dfckit/states/stability.py,sha256=MaSMTa8zFkW7KNNn1rFXKiUk95LcjKv7NNOJS_Bn84A,7709
60
+ dfckit/states/streaming.py,sha256=LP6Dvrr9Bry8MHKxuD6T8D3NgcwFwgR2LX6uLJVBfm4,34964
61
+ dfckit/states/streaming_hmm.py,sha256=ZwzPc1AGL6OWLsBM17tt29xBvghPtjYDYRwbd_9bW4Y,16947
62
+ dfckit/storage/__init__.py,sha256=QbQmYKXRF14bD-lJDSnlu6U8h68-kYq7B0s6u8i-_pU,642
63
+ dfckit/storage/_statistics.py,sha256=MOZsg-JlbYEypzzxMEwmHtpDw6wQSXlFSLLbCPgX9cQ,2693
64
+ dfckit/storage/builders.py,sha256=Kj9mNBTExWzphzsjtSoq_goEDf71ihWJHFyobWBcJK0,11864
65
+ dfckit/storage/store.py,sha256=A8sLsG21G6hIdz98LZ3CTAPPNKz6KWnSBEdP-_T-Reg,25405
66
+ dfckit/storage/summary.py,sha256=PMFqLZzMfoIQe9HYPXPfol7b8NLKAEdUigr49eb-N_s,4898
67
+ dfc_kit-1.0.0.dist-info/METADATA,sha256=QwEZmbTqDwRox8kXOJmSJ9Z__I4Jr_fgvUvM8Txd9SU,6841
68
+ dfc_kit-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
69
+ dfc_kit-1.0.0.dist-info/entry_points.txt,sha256=SWlP-V-AosW-cKE7Ji2ItNeHXnzpf2WLtVjDHlQtXWw,44
70
+ dfc_kit-1.0.0.dist-info/top_level.txt,sha256=aZFx4KeqTtnkO3XFb6XLsYLRJvgNi2VqONu0A5psdk0,7
71
+ dfc_kit-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dfc-kit = dfckit.cli:main
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, dfc-kit contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ dfckit
dfckit/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Censor-aware dynamic functional connectivity tools."""
2
+
3
+ from .data import TimeSeriesDataset, TimeSeriesRun, TimeWindow, validate_subject_disjoint
4
+
5
+ __all__ = [
6
+ "TimeSeriesDataset",
7
+ "TimeSeriesRun",
8
+ "TimeWindow",
9
+ "validate_subject_disjoint",
10
+ ]
11
+ __version__ = "1.0.0"
dfckit/_arrays.py ADDED
@@ -0,0 +1,13 @@
1
+ """Internal NumPy array ownership helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from numpy.typing import ArrayLike, NDArray
7
+
8
+
9
+ def readonly_copy(values: ArrayLike) -> NDArray:
10
+ """Return an independent NumPy array with mutation disabled."""
11
+ output = np.asarray(values).copy()
12
+ output.setflags(write=False)
13
+ return output
@@ -0,0 +1,46 @@
1
+ """Shared preprocessing for censor-bounded time series."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from ._arrays import readonly_copy as _readonly
9
+ from .data import TimeSeriesRun
10
+
11
+
12
+ def _segment_standardized_samples(
13
+ run: TimeSeriesRun,
14
+ *,
15
+ method_name: str = "ETS",
16
+ ) -> tuple[NDArray[np.float64], NDArray[np.int64], NDArray[np.int64]]:
17
+ """Z-score each ROI within each retained contiguous segment.
18
+
19
+ Segments shorter than two frames are omitted. Constant ROI columns become
20
+ zero after centering, and returned rows retain their original frame and
21
+ segment identities.
22
+ """
23
+ samples: list[NDArray[np.float64]] = []
24
+ original_indices: list[NDArray[np.int64]] = []
25
+ segment_ids: list[NDArray[np.int64]] = []
26
+
27
+ for segment_id, positions in enumerate(run.segments()):
28
+ if len(positions) < 2:
29
+ continue
30
+ values = run.values[positions]
31
+ scale = values.std(axis=0, ddof=0)
32
+ scale = np.where(scale < 1e-8, 1.0, scale)
33
+ samples.append((values - values.mean(axis=0)) / scale)
34
+ original_indices.append(run.original_indices[positions])
35
+ segment_ids.append(np.full(len(positions), segment_id, dtype=np.int64))
36
+
37
+ if not samples:
38
+ raise ValueError(f"{method_name} requires at least one retained segment with two frames")
39
+ standardized = np.concatenate(samples, axis=0)
40
+ if not np.isfinite(standardized).all():
41
+ raise ValueError(f"standardized {method_name} samples contain non-finite values")
42
+ return (
43
+ _readonly(standardized),
44
+ _readonly(np.concatenate(original_indices)),
45
+ _readonly(np.concatenate(segment_ids)),
46
+ )
dfckit/_validation.py ADDED
@@ -0,0 +1,87 @@
1
+ """Internal validation shared across method families."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Sequence
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+
11
+ def validated_integer(value: object, *, label: str, minimum: int = 0) -> int:
12
+ """Return a Python integer after type and lower-bound validation."""
13
+ if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)):
14
+ raise TypeError(f"{label} must be an integer")
15
+ if value < minimum:
16
+ raise ValueError(f"{label} must be at least {minimum}")
17
+ return int(value)
18
+
19
+
20
+ def validated_seed(value: object, *, label: str) -> int:
21
+ """Return a non-negative Python integer suitable as a random seed."""
22
+ return validated_integer(value, label=label, minimum=0)
23
+
24
+
25
+ def validated_positive_integer(value: object, label: str) -> int:
26
+ """Validate an integer using the legacy positive-value error contract."""
27
+ if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)):
28
+ raise TypeError(f"{label} must be an integer")
29
+ if value < 1:
30
+ raise ValueError(f"{label} must be positive")
31
+ return int(value)
32
+
33
+
34
+ def validated_nonnegative_integer(value: object, label: str) -> int:
35
+ """Validate an integer using the legacy non-negative error contract."""
36
+ if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)):
37
+ raise TypeError(f"{label} must be an integer")
38
+ if value < 0:
39
+ raise ValueError(f"{label} must be non-negative")
40
+ return int(value)
41
+
42
+
43
+ def validated_roi_indices(
44
+ nodes: Iterable[int],
45
+ *,
46
+ n_rois: int,
47
+ label: str,
48
+ minimum: int = 1,
49
+ ) -> NDArray[np.int64]:
50
+ """Validate unique integer ROI indices against a known ROI axis."""
51
+ raw = tuple(nodes)
52
+ if len(raw) < minimum:
53
+ quantity = "one ROI index" if minimum == 1 else f"{minimum} ROI indices"
54
+ raise ValueError(f"{label} must contain at least {quantity}")
55
+ if any(
56
+ isinstance(node, (bool, np.bool_)) or not isinstance(node, (int, np.integer))
57
+ for node in raw
58
+ ):
59
+ raise TypeError(f"{label} must contain integer ROI indices")
60
+ output = np.asarray(raw, dtype=np.int64)
61
+ if len(set(output.tolist())) != len(output):
62
+ raise ValueError(f"{label} contains duplicate ROI indices")
63
+ if np.any(output < 0) or np.any(output >= n_rois):
64
+ raise ValueError(f"{label} contains an ROI index outside [0, {n_rois})")
65
+ return output
66
+
67
+
68
+ def validated_subject_labels(subjects: Iterable[str], *, n_observations: int) -> tuple[str, ...]:
69
+ """Validate one non-empty subject label per observation, allowing repeats."""
70
+ output = tuple(str(subject) for subject in subjects)
71
+ if len(output) != n_observations:
72
+ raise ValueError("subjects must contain one identifier per observation")
73
+ if any(not subject.strip() for subject in output):
74
+ raise ValueError("subject identifiers must be non-empty")
75
+ return output
76
+
77
+
78
+ def validated_subject_ids(subject_ids: Sequence[str], n_observations: int) -> tuple[str, ...]:
79
+ """Validate one unique, non-empty participant ID per observation."""
80
+ identifiers = tuple(str(subject) for subject in subject_ids)
81
+ if len(identifiers) != n_observations:
82
+ raise ValueError("subject_ids must match the number of observations")
83
+ if any(not subject.strip() for subject in identifiers):
84
+ raise ValueError("subject_ids cannot contain empty identifiers")
85
+ if len(set(identifiers)) != len(identifiers):
86
+ raise ValueError("subject_ids must contain one unique entry per participant")
87
+ return identifiers
@@ -0,0 +1,36 @@
1
+ """Portable model, prediction, score, and alignment artifacts."""
2
+
3
+ from .models import (
4
+ FittedModel,
5
+ load_fitted_model,
6
+ save_fitted_model,
7
+ )
8
+ from .state_alignment import load_state_alignment, save_state_alignment
9
+ from .state_results import (
10
+ StatePredictions,
11
+ load_state_predictions,
12
+ save_state_predictions,
13
+ write_state_metrics,
14
+ )
15
+ from .state_scoring import (
16
+ StateModelScoreReport,
17
+ load_state_model_scores,
18
+ write_state_model_scores,
19
+ )
20
+ from .state_stability import write_state_stability
21
+
22
+ __all__ = [
23
+ "FittedModel",
24
+ "StateModelScoreReport",
25
+ "StatePredictions",
26
+ "load_fitted_model",
27
+ "load_state_alignment",
28
+ "load_state_model_scores",
29
+ "load_state_predictions",
30
+ "save_fitted_model",
31
+ "save_state_alignment",
32
+ "save_state_predictions",
33
+ "write_state_metrics",
34
+ "write_state_model_scores",
35
+ "write_state_stability",
36
+ ]
@@ -0,0 +1,67 @@
1
+ """Scalar and collection validation for persisted artifact schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import pairwise
6
+
7
+ import numpy as np
8
+
9
+
10
+ def artifact_integer(value: object, name: str, *, minimum: int = 0) -> int:
11
+ """Validate an integer artifact field using schema-style errors."""
12
+ if (
13
+ isinstance(value, (bool, np.bool_))
14
+ or not isinstance(value, (int, np.integer))
15
+ or value < minimum
16
+ ):
17
+ raise ValueError(f"{name} must be an integer of at least {minimum}")
18
+ return int(value)
19
+
20
+
21
+ def artifact_finite_float(
22
+ value: object,
23
+ name: str,
24
+ *,
25
+ positive: bool = False,
26
+ ) -> float:
27
+ """Validate a finite numeric artifact field."""
28
+ if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, float)):
29
+ raise TypeError(f"{name} must be numeric")
30
+ output = float(value)
31
+ if not np.isfinite(output) or (positive and output <= 0.0):
32
+ qualifier = "finite and positive" if positive else "finite"
33
+ raise ValueError(f"{name} must be {qualifier}")
34
+ return output
35
+
36
+
37
+ def artifact_integer_grid(
38
+ value: object,
39
+ name: str,
40
+ *,
41
+ minimum: int,
42
+ minimum_count: int,
43
+ ) -> tuple[int, ...]:
44
+ """Validate a strictly increasing integer grid in an artifact."""
45
+ if not isinstance(value, list) or len(value) < minimum_count:
46
+ raise ValueError(f"{name} must contain at least {minimum_count} values")
47
+ output = tuple(artifact_integer(item, name, minimum=minimum) for item in value)
48
+ if len(set(output)) != len(output) or any(
49
+ right <= left for left, right in pairwise(output)
50
+ ):
51
+ raise ValueError(f"{name} must be strictly increasing and unique")
52
+ return output
53
+
54
+
55
+ def sample_intervals_match(left: object, right: float | None) -> bool:
56
+ """Compare nullable positive sample intervals with fixed absolute tolerance."""
57
+ if left is None or right is None:
58
+ return left is None and right is None
59
+ try:
60
+ observed = artifact_finite_float(
61
+ left,
62
+ "sample_interval_seconds",
63
+ positive=True,
64
+ )
65
+ except (TypeError, ValueError):
66
+ return False
67
+ return bool(np.isclose(observed, right, rtol=0.0, atol=1e-9))
@@ -0,0 +1,92 @@
1
+ """Strict JSON persistence shared by artifact modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def strict_object_hook(context: str) -> Callable[[list[tuple[str, object]]], dict[str, object]]:
14
+ """Return an object-pairs hook that rejects duplicate JSON fields."""
15
+
16
+ def hook(pairs: list[tuple[str, object]]) -> dict[str, object]:
17
+ output: dict[str, object] = {}
18
+ for key, value in pairs:
19
+ if key in output:
20
+ raise ValueError(f"duplicate JSON field in {context}: {key}")
21
+ output[key] = value
22
+ return output
23
+
24
+ return hook
25
+
26
+
27
+ def nonstandard_constant_hook(context: str) -> Callable[[str], object]:
28
+ """Return a parse hook that rejects NaN and infinite JSON constants."""
29
+
30
+ def hook(value: str) -> object:
31
+ raise ValueError(f"non-standard JSON constant in {context}: {value}")
32
+
33
+ return hook
34
+
35
+
36
+ def load_json_object(path: str | Path, *, context: str) -> dict[str, Any]:
37
+ """Read one finite JSON object while rejecting duplicate fields."""
38
+ source = Path(path)
39
+ if not source.is_file():
40
+ raise FileNotFoundError(f"{context} does not exist: {source}")
41
+ try:
42
+ value = json.loads(
43
+ source.read_text(encoding="utf-8"),
44
+ object_pairs_hook=strict_object_hook(context),
45
+ parse_constant=nonstandard_constant_hook(context),
46
+ )
47
+ except (OSError, json.JSONDecodeError) as error:
48
+ raise ValueError(f"cannot read {context} {source}: {error}") from error
49
+ if not isinstance(value, dict):
50
+ raise TypeError(f"{context} must be a JSON object")
51
+ return value
52
+
53
+
54
+ def write_json_atomic(
55
+ path: str | Path,
56
+ payload: object,
57
+ *,
58
+ overwrite: bool = False,
59
+ ) -> Path:
60
+ """Atomically write finite JSON, creating or replacing one regular file."""
61
+ target = Path(path)
62
+ if not overwrite and (target.exists() or target.is_symlink()):
63
+ raise FileExistsError(f"JSON output already exists: {target}")
64
+ target.parent.mkdir(parents=True, exist_ok=True)
65
+ descriptor, temporary_name = tempfile.mkstemp(
66
+ prefix=f".{target.name}.tmp-",
67
+ dir=target.parent,
68
+ text=True,
69
+ )
70
+ temporary = Path(temporary_name)
71
+ try:
72
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
73
+ json.dump(payload, stream, indent=2, sort_keys=True, allow_nan=False)
74
+ stream.write("\n")
75
+ if overwrite:
76
+ os.replace(temporary, target)
77
+ else:
78
+ if target.exists() or target.is_symlink():
79
+ raise FileExistsError(f"JSON output already exists: {target}")
80
+ os.rename(temporary, target)
81
+ except BaseException:
82
+ temporary.unlink(missing_ok=True)
83
+ raise
84
+ return target
85
+
86
+
87
+ __all__ = [
88
+ "load_json_object",
89
+ "nonstandard_constant_hook",
90
+ "strict_object_hook",
91
+ "write_json_atomic",
92
+ ]