dfc-kit 1.0.1__tar.gz → 1.0.2__tar.gz

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 (184) hide show
  1. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/CHANGELOG.md +13 -1
  2. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/CITATION.cff +2 -2
  3. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/MANIFEST.in +0 -3
  4. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/PKG-INFO +1 -1
  5. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/api.md +8 -1
  6. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/cli.md +25 -0
  7. dfc_kit-1.0.2/docs/granger.md +35 -0
  8. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/nbs.md +30 -0
  9. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/states.md +60 -0
  10. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/storage.md +31 -0
  11. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/mkdocs.yml +1 -0
  12. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/pyproject.toml +1 -1
  13. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/PKG-INFO +1 -1
  14. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/SOURCES.txt +6 -3
  15. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/__init__.py +1 -1
  16. dfc_kit-1.0.2/src/dfckit/_feature_selection.py +43 -0
  17. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/models.py +14 -0
  18. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/parser.py +37 -1
  19. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/reporting.py +6 -2
  20. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/states.py +38 -3
  21. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/__init__.py +8 -0
  22. dfc_kit-1.0.2/src/dfckit/connectivity/granger.py +243 -0
  23. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/data.py +82 -16
  24. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/interpretation.py +3 -0
  25. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/kmeans.py +103 -3
  26. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/streaming.py +2 -0
  27. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/storage/__init__.py +2 -1
  28. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/storage/store.py +158 -9
  29. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/storage/summary.py +40 -24
  30. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_cli.py +148 -0
  31. dfc_kit-1.0.2/tests/unit/test_feature_selection.py +117 -0
  32. dfc_kit-1.0.2/tests/unit/test_granger.py +73 -0
  33. dfc_kit-1.0.2/tests/unit/test_granger_optional_dependency.py +49 -0
  34. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_model_io.py +30 -0
  35. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_states.py +76 -1
  36. dfc_kit-1.0.2/tests/unit/test_store_summary.py +355 -0
  37. dfc_kit-1.0.1/ENGINEERING_PLAN.md +0 -42
  38. dfc_kit-1.0.1/ROADMAP.md +0 -26
  39. dfc_kit-1.0.1/SECURITY.md +0 -12
  40. dfc_kit-1.0.1/tests/unit/test_store_summary.py +0 -137
  41. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/CONTRIBUTING.md +0 -0
  42. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/LICENSE +0 -0
  43. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/README.md +0 -0
  44. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/benchmarks/README.md +0 -0
  45. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/benchmarks/profile_core.py +0 -0
  46. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/benchmarks/profile_information.py +0 -0
  47. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/architecture.md +0 -0
  48. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/correlation.md +0 -0
  49. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/datasets.md +0 -0
  50. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/getting_started.md +0 -0
  51. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/group_inference.md +0 -0
  52. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/hmm.md +0 -0
  53. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/index.md +0 -0
  54. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/inference.md +0 -0
  55. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/inference_matching.md +0 -0
  56. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/information.md +0 -0
  57. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/instantaneous_edges.md +0 -0
  58. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/leida.md +0 -0
  59. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/lowrank.md +0 -0
  60. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/method_inventory.md +0 -0
  61. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/model_artifacts.md +0 -0
  62. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/partition_graphs.md +0 -0
  63. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/reference.md +0 -0
  64. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/release.md +0 -0
  65. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/state_alignment.md +0 -0
  66. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/state_scoring.md +0 -0
  67. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/state_selection.md +0 -0
  68. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/state_stability.md +0 -0
  69. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/tutorial_xcpd_to_states.md +0 -0
  70. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/docs/xcpd_input.md +0 -0
  71. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/setup.cfg +0 -0
  72. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/dependency_links.txt +0 -0
  73. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/entry_points.txt +0 -0
  74. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/requires.txt +0 -0
  75. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfc_kit.egg-info/top_level.txt +0 -0
  76. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/_arrays.py +0 -0
  77. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/_preprocessing.py +0 -0
  78. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/_validation.py +0 -0
  79. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/__init__.py +0 -0
  80. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/_fields.py +0 -0
  81. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/_json.py +0 -0
  82. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/_numpy.py +0 -0
  83. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/state_alignment.py +0 -0
  84. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/state_results.py +0 -0
  85. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/state_scoring.py +0 -0
  86. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/artifacts/state_stability.py +0 -0
  87. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/cli.py +0 -0
  88. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/__init__.py +0 -0
  89. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/source.py +0 -0
  90. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/commands/stability.py +0 -0
  91. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/_edge_products.py +0 -0
  92. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/correlation.py +0 -0
  93. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/instantaneous.py +0 -0
  94. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/leida.py +0 -0
  95. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/lowrank.py +0 -0
  96. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/partition.py +0 -0
  97. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/connectivity/windows.py +0 -0
  98. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/data.py +0 -0
  99. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/__init__.py +0 -0
  100. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/endpoints.py +0 -0
  101. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/hc3.py +0 -0
  102. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/independent_endpoints.py +0 -0
  103. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/matching.py +0 -0
  104. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/multiple_testing.py +0 -0
  105. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/nbs.py +0 -0
  106. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/nbs_endpoints.py +0 -0
  107. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/paired.py +0 -0
  108. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/inference/state_metrics.py +0 -0
  109. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/information/__init__.py +0 -0
  110. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/information/_artifact.py +0 -0
  111. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/information/estimators.py +0 -0
  112. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/information/fixed.py +0 -0
  113. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/information/summary.py +0 -0
  114. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/io/__init__.py +0 -0
  115. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/io/xcpd.py +0 -0
  116. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/reference.py +0 -0
  117. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/segments.py +0 -0
  118. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/__init__.py +0 -0
  119. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/alignment.py +0 -0
  120. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/cap.py +0 -0
  121. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/cross_validation.py +0 -0
  122. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/hmm.py +0 -0
  123. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/metrics.py +0 -0
  124. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/scoring.py +0 -0
  125. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/selection.py +0 -0
  126. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/stability.py +0 -0
  127. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/states/streaming_hmm.py +0 -0
  128. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/storage/_statistics.py +0 -0
  129. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/src/dfckit/storage/builders.py +0 -0
  130. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/__init__.py +0 -0
  131. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/fixtures/audited_kernels.json +0 -0
  132. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/__init__.py +0 -0
  133. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_leida_state_pipeline.py +0 -0
  134. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_model_artifact_pipeline.py +0 -0
  135. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_information.py +0 -0
  136. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_lowrank.py +0 -0
  137. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_sliding_window.py +0 -0
  138. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_state_pipeline.py +0 -0
  139. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_static_nbs.py +0 -0
  140. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/integration/test_xcpd_window_pattern.py +0 -0
  141. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/regression/__init__.py +0 -0
  142. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/regression/test_audited_kernels.py +0 -0
  143. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/regression/test_information_reference.py +0 -0
  144. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/regression/test_nbs_independent_reference.py +0 -0
  145. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/regression/test_nbs_reference.py +0 -0
  146. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/__init__.py +0 -0
  147. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_artifact_helpers.py +0 -0
  148. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_correlation.py +0 -0
  149. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_cross_validation.py +0 -0
  150. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_data.py +0 -0
  151. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_endpoint_inference.py +0 -0
  152. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_ets.py +0 -0
  153. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_hmm.py +0 -0
  154. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_independent_endpoint_inference.py +0 -0
  155. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_inference.py +0 -0
  156. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_inference_matching.py +0 -0
  157. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_information.py +0 -0
  158. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_information_io.py +0 -0
  159. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_information_parallel.py +0 -0
  160. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_information_summary.py +0 -0
  161. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_internal_helpers.py +0 -0
  162. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_leida.py +0 -0
  163. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_lowrank.py +0 -0
  164. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_mtd.py +0 -0
  165. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_nbs.py +0 -0
  166. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_nbs_endpoints.py +0 -0
  167. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_outofcore.py +0 -0
  168. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_outofcore_hmm.py +0 -0
  169. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_partition_graph.py +0 -0
  170. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_preprocessing.py +0 -0
  171. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_public_api.py +0 -0
  172. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_reference.py +0 -0
  173. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_sliding_window.py +0 -0
  174. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_stability.py +0 -0
  175. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_alignment_io.py +0 -0
  176. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_interpretation.py +0 -0
  177. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_metric_inference.py +0 -0
  178. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_results.py +0 -0
  179. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_scoring_io.py +0 -0
  180. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_selection.py +0 -0
  181. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_selection_io.py +0 -0
  182. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_state_stability_io.py +0 -0
  183. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_storage.py +0 -0
  184. {dfc_kit-1.0.1 → dfc_kit-1.0.2}/tests/unit/test_xcpd.py +0 -0
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## 1.0.1 (unreleased)
3
+ ## 1.0.2 - 2026-09-11
4
+
5
+ - Added exact-feature selection for in-memory and disk-backed state models,
6
+ including clustering on a fixed NBS component's edges.
7
+ - Added conditional Granger predictability with censor-bounded lagged samples
8
+ and VAR diagnostics. SciPy is loaded only when those diagnostics are used,
9
+ preserving the NumPy-only base installation.
10
+ - FeatureStore summaries can now aggregate a named feature set within each
11
+ sample before computing acquisition-level mean, variance, standard deviation,
12
+ and extrema. The CLI exposes `--feature-keys` and `--feature-mean NAME` for
13
+ fixed-network summaries, including sparse NBS components.
14
+
15
+ ## 1.0.1
4
16
 
5
17
  - Fixed-length MI/CMI now shares one process pool across acquisition, window
6
18
  length, and draw combinations. Bounded task submission avoids materializing
@@ -4,6 +4,6 @@ title: "dfc-kit: Censor-aware dynamic functional connectivity tools"
4
4
  type: software
5
5
  authors:
6
6
  - name: "dfc-kit contributors"
7
- version: 1.0.1
8
- date-released: 2026-09-05
7
+ version: 1.0.2
8
+ date-released: 2026-09-11
9
9
  license: BSD-3-Clause
@@ -1,11 +1,8 @@
1
1
  include CHANGELOG.md
2
2
  include CITATION.cff
3
3
  include CONTRIBUTING.md
4
- include ENGINEERING_PLAN.md
5
4
  include LICENSE
6
5
  include README.md
7
- include ROADMAP.md
8
- include SECURITY.md
9
6
  include mkdocs.yml
10
7
  recursive-include benchmarks *.md *.py
11
8
  recursive-include docs *.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfc-kit
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Censor-aware tools for dynamic functional connectivity analysis
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/yidao9518/dfc-kit
@@ -60,10 +60,17 @@ store = FeatureStore.create(
60
60
  sample_interval_seconds=result.sample_interval_seconds,
61
61
  )
62
62
  store.append_dataset(dataset)
63
+
64
+ # Select exact edges without duplicating the disk-backed store.
65
+ selected = store.select_features(
66
+ feature_keys=(("V1_L", "PUT-DP_L"), ("V1_R", "PUT-DP_R"))
67
+ )
63
68
  ```
64
69
 
65
70
  FeatureStore chunks are memory-mappable and retain sequence/acquisition
66
- boundaries. They are an execution format, not a provenance database.
71
+ boundaries. A selected feature view preserves those boundaries and is accepted
72
+ by the store-based state fitters. FeatureStore objects are an execution format,
73
+ not a provenance database.
67
74
 
68
75
  ## State models
69
76
 
@@ -32,6 +32,13 @@ dfc-kit fit-states /path/to/features.store /path/to/models/k4.model \
32
32
  --max-iter 100 --streaming-tol 1e-4 \
33
33
  --streaming-patience 3 --streaming-min-passes 2
34
34
 
35
+ # Exact ordered edge subset with equal participant-session influence.
36
+ dfc-kit fit-states /path/to/features.store /path/to/models/k2.model \
37
+ --method kmeans --n-states 2 --seed 20260907 \
38
+ --feature-keys /path/to/edges.json \
39
+ --fitting-mode materialized --algorithm lloyd \
40
+ --sample-weight-mode subject_session_balanced
41
+
35
42
  dfc-kit predict-states /path/to/features.store /path/to/models/k4.model \
36
43
  /path/to/results/k4.labels --subject sub-010
37
44
 
@@ -177,6 +184,24 @@ and `maximum`. Variance and standard deviation use all retained samples with
177
184
  `ddof=0`. Censor-delimited segments remain distinct during feature generation,
178
185
  then their valid samples are combined by sample count within the acquisition.
179
186
 
187
+ To summarize a fixed NBS component, supply its exact ROI-pair keys as a JSON
188
+ array (for example, `[["V1_L", "PUT-DP_L"], ["V1_R", "PUT-DP_R"]]`):
189
+
190
+ ```bash
191
+ dfc-kit summarize-store features/window-fc.store results/nbs24-summary.json \
192
+ --feature-keys nbs24-edges.json --feature-mean NBS24 \
193
+ --statistic mean --statistic standard_deviation
194
+ ```
195
+
196
+ `--feature-keys` selects columns. `--feature-mean NAME` averages those columns
197
+ equally within each sample, then summarizes the resulting network time series
198
+ under `NAME.mean`, `NAME.standard_deviation`, and any other requested statistics.
199
+ With no key file, the mean uses all store features; with no `--feature-mean`,
200
+ selected features are summarized separately. Signed values and source units
201
+ are preserved. Network standard deviation is computed **after** averaging
202
+ edges, not by averaging their individual standard deviations. The resulting
203
+ endpoint file can be used with `infer-paired-endpoints`.
204
+
180
205
  Use `summarize-states`, `summarize-information`, `describe-states`,
181
206
  `infer-state-metrics`, `infer-paired-endpoints`, and `infer-paired-nbs` for
182
207
  reporting and inference. Every command writes finite JSON and refuses
@@ -0,0 +1,35 @@
1
+ # Conditional Granger predictability
2
+
3
+ Install the inference dependency before running this analysis:
4
+
5
+ ```bash
6
+ python -m pip install 'dfc-kit[inference]'
7
+ ```
8
+
9
+ `conditional_granger()` fits pooled linear VAR models while constructing every
10
+ lagged row inside one contiguous retained-frame segment. Its Geweke statistic is
11
+ the log ratio of restricted to unrestricted target-residual variance (or
12
+ generalized variance for a multivariate target). The restricted model removes
13
+ source lags and retains target and conditioning lags.
14
+
15
+ ```python
16
+ from dfckit.connectivity import conditional_granger
17
+
18
+ result = conditional_granger(
19
+ run,
20
+ source=[0],
21
+ target=[1],
22
+ conditioning=[2, 3],
23
+ lag_order=2,
24
+ )
25
+ print(result.geweke, result.spectral_radius, result.whiteness.min_ljung_box_p)
26
+ ```
27
+
28
+ The result reports residual degrees of freedom, VAR spectral radius, and a
29
+ Ljung--Box residual-autocorrelation diagnostic. Segment standardization is
30
+ enabled by default. These estimates describe lagged linear predictability in the
31
+ processed time series and do not by themselves establish neural causality.
32
+
33
+ References: Geweke (1982), *Journal of the American Statistical Association*,
34
+ doi:10.1080/01621459.1982.10477803; Barnett and Seth (2014), *Journal of
35
+ Neuroscience Methods*, doi:10.1016/j.jneumeth.2013.10.018.
@@ -98,6 +98,36 @@ tests reconstruct the OLS statistics with NumPy and the graph components with
98
98
  SciPy, and compare observed components and complete null arrays for both sign
99
99
  modes and for unadjusted and confound-adjusted analyses.
100
100
 
101
+ ## Summarize a component's connection strength
102
+
103
+ NBS reports component size or a sum of edge t statistics; neither is its mean
104
+ FC. To measure a fixed component in every acquisition, take its exact
105
+ `component_edges` from an `infer-paired-nbs` result and select those keys in
106
+ the FC store:
107
+
108
+ ```python
109
+ from dfckit.storage import FeatureStore, summarize_store_statistics
110
+
111
+ store = FeatureStore.open("features/window-fc.store")
112
+ selected = store.select_features(feature_keys=component_edges)
113
+ network_endpoints = summarize_store_statistics(
114
+ selected,
115
+ statistics=("mean", "standard_deviation"),
116
+ feature_mean="NBS_component",
117
+ )
118
+ ```
119
+
120
+ This averages the selected signed edge values in each window, then computes
121
+ their network mean and temporal variability within each acquisition. Use the
122
+ same fixed edge set in every condition. The stored edge names, not node
123
+ membership alone, define the component: adding all connections between its
124
+ nodes would include edges that NBS did not select. A mean of window FC is also
125
+ distinct from whole-acquisition static FC.
126
+
127
+ The generic endpoint output supports paired inference. Testing the contrast
128
+ that selected a component on the same participants is a descriptive follow-up,
129
+ not an independent confirmation of the NBS discovery.
130
+
101
131
  ## References
102
132
 
103
133
  - Zalesky A, Fornito A, Bullmore ET. Network-based statistic: identifying
@@ -40,6 +40,16 @@ directly in the original edge-feature space:
40
40
  window FC edges -> training-set feature scaling -> KMeans
41
41
  ```
42
42
 
43
+ The default `sample_weight_mode="uniform"` gives every window equal weight and
44
+ preserves the original fitting behavior. For cohorts with unequal usable scan
45
+ lengths, `sample_weight_mode="subject_session_balanced"` first gives every
46
+ participant equal total weight, then divides that weight equally among the
47
+ participant's sessions and their retained windows. The weighted mean and
48
+ variance are used for feature scaling, and the same weights enter the Lloyd
49
+ KMeans objective. The model records the weight sums and Kish effective row
50
+ count; this diagnostic is not a count of statistically independent samples.
51
+ Balanced weighting currently requires materialized Lloyd KMeans without PCA.
52
+
43
53
  For a lower-dimensional sensitivity path, set `n_pca_components`. PCA is fitted
44
54
  only on the training participants, and the frozen scaler and PCA basis are
45
55
  applied before KMeans:
@@ -123,6 +133,56 @@ The prediction artifact retains the original sequence identity and sample
123
133
  indices. The summary command applies the same gap-safe
124
134
  `summarize_state_assignments` implementation used by the Python API.
125
135
 
136
+ ## Clustering a specified feature or edge set
137
+
138
+ State fitting uses every input feature by default. Restrict an in-memory
139
+ feature dataset by exact feature identity before fitting:
140
+
141
+ ```python
142
+ selected = training_sequences.select_features(
143
+ feature_keys=(
144
+ ("V1_L", "PUT-DP_L"),
145
+ ("V1_R", "PUT-DP_R"),
146
+ )
147
+ )
148
+ fit = fit_kmeans_states(selected, n_states=2, seed=20260907)
149
+ ```
150
+
151
+ For a disk-backed store, `select_features` returns a read-only column view. It
152
+ does not rewrite the store and works with materialized or streaming state
153
+ fitting:
154
+
155
+ ```python
156
+ store = FeatureStore.open("/path/to/window_fc.store")
157
+ selected_store = store.select_features(feature_mask=nbs_edge_mask)
158
+ fit = fit_minibatch_kmeans_store(
159
+ selected_store,
160
+ n_states=2,
161
+ seed=20260907,
162
+ )
163
+ ```
164
+
165
+ `feature_keys` are exact identities and determine output order. A Boolean
166
+ `feature_mask` must have one entry per source feature and preserves source
167
+ order. Fitted models record only the selected keys, so prediction must use the
168
+ same selected view.
169
+
170
+ The command line accepts the same exact ordered selection as a JSON array of
171
+ feature-key arrays:
172
+
173
+ ```bash
174
+ dfc-kit fit-states window-fc.store models/nbs-k2.model \
175
+ --method kmeans --n-states 2 --feature-keys nbs-edges.json \
176
+ --fitting-mode materialized --algorithm lloyd \
177
+ --sample-weight-mode subject_session_balanced
178
+ dfc-kit predict-states window-fc.store models/nbs-k2.model labels \
179
+ --feature-keys nbs-edges.json --allow-fit-subjects
180
+ ```
181
+
182
+ Participant-session-balanced weighting gives every acquisition sequence equal
183
+ total influence regardless of its number of windows. It is supported only by
184
+ materialized Lloyd KMeans; incompatible engines are rejected explicitly.
185
+
126
186
  ## CAP
127
187
 
128
188
  ```python
@@ -176,6 +176,37 @@ or `feature_12.variance`. For edge FeatureStores, one selected statistic at a
176
176
  time can be reshaped into a participants-by-edges matrix for NBS. Mixing two
177
177
  statistics in one NBS graph would duplicate every edge and is invalid.
178
178
 
179
+ ### Mean strength and variability of a fixed network
180
+
181
+ Select exact edge keys, then name the feature mean to summarize the network
182
+ instead of each edge separately:
183
+
184
+ ```python
185
+ selected = store.select_features(feature_keys=component_edges)
186
+ payload = summarize_store_statistics(
187
+ selected,
188
+ statistics=("mean", "standard_deviation"),
189
+ feature_mean="NBS24",
190
+ )
191
+ ```
192
+
193
+ For each sample, the selected edges are averaged with equal weights. The
194
+ resulting network time series is then summarized separately for each
195
+ participant, session, and acquisition. `NBS24.mean` is its temporal mean;
196
+ `NBS24.standard_deviation` is its population standard deviation, **not** the
197
+ average of the individual edges' standard deviations. Each retained sample
198
+ has equal weight, including when segments or storage chunks differ in length.
199
+
200
+ Values keep the store's scale and sign: a Fisher-z window-FC store produces
201
+ means of signed Fisher-z values, without taking absolute values or applying
202
+ another transform. These are mean-window FC summaries, not whole-acquisition
203
+ static FC. A store with one static FC row per acquisition instead gives its
204
+ across-edge mean and a zero temporal standard deviation.
205
+
206
+ The output lists `source_feature_keys` and marks `feature_type="feature_set"`.
207
+ It can enter `infer-paired-endpoints`, but is no longer an edge matrix for
208
+ `infer-paired-nbs`. Omitting `feature_mean` retains per-feature summaries.
209
+
179
210
  ## Stream MiniBatchKMeans
180
211
 
181
212
  Feature stores can be fitted directly without calling `read_dataset()`:
@@ -16,6 +16,7 @@ nav:
16
16
  - Architecture: architecture.md
17
17
  - Connectivity methods:
18
18
  - Correlation and sliding-window FC: correlation.md
19
+ - Conditional Granger predictability: granger.md
19
20
  - Instantaneous edges (ETS and MTD): instantaneous_edges.md
20
21
  - LEiDA: leida.md
21
22
  - Low-rank covariance: lowrank.md
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dfc-kit"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Censor-aware tools for dynamic functional connectivity analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfc-kit
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Censor-aware tools for dynamic functional connectivity analysis
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/yidao9518/dfc-kit
@@ -1,12 +1,9 @@
1
1
  CHANGELOG.md
2
2
  CITATION.cff
3
3
  CONTRIBUTING.md
4
- ENGINEERING_PLAN.md
5
4
  LICENSE
6
5
  MANIFEST.in
7
6
  README.md
8
- ROADMAP.md
9
- SECURITY.md
10
7
  mkdocs.yml
11
8
  pyproject.toml
12
9
  benchmarks/README.md
@@ -18,6 +15,7 @@ docs/cli.md
18
15
  docs/correlation.md
19
16
  docs/datasets.md
20
17
  docs/getting_started.md
18
+ docs/granger.md
21
19
  docs/group_inference.md
22
20
  docs/hmm.md
23
21
  docs/index.md
@@ -49,6 +47,7 @@ src/dfc_kit.egg-info/requires.txt
49
47
  src/dfc_kit.egg-info/top_level.txt
50
48
  src/dfckit/__init__.py
51
49
  src/dfckit/_arrays.py
50
+ src/dfckit/_feature_selection.py
52
51
  src/dfckit/_preprocessing.py
53
52
  src/dfckit/_validation.py
54
53
  src/dfckit/cli.py
@@ -73,6 +72,7 @@ src/dfckit/commands/states.py
73
72
  src/dfckit/connectivity/__init__.py
74
73
  src/dfckit/connectivity/_edge_products.py
75
74
  src/dfckit/connectivity/correlation.py
75
+ src/dfckit/connectivity/granger.py
76
76
  src/dfckit/connectivity/instantaneous.py
77
77
  src/dfckit/connectivity/leida.py
78
78
  src/dfckit/connectivity/lowrank.py
@@ -138,6 +138,9 @@ tests/unit/test_cross_validation.py
138
138
  tests/unit/test_data.py
139
139
  tests/unit/test_endpoint_inference.py
140
140
  tests/unit/test_ets.py
141
+ tests/unit/test_feature_selection.py
142
+ tests/unit/test_granger.py
143
+ tests/unit/test_granger_optional_dependency.py
141
144
  tests/unit/test_hmm.py
142
145
  tests/unit/test_independent_endpoint_inference.py
143
146
  tests/unit/test_inference.py
@@ -8,4 +8,4 @@ __all__ = [
8
8
  "TimeWindow",
9
9
  "validate_subject_disjoint",
10
10
  ]
11
- __version__ = "1.0.1"
11
+ __version__ = "1.0.2"
@@ -0,0 +1,43 @@
1
+ """Shared validation for selecting named feature columns."""
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 ArrayLike, NDArray
9
+
10
+ FeatureKey = tuple[str, ...]
11
+
12
+
13
+ def resolve_feature_selection(
14
+ available_keys: Sequence[FeatureKey],
15
+ *,
16
+ feature_keys: Iterable[Sequence[str]] | None,
17
+ feature_mask: ArrayLike | None,
18
+ ) -> tuple[NDArray[np.int64], tuple[FeatureKey, ...]]:
19
+ """Resolve one exact-key or Boolean-mask selection against known features."""
20
+ if (feature_keys is None) == (feature_mask is None):
21
+ raise ValueError("provide exactly one of feature_keys or feature_mask")
22
+
23
+ available = tuple(tuple(str(part) for part in key) for key in available_keys)
24
+ if feature_keys is not None:
25
+ requested = tuple(tuple(str(part) for part in key) for key in feature_keys)
26
+ if not requested or any(not key for key in requested):
27
+ raise ValueError("feature_keys must select at least one non-empty key")
28
+ if len(set(requested)) != len(requested):
29
+ raise ValueError("feature_keys must be unique")
30
+ positions = {key: index for index, key in enumerate(available)}
31
+ missing = tuple(key for key in requested if key not in positions)
32
+ if missing:
33
+ raise KeyError(f"selected feature keys are absent: {missing}")
34
+ indices = np.asarray([positions[key] for key in requested], dtype=np.int64)
35
+ return indices, requested
36
+
37
+ mask = np.asarray(feature_mask)
38
+ if mask.dtype.kind != "b" or mask.shape != (len(available),):
39
+ raise ValueError(f"feature_mask must be a Boolean vector with shape ({len(available)},)")
40
+ indices = np.flatnonzero(mask).astype(np.int64, copy=False)
41
+ if not len(indices):
42
+ raise ValueError("feature_mask must select at least one feature")
43
+ return indices, tuple(available[int(index)] for index in indices)
@@ -47,6 +47,10 @@ _KMEANS_METADATA = {
47
47
  "n_states",
48
48
  "reassignment_ratio",
49
49
  "sample_interval_seconds",
50
+ "sample_weight_effective_row_count",
51
+ "sample_weight_mode",
52
+ "sample_weight_sum",
53
+ "sample_weight_sum_squares",
50
54
  "seed",
51
55
  "source_contract",
52
56
  "standardize_features",
@@ -249,6 +253,10 @@ def _array_payload(model: FittedModel) -> tuple[str, dict[str, object], dict[str
249
253
  ),
250
254
  "iterations": int(model.iterations),
251
255
  "inertia": float(model.inertia),
256
+ "sample_weight_mode": str(model.sample_weight_mode),
257
+ "sample_weight_sum": model.sample_weight_sum,
258
+ "sample_weight_sum_squares": model.sample_weight_sum_squares,
259
+ "sample_weight_effective_row_count": model.sample_weight_effective_row_count,
252
260
  }
253
261
  arrays = {name: np.asarray(getattr(model, name)) for name in _KMEANS_ARRAYS}
254
262
  return "kmeans-state", metadata, arrays
@@ -425,6 +433,12 @@ def _load_kmeans(
425
433
  fit_subjects=_string_tuple(metadata, "fit_subjects"),
426
434
  fit_sample_count=fit_sample_count,
427
435
  implementation=_string(metadata, "implementation"),
436
+ sample_weight_mode=_string(metadata, "sample_weight_mode"),
437
+ sample_weight_sum=_finite_float(metadata, "sample_weight_sum"),
438
+ sample_weight_sum_squares=_finite_float(metadata, "sample_weight_sum_squares"),
439
+ sample_weight_effective_row_count=_finite_float(
440
+ metadata, "sample_weight_effective_row_count"
441
+ ),
428
442
  )
429
443
 
430
444
 
@@ -174,6 +174,11 @@ def build_parser() -> argparse.ArgumentParser:
174
174
  )
175
175
  fit.add_argument("store", type=Path, help="existing FeatureStore directory")
176
176
  fit.add_argument("output", type=Path, help="new fitted-model artifact directory")
177
+ fit.add_argument(
178
+ "--feature-keys",
179
+ type=Path,
180
+ help="JSON array of exact ordered feature keys to fit",
181
+ )
177
182
  fit.add_argument("--method", choices=("kmeans", "hmm"), required=True)
178
183
  fit.add_argument("--n-states", type=int, required=True)
179
184
  fit.add_argument("--seed", type=int, required=True)
@@ -228,6 +233,12 @@ def build_parser() -> argparse.ArgumentParser:
228
233
  fit.add_argument("--tol", type=float, default=1e-3)
229
234
  fit.add_argument("--pca-batch-size", type=int, default=4096)
230
235
  fit.add_argument("--minimum-sequence-length", type=int, default=2)
236
+ fit.add_argument(
237
+ "--sample-weight-mode",
238
+ choices=("uniform", "subject_session_balanced"),
239
+ default="uniform",
240
+ help="row weighting for materialized Lloyd KMeans",
241
+ )
231
242
 
232
243
  predict = subparsers.add_parser(
233
244
  "predict-states",
@@ -236,6 +247,11 @@ def build_parser() -> argparse.ArgumentParser:
236
247
  predict.add_argument("store", type=Path, help="FeatureStore to decode")
237
248
  predict.add_argument("model", type=Path, help="saved fitted-model artifact")
238
249
  predict.add_argument("output", type=Path, help="new state-prediction artifact directory")
250
+ predict.add_argument(
251
+ "--feature-keys",
252
+ type=Path,
253
+ help="JSON array of exact ordered feature keys expected by the model",
254
+ )
239
255
  predict.add_argument(
240
256
  "--subject",
241
257
  action="append",
@@ -256,10 +272,20 @@ def build_parser() -> argparse.ArgumentParser:
256
272
 
257
273
  summarize_features = subparsers.add_parser(
258
274
  "summarize-store",
259
- help="write acquisition-level statistics for every named FeatureStore feature",
275
+ help="write acquisition-level statistics for features or their equal-weight mean",
260
276
  )
261
277
  summarize_features.add_argument("store", type=Path, help="FeatureStore to summarize")
262
278
  summarize_features.add_argument("output", type=Path, help="new endpoint-summary JSON file")
279
+ summarize_features.add_argument(
280
+ "--feature-keys",
281
+ type=Path,
282
+ help="JSON array of exact feature keys to select, such as an NBS component's edges",
283
+ )
284
+ summarize_features.add_argument(
285
+ "--feature-mean",
286
+ metavar="NAME",
287
+ help="average selected features within each sample before summarizing under NAME",
288
+ )
263
289
  summarize_features.add_argument(
264
290
  "--statistic",
265
291
  action="append",
@@ -282,6 +308,11 @@ def build_parser() -> argparse.ArgumentParser:
282
308
  describe.add_argument("store", type=Path, help="FeatureStore used to fit the model")
283
309
  describe.add_argument("model", type=Path, help="saved KMeans or Gaussian HMM artifact")
284
310
  describe.add_argument("output", type=Path, help="new state-description JSON file")
311
+ describe.add_argument(
312
+ "--feature-keys",
313
+ type=Path,
314
+ help="JSON array of exact ordered feature keys used to fit the model",
315
+ )
285
316
  describe.add_argument("--top-features", type=int, default=10)
286
317
  describe.add_argument(
287
318
  "--network-map",
@@ -440,6 +471,11 @@ def build_parser() -> argparse.ArgumentParser:
440
471
  score.add_argument("store", type=Path, help="FeatureStore to score")
441
472
  score.add_argument("model", type=Path, help="saved fitted-model artifact")
442
473
  score.add_argument("output", type=Path, help="new held-out score JSON file")
474
+ score.add_argument(
475
+ "--feature-keys",
476
+ type=Path,
477
+ help="JSON array of exact ordered feature keys expected by the model",
478
+ )
443
479
  score.add_argument(
444
480
  "--subject",
445
481
  action="append",
@@ -29,7 +29,9 @@ from ..states.interpretation import (
29
29
  write_state_description,
30
30
  )
31
31
  from ..states.selection import _write_state_count_comparison
32
- from ..storage.summary import _summarize_store_file, _write_store_summary
32
+ from ..storage import FeatureStore, summarize_store_statistics
33
+ from ..storage.summary import _write_store_summary
34
+ from .states import _load_feature_keys, _selected_feature_store
33
35
 
34
36
 
35
37
  def describe_states(namespace: argparse.Namespace) -> dict[str, object]:
@@ -39,6 +41,7 @@ def describe_states(namespace: argparse.Namespace) -> dict[str, object]:
39
41
  namespace.model,
40
42
  top_features=namespace.top_features,
41
43
  network_map_path=namespace.network_map,
44
+ feature_keys=_load_feature_keys(namespace.feature_keys),
42
45
  )
43
46
  output = write_state_description(payload, namespace.output)
44
47
  return {
@@ -84,7 +87,8 @@ def infer_state_metrics(namespace: argparse.Namespace) -> dict[str, object]:
84
87
  def summarize_store(namespace: argparse.Namespace) -> dict[str, object]:
85
88
  """Write acquisition-level feature statistics from a FeatureStore."""
86
89
  statistics = tuple(namespace.statistic or ("mean",))
87
- payload = _summarize_store_file(namespace.store, statistics)
90
+ store = _selected_feature_store(FeatureStore.open(namespace.store), namespace.feature_keys)
91
+ payload = summarize_store_statistics(store, statistics, feature_mean=namespace.feature_mean)
88
92
  output = _write_store_summary(payload, namespace.output)
89
93
  return {
90
94
  "output": str(output),
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import json
6
7
 
7
8
  from ..artifacts import (
8
9
  StatePredictions,
@@ -31,9 +32,29 @@ from ..states.streaming_hmm import (
31
32
  from ..storage import FeatureStore
32
33
 
33
34
 
35
+ def _load_feature_keys(path):
36
+ if path is None:
37
+ return None
38
+ if not path.is_file() or path.is_symlink():
39
+ raise ValueError(f"feature-keys file must be an existing non-symlink file: {path}")
40
+ raw = json.loads(path.read_text(encoding="utf-8"))
41
+ if not isinstance(raw, list) or not raw:
42
+ raise ValueError("feature-keys JSON must be a non-empty array")
43
+ if any(not isinstance(key, list) or not key for key in raw):
44
+ raise ValueError("every feature key must be a non-empty JSON array")
45
+ return tuple(tuple(str(part) for part in key) for key in raw)
46
+
47
+
48
+ def _selected_feature_store(store: FeatureStore, path):
49
+ keys = _load_feature_keys(path)
50
+ return store if keys is None else store.select_features(feature_keys=keys)
51
+
52
+
34
53
  def fit_states(namespace: argparse.Namespace) -> dict[str, object]:
35
54
  """Fit one state model from a disk-backed feature store."""
36
- store = FeatureStore.open(namespace.store)
55
+ store = _selected_feature_store(
56
+ FeatureStore.open(namespace.store), namespace.feature_keys
57
+ )
37
58
  if namespace.output.exists() or namespace.output.is_symlink():
38
59
  raise FileExistsError(f"model artifact path already exists: {namespace.output}")
39
60
  subjects = None if namespace.subject is None else tuple(namespace.subject)
@@ -43,6 +64,14 @@ def fit_states(namespace: argparse.Namespace) -> dict[str, object]:
43
64
  raise ValueError(f"requested fit subjects are absent from the store: {missing}")
44
65
 
45
66
  if namespace.method == "kmeans":
67
+ if namespace.sample_weight_mode != "uniform" and (
68
+ namespace.fitting_mode != "materialized"
69
+ or namespace.algorithm != "lloyd"
70
+ or namespace.n_pca_components is not None
71
+ ):
72
+ raise ValueError(
73
+ "subject_session_balanced weights require materialized Lloyd KMeans without PCA"
74
+ )
46
75
  if namespace.fitting_mode == "streaming":
47
76
  if namespace.algorithm != "minibatch":
48
77
  raise ValueError("streaming KMeans requires --algorithm minibatch")
@@ -78,6 +107,7 @@ def fit_states(namespace: argparse.Namespace) -> dict[str, object]:
78
107
  reassignment_ratio=namespace.reassignment_ratio,
79
108
  n_pca_components=namespace.n_pca_components,
80
109
  subjects=subjects,
110
+ sample_weight_mode=namespace.sample_weight_mode,
81
111
  )
82
112
  model = fit.model
83
113
  fit_sequence_count = len(fit.assignments.sequences)
@@ -97,6 +127,7 @@ def fit_states(namespace: argparse.Namespace) -> dict[str, object]:
97
127
  "initialization_passes": list(fit.initialization_passes),
98
128
  "log_likelihood": None,
99
129
  "inertia": model.inertia,
130
+ "sample_weight_mode": model.sample_weight_mode,
100
131
  "init_sample_size": model.init_sample_size,
101
132
  "n_pca_components": model.n_pca_components,
102
133
  "pca_batch_size": model.pca_batch_size,
@@ -151,7 +182,9 @@ def selected_store_subjects(
151
182
 
152
183
  def predict_states(namespace: argparse.Namespace) -> dict[str, object]:
153
184
  """Decode a feature store with a fitted state model."""
154
- store = FeatureStore.open(namespace.store)
185
+ store = _selected_feature_store(
186
+ FeatureStore.open(namespace.store), namespace.feature_keys
187
+ )
155
188
  model = load_fitted_model(namespace.model)
156
189
  subjects = selected_store_subjects(store, namespace.subject)
157
190
  if isinstance(model, KMeansStateModel):
@@ -214,7 +247,9 @@ def score_states(namespace: argparse.Namespace) -> dict[str, object]:
214
247
  """Score a fitted state model on selected acquisitions."""
215
248
  if namespace.output.exists() or namespace.output.is_symlink():
216
249
  raise FileExistsError(f"state-model score output path already exists: {namespace.output}")
217
- store = FeatureStore.open(namespace.store)
250
+ store = _selected_feature_store(
251
+ FeatureStore.open(namespace.store), namespace.feature_keys
252
+ )
218
253
  model = load_fitted_model(namespace.model)
219
254
  subjects = selected_store_subjects(store, namespace.subject)
220
255
  if isinstance(model, KMeansStateModel):