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,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,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
|
dfckit/_preprocessing.py
ADDED
|
@@ -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
|
+
]
|