physioex 2.0.0__tar.gz → 2.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 (309) hide show
  1. {physioex-2.0.0 → physioex-2.0.2}/.github/workflows/test.yml +4 -3
  2. physioex-2.0.2/CHANGELOG.md +73 -0
  3. {physioex-2.0.0 → physioex-2.0.2}/PKG-INFO +23 -3
  4. {physioex-2.0.0 → physioex-2.0.2}/README.md +19 -1
  5. {physioex-2.0.0 → physioex-2.0.2}/docs/api/index.md +2 -1
  6. physioex-2.0.2/docs/pages/explain/explain.md +158 -0
  7. physioex-2.0.2/examples/explain/lrp_seqsleepnet_n3.py +187 -0
  8. physioex-2.0.2/physioex/__init__.py +1 -0
  9. physioex-2.0.2/physioex/explain/__init__.py +34 -0
  10. physioex-2.0.2/physioex/explain/lrp/__init__.py +89 -0
  11. physioex-2.0.2/physioex/explain/lrp/_functional.py +167 -0
  12. physioex-2.0.2/physioex/explain/lrp/_rules.py +70 -0
  13. physioex-2.0.2/physioex/explain/lrp/attributor.py +78 -0
  14. physioex-2.0.2/physioex/explain/lrp/canonizers.py +37 -0
  15. physioex-2.0.2/physioex/explain/lrp/composites.py +106 -0
  16. physioex-2.0.2/physioex/explain/lrp/diagnostics.py +61 -0
  17. physioex-2.0.2/physioex/explain/lrp/model.py +412 -0
  18. physioex-2.0.2/physioex/explain/lrp/pooling.py +116 -0
  19. physioex-2.0.2/physioex/explain/lrp/recurrent.py +257 -0
  20. physioex-2.0.2/physioex/explain/lrp/transformer.py +284 -0
  21. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/protosleepnet.py +16 -6
  22. {physioex-2.0.0 → physioex-2.0.2}/pyproject.toml +3 -0
  23. physioex-2.0.2/tests/explain/lrp/test_functional.py +98 -0
  24. physioex-2.0.2/tests/explain/lrp/test_lrp.py +158 -0
  25. physioex-2.0.2/tests/explain/lrp/test_model.py +361 -0
  26. physioex-2.0.2/tests/explain/lrp/test_real_models.py +202 -0
  27. physioex-2.0.2/tests/explain/lrp/test_recurrent.py +170 -0
  28. physioex-2.0.2/tests/explain/lrp/test_transformer.py +194 -0
  29. physioex-2.0.2/tests/test_modality_dropout.py +62 -0
  30. physioex-2.0.0/docs/pages/explain/explain.md +0 -56
  31. physioex-2.0.0/physioex/__init__.py +0 -1
  32. {physioex-2.0.0 → physioex-2.0.2}/.claudeignore +0 -0
  33. {physioex-2.0.0 → physioex-2.0.2}/.github/copilot-instructions.md +0 -0
  34. {physioex-2.0.0 → physioex-2.0.2}/.github/workflows/black.yml +0 -0
  35. {physioex-2.0.0 → physioex-2.0.2}/.github/workflows/ci.yml +0 -0
  36. {physioex-2.0.0 → physioex-2.0.2}/.github/workflows/publish.yaml +0 -0
  37. {physioex-2.0.0 → physioex-2.0.2}/.gitignore +0 -0
  38. {physioex-2.0.0 → physioex-2.0.2}/LICENSE +0 -0
  39. {physioex-2.0.0 → physioex-2.0.2}/MANIFEST.in +0 -0
  40. {physioex-2.0.0 → physioex-2.0.2}/REPO_MAP.txt +0 -0
  41. {physioex-2.0.0 → physioex-2.0.2}/docs/_ext/related_works.py +0 -0
  42. {physioex-2.0.0 → physioex-2.0.2}/docs/_static/custom.css +0 -0
  43. {physioex-2.0.0 → physioex-2.0.2}/docs/assets/images/data/sequence_viz.png +0 -0
  44. {physioex-2.0.0 → physioex-2.0.2}/docs/assets/images/logo.psd +0 -0
  45. {physioex-2.0.0 → physioex-2.0.2}/docs/assets/images/logo.svg +0 -0
  46. {physioex-2.0.0 → physioex-2.0.2}/docs/assets/images/logo_bar.svg +0 -0
  47. {physioex-2.0.0 → physioex-2.0.2}/docs/conf.py +0 -0
  48. {physioex-2.0.0 → physioex-2.0.2}/docs/index.md +0 -0
  49. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/library/data.md +0 -0
  50. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/library/explain.md +0 -0
  51. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/library/models.md +0 -0
  52. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/library/overview.md +0 -0
  53. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/library/train.md +0 -0
  54. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/architecture/testing/overview.md +0 -0
  55. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/contribute.md +0 -0
  56. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/data.md +0 -0
  57. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/preprocess.md +0 -0
  58. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/train/cli.md +0 -0
  59. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/train/networks/chambon2018.md +0 -0
  60. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/train/networks.md +0 -0
  61. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/train/train.md +0 -0
  62. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/unavailable.md +0 -0
  63. {physioex-2.0.0 → physioex-2.0.2}/docs/pages/user_guide.md +0 -0
  64. {physioex-2.0.0 → physioex-2.0.2}/docs/related/_thumbs/_placeholder.svg +0 -0
  65. {physioex-2.0.0 → physioex-2.0.2}/docs/related/fetch_citations.py +0 -0
  66. {physioex-2.0.0 → physioex-2.0.2}/docs/related/index.md +0 -0
  67. {physioex-2.0.0 → physioex-2.0.2}/docs/related/works.yaml +0 -0
  68. {physioex-2.0.0 → physioex-2.0.2}/examples/__init__.py +0 -0
  69. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/__init__.py +0 -0
  70. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/compare_strategies.py +0 -0
  71. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/extract_all_concepts.py +0 -0
  72. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/report.py +0 -0
  73. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/utils.py +0 -0
  74. {physioex-2.0.0 → physioex-2.0.2}/examples/analyze_csd_concepts/visualize_concepts.py +0 -0
  75. {physioex-2.0.0 → physioex-2.0.2}/examples/explain/conceptualspectraldecomposition/README.md +0 -0
  76. {physioex-2.0.0 → physioex-2.0.2}/examples/explain/conceptualspectraldecomposition/explain_csd.py +0 -0
  77. {physioex-2.0.0 → physioex-2.0.2}/examples/explain/conceptualspectraldecomposition/train_probe.py +0 -0
  78. {physioex-2.0.0 → physioex-2.0.2}/examples/explain/conceptualspectraldecomposition/utils.py +0 -0
  79. {physioex-2.0.0 → physioex-2.0.2}/examples/explain/conceptualspectraldecomposition/visualize.py +0 -0
  80. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/bendr/extract_embeddings.py +0 -0
  81. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/biot/extract_embeddings.py +0 -0
  82. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/cbramod/extract_embeddings.py +0 -0
  83. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/labram/extract_embeddings.py +0 -0
  84. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/neurolm/extract_embeddings.py +0 -0
  85. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/reve/extract_embeddings.py +0 -0
  86. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/sjepa/extract_embeddings.py +0 -0
  87. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/sleepfm/extract_embeddings.py +0 -0
  88. {physioex-2.0.0 → physioex-2.0.2}/examples/foundation/tfc/extract_embeddings.py +0 -0
  89. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/extract_embeddings.py +0 -0
  90. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/extract_embeddings.sbatch +0 -0
  91. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/quickstart.py +0 -0
  92. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/smoke.sbatch +0 -0
  93. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/test_pretrained.py +0 -0
  94. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/train.py +0 -0
  95. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/chambon2018/train.sbatch +0 -0
  96. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/coresleep-kontras/extract_embeddings.py +0 -0
  97. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/coresleep-kontras/train.py +0 -0
  98. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/extract_embeddings.py +0 -0
  99. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/quickstart.py +0 -0
  100. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/smoke.sbatch +0 -0
  101. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/test_pretrained.py +0 -0
  102. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/test_pretrained.sbatch +0 -0
  103. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/train.py +0 -0
  104. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/lseqsleepnet-phan/train.sbatch +0 -0
  105. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/protosleepnet/README.md +0 -0
  106. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/protosleepnet/quickstart.py +0 -0
  107. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/extract_embeddings.py +0 -0
  108. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/extract_embeddings.sbatch +0 -0
  109. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/quickstart.py +0 -0
  110. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/test.py +0 -0
  111. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/test_pretrained.py +0 -0
  112. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/test_pretrained.sbatch +0 -0
  113. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/test_stages_sites.py +0 -0
  114. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/train.py +0 -0
  115. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/train.sbatch +0 -0
  116. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/train_shhs.py +0 -0
  117. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/seqsleepnet-phan/train_shhs.sbatch +0 -0
  118. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/extract_embeddings.py +0 -0
  119. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/extract_embeddings.sbatch +0 -0
  120. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/quickstart.py +0 -0
  121. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/smoke.sbatch +0 -0
  122. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/test_pretrained.py +0 -0
  123. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/test_pretrained.sbatch +0 -0
  124. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/train.py +0 -0
  125. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/sleeptransformer-phan/train.sbatch +0 -0
  126. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.py +0 -0
  127. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.sbatch +0 -0
  128. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/quickstart.py +0 -0
  129. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/smoke.sbatch +0 -0
  130. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/test_pretrained.py +0 -0
  131. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/test_pretrained.sbatch +0 -0
  132. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/train.py +0 -0
  133. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tinysleepnet-supratak/train.sbatch +0 -0
  134. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/extract_embeddings.py +0 -0
  135. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/quickstart.py +0 -0
  136. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/smoke.sbatch +0 -0
  137. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/test_pretrained.py +0 -0
  138. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/train.py +0 -0
  139. {physioex-2.0.0 → physioex-2.0.2}/examples/pretrained/tsinalis-2016/train.sbatch +0 -0
  140. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/notebook.ipynb +0 -0
  141. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_barplot.png +0 -0
  142. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_hypnogram.png +0 -0
  143. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_barplot.png +0 -0
  144. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_hypnogram.png +0 -0
  145. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell17_ig_tiny.png +0 -0
  146. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell19_ig_seq.png +0 -0
  147. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell19b_attr_vs_psd.png +0 -0
  148. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell5_raw_n3_psd.png +0 -0
  149. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/outputs/cell7_spec_n3_psd.png +0 -0
  150. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/run_notebook.py +0 -0
  151. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/script.pdf +0 -0
  152. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/slides.pdf +0 -0
  153. {physioex-2.0.0 → physioex-2.0.2}/lectures/2026-06-24_ugent_xai/util.py +0 -0
  154. {physioex-2.0.0 → physioex-2.0.2}/lectures/README.md +0 -0
  155. {physioex-2.0.0 → physioex-2.0.2}/lectures/assets/kuleuven_logo.png +0 -0
  156. {physioex-2.0.0 → physioex-2.0.2}/lectures/assets/physioex_logo.png +0 -0
  157. {physioex-2.0.0 → physioex-2.0.2}/lectures/assets/physioex_logo.svg +0 -0
  158. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/__init__.py +0 -0
  159. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/base.py +0 -0
  160. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/cache.py +0 -0
  161. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/collate.py +0 -0
  162. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datareader.py +0 -0
  163. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/dataset.py +0 -0
  164. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/__init__.py +0 -0
  165. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/_nsrr.py +0 -0
  166. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/_splits/shhs.json +0 -0
  167. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/alzheimers.py +0 -0
  168. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/channels_table.md +0 -0
  169. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/dcsm.py +0 -0
  170. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/hmc.py +0 -0
  171. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/hpap.py +0 -0
  172. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/mass.py +0 -0
  173. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/mesa.py +0 -0
  174. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/mros.py +0 -0
  175. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/parkinsons.py +0 -0
  176. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/shhs.py +0 -0
  177. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/sleepedf.py +0 -0
  178. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/stages.py +0 -0
  179. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/datasets/wsc.py +0 -0
  180. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/events.py +0 -0
  181. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/modality.py +0 -0
  182. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/multi.py +0 -0
  183. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/pipeline.py +0 -0
  184. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/presets.py +0 -0
  185. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/readers/__init__.py +0 -0
  186. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/readers/annotations.py +0 -0
  187. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/readers/edf.py +0 -0
  188. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/__init__.py +0 -0
  189. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/bandpass.py +0 -0
  190. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/highpass.py +0 -0
  191. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/identity.py +0 -0
  192. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/normalize.py +0 -0
  193. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/notch.py +0 -0
  194. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/resample.py +0 -0
  195. {physioex-2.0.0 → physioex-2.0.2}/physioex/data/steps/spectrogram.py +0 -0
  196. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/__init__.py +0 -0
  197. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/csd.py +0 -0
  198. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/multichannel_sg.py +0 -0
  199. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/report.py +0 -0
  200. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/sleep_bands.py +0 -0
  201. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/foundational/specificity.py +0 -0
  202. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/__init__.py +0 -0
  203. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/filters.py +0 -0
  204. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/functionizer.py +0 -0
  205. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/gradients.py +0 -0
  206. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/metrics/__init__.py +0 -0
  207. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/spectralgradients.py +0 -0
  208. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/vidft.py +0 -0
  209. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/posthoc/vistdft.py +0 -0
  210. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/__init__.py +0 -0
  211. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/local.py +0 -0
  212. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/posthoc/__init__.py +0 -0
  213. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/posthoc/nmf.py +0 -0
  214. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/posthoc/utils.py +0 -0
  215. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/posthoc/vq.py +0 -0
  216. {physioex-2.0.0 → physioex-2.0.2}/physioex/explain/prototypes/reconstruct.py +0 -0
  217. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/__init__.py +0 -0
  218. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/bendr.py +0 -0
  219. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/biot.py +0 -0
  220. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/cbramod.py +0 -0
  221. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/chambon2018.py +0 -0
  222. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/coresleep.py +0 -0
  223. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/embed.py +0 -0
  224. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/foundation_base.py +0 -0
  225. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/foundation_channel_mapping.py +0 -0
  226. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/foundation_checkpoints.py +0 -0
  227. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/foundation_datasets.py +0 -0
  228. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/foundation_preproc.py +0 -0
  229. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/labram.py +0 -0
  230. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/lseqsleepnet.py +0 -0
  231. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/neurolm.py +0 -0
  232. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/pretrained.py +0 -0
  233. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/reve.py +0 -0
  234. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/seqsleepnet.py +0 -0
  235. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/sjepa.py +0 -0
  236. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/sleep_tokenizer.py +0 -0
  237. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/sleepfm.py +0 -0
  238. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/sleeptransformer.py +0 -0
  239. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/tfc.py +0 -0
  240. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/tinysleepnet.py +0 -0
  241. {physioex-2.0.0 → physioex-2.0.2}/physioex/models/tsinalis.py +0 -0
  242. {physioex-2.0.0/physioex/explain → physioex-2.0.2/physioex/train}/__init__.py +0 -0
  243. {physioex-2.0.0/physioex/train → physioex-2.0.2/physioex/train/bin}/__init__.py +0 -0
  244. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/bin/_common.py +0 -0
  245. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/bin/finetune.py +0 -0
  246. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/bin/test.py +0 -0
  247. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/bin/train.py +0 -0
  248. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/logger.py +0 -0
  249. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/metrics.py +0 -0
  250. {physioex-2.0.0/physioex/train/bin → physioex-2.0.2/physioex/train/models}/__init__.py +0 -0
  251. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/models/check_table.csv +0 -0
  252. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/models/load.py +0 -0
  253. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/multidevicetrainer.py +0 -0
  254. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/progress.py +0 -0
  255. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/stats.py +0 -0
  256. {physioex-2.0.0 → physioex-2.0.2}/physioex/train/trainer.py +0 -0
  257. {physioex-2.0.0/physioex/train/models → physioex-2.0.2/tests}/__init__.py +0 -0
  258. {physioex-2.0.0 → physioex-2.0.2}/tests/conftest.py +0 -0
  259. {physioex-2.0.0 → physioex-2.0.2}/tests/data/test_base_smoke.py +0 -0
  260. {physioex-2.0.0 → physioex-2.0.2}/tests/data/test_modality.py +0 -0
  261. {physioex-2.0.0 → physioex-2.0.2}/tests/data/test_multi.py +0 -0
  262. {physioex-2.0.0 → physioex-2.0.2}/tests/data/test_readers_annotations.py +0 -0
  263. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/foundational/test_sleep_bands.py +0 -0
  264. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/foundational/test_specificity.py +0 -0
  265. {physioex-2.0.0/tests → physioex-2.0.2/tests/explain/lrp}/__init__.py +0 -0
  266. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/__init__.py +0 -0
  267. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/test_gradients.py +0 -0
  268. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/test_metrics.py +0 -0
  269. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/test_spectralgradients.py +0 -0
  270. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/test_vidft.py +0 -0
  271. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/posthoc/test_vistdft.py +0 -0
  272. {physioex-2.0.0 → physioex-2.0.2}/tests/explain/prototypes/posthoc/test_nmf_vq.py +0 -0
  273. {physioex-2.0.0 → physioex-2.0.2}/tests/factories/__init__.py +0 -0
  274. {physioex-2.0.0 → physioex-2.0.2}/tests/factories/edf.py +0 -0
  275. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_architectures.py +0 -0
  276. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_foundation_base.py +0 -0
  277. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_foundation_encoders.py +0 -0
  278. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_foundation_preproc.py +0 -0
  279. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_pretrained_embed_helpers.py +0 -0
  280. {physioex-2.0.0 → physioex-2.0.2}/tests/models/test_sleep_tokenizer.py +0 -0
  281. {physioex-2.0.0 → physioex-2.0.2}/tests/test_alzheimers_parkinsons.py +0 -0
  282. {physioex-2.0.0 → physioex-2.0.2}/tests/test_api_surface.py +0 -0
  283. {physioex-2.0.0 → physioex-2.0.2}/tests/test_cache.py +0 -0
  284. {physioex-2.0.0 → physioex-2.0.2}/tests/test_cache_enabled.py +0 -0
  285. {physioex-2.0.0 → physioex-2.0.2}/tests/test_channel_resolution.py +0 -0
  286. {physioex-2.0.0 → physioex-2.0.2}/tests/test_cli_workflows.py +0 -0
  287. {physioex-2.0.0 → physioex-2.0.2}/tests/test_dataset_config.py +0 -0
  288. {physioex-2.0.0 → physioex-2.0.2}/tests/test_e2e_smoke.py +0 -0
  289. {physioex-2.0.0 → physioex-2.0.2}/tests/test_events.py +0 -0
  290. {physioex-2.0.0 → physioex-2.0.2}/tests/test_explain_fixes.py +0 -0
  291. {physioex-2.0.0 → physioex-2.0.2}/tests/test_label_hardening.py +0 -0
  292. {physioex-2.0.0 → physioex-2.0.2}/tests/test_load_model.py +0 -0
  293. {physioex-2.0.0 → physioex-2.0.2}/tests/test_mass_dataset.py +0 -0
  294. {physioex-2.0.0 → physioex-2.0.2}/tests/test_metrics_and_packaging.py +0 -0
  295. {physioex-2.0.0 → physioex-2.0.2}/tests/test_phase_b_datasets.py +0 -0
  296. {physioex-2.0.0 → physioex-2.0.2}/tests/test_phase_c_datasets.py +0 -0
  297. {physioex-2.0.0 → physioex-2.0.2}/tests/test_pipeline.py +0 -0
  298. {physioex-2.0.0 → physioex-2.0.2}/tests/test_presets.py +0 -0
  299. {physioex-2.0.0 → physioex-2.0.2}/tests/test_raw_dataset_integration.py +0 -0
  300. {physioex-2.0.0 → physioex-2.0.2}/tests/test_reading_modes.py +0 -0
  301. {physioex-2.0.0 → physioex-2.0.2}/tests/test_stages_dataset.py +0 -0
  302. {physioex-2.0.0 → physioex-2.0.2}/tests/test_trainer_enhancements.py +0 -0
  303. {physioex-2.0.0 → physioex-2.0.2}/tests/test_trainer_fixes.py +0 -0
  304. {physioex-2.0.0 → physioex-2.0.2}/tests/test_trainer_integration.py +0 -0
  305. {physioex-2.0.0 → physioex-2.0.2}/tests/test_wake_trimming.py +0 -0
  306. {physioex-2.0.0 → physioex-2.0.2}/tests/test_wsc_dataset.py +0 -0
  307. {physioex-2.0.0 → physioex-2.0.2}/tests/train/test_logger.py +0 -0
  308. {physioex-2.0.0 → physioex-2.0.2}/tests/train/test_progress.py +0 -0
  309. {physioex-2.0.0 → physioex-2.0.2}/tests/train/test_stats.py +0 -0
@@ -27,12 +27,13 @@ jobs:
27
27
  # torchaudio must come from the same CPU index as torch, otherwise the
28
28
  # transitively-installed (braindecode) torchaudio has a mismatched ABI
29
29
  # and fails to load its compiled _torchaudio.so.
30
- pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu
30
+ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
31
31
 
32
- - name: Install package (editable) with dev + legacy extras
32
+ - name: Install package (editable) with dev + legacy + explain extras
33
33
  # Editable so coverage's source=["physioex"] measures the executed tree
34
34
  # (a non-editable install runs the site-packages copy -> 0% reported).
35
- run: pip install -e .[dev,legacy]
35
+ # 'explain' pulls in zennit so the LRP tests run instead of skipping.
36
+ run: pip install -e .[dev,legacy,explain]
36
37
 
37
38
  - name: Run test suite with coverage gate
38
39
  env:
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ All notable changes to PhysioEx are documented here. Versions follow the
4
+ `physioex.__version__` string (read dynamically by the build) and the `v*` git tags
5
+ that publish to PyPI.
6
+
7
+ ## 2.0.2 — 2026-10-10
8
+
9
+ ### Fixed — accuracy-weighted modality dropout (`physioex.models.protosleepnet`)
10
+
11
+ `ZeroEmbeddingDropout` now samples the channels to occlude with probability
12
+ proportional to their accuracy, `p_c = a_c / sum_j a_j`, so the channels the
13
+ model relies on most are dropped most often. This is the rule the released
14
+ ProtoSleepNet and ProtoSleepTransformer models were trained with; a later
15
+ refactoring had inverted the sampling weights to `1 - a_c`. When no accuracy
16
+ estimate is available yet the sampling is uniform. Unit tests in
17
+ `tests/test_modality_dropout.py`.
18
+
19
+ ## 2.0.1 — 2026-09-13
20
+
21
+ ### Added — Layer-wise Relevance Propagation (`physioex.explain.lrp`)
22
+
23
+ A conservation-based attribution family (Bach et al. 2015) covering the whole
24
+ model zoo. Install with the new optional extra `pip install "physioex[explain]"`
25
+ (Zennit only; no LXT dependency).
26
+
27
+ - **Two entry points**, both seeding the target neuron with its logit so that
28
+ `Σ R ≈ f_c(x)`, both returning a per-sample `ConservationReport` with
29
+ `return_report=True`:
30
+ - `LRP` — Zennit composites for feed-forward / CNN models (Tsinalis,
31
+ Chambon2018): ε on dense, γ on convolutions, **w²** on the first layer
32
+ (unbounded EEG, not the pixel z-box), BatchNorm canonized; αβ / flat / z-box
33
+ configurable via `physioex_composite`.
34
+ - `ModelLRP` — whole-model LRP for recurrent, attention and transformer
35
+ architectures (TinySleepNet, SeqSleepNet, L-SeqSleepNet, SleepTransformer,
36
+ CoReSleep, ProtoSleepNet): **Arras signal-take** rule for LSTM/GRU at cell
37
+ level (`LRPLSTM`/`LRPGRU`, drop-in subclasses with a bit-identical forward,
38
+ `gate_rule="signal_take" | "uniform"`), **CP-LRP** for attention (value
39
+ path, exact conservation; Ali et al. 2022), identity rule for
40
+ LayerNorm/activations, proportional rule for residual sums, ε for linear
41
+ leaves and PhysioEx's learnable filterbank, CP-LRP adapters for the softmax
42
+ poolings (`AttentionPooling`, `AttentionLayer`, `ChannelMixer`).
43
+ - `prepare_model_for_lrp` rewrites a deep copy of the model; plain `+` residuals
44
+ written in a model's own `forward` are redirected to the proportional rule at
45
+ runtime (`patch_residuals`); `audit_lrp_coverage` / `strict=True` report any
46
+ parametric leaf left on plain autograd; `register_lrp_adapter` adds custom
47
+ blocks. Signed, dtype-safe stabiliser `z + ε·sign z` everywhere (fp16-safe).
48
+ - Docs: entry-point/rule matrix, how to read conservation and choose ε (ε must be
49
+ scaled to the activations: measured on the pretrained SeqSleepNet, 1e-6 inflates
50
+ relevance 2–3×, 1e-2 conserves), unsupported cases, and a literature note on
51
+ LRP vs path-based attribution (Deep Taylor, Integrated Gradients,
52
+ DeepLIFT/DeepSHAP). `physioex.explain.lrp` is in the API reference.
53
+ - Example: `examples/explain/lrp_seqsleepnet_n3.py` explains the N3 logit of the
54
+ pretrained `seqsleepnet-phan` on MASS training sequences with Saliency,
55
+ Input×Gradient, Integrated Gradients and LRP under different rule assignments.
56
+ - Tests: 91 new tests (`tests/explain/lrp/`), including forward-equivalence to
57
+ the fused modules, exact conservation on bias-free blocks and models, and all
58
+ six real architectures end-to-end with full rule coverage.
59
+
60
+ ### Known limitations
61
+
62
+ CP-LRP assigns zero relevance to the attention query/key path by design (full
63
+ AttnLRP is not implemented); bias relevance is absorbed (LRP-ε convention);
64
+ attention masks, `batch_first=False` transformers, explicit RNN initial states
65
+ and `proj_size` raise; ProtoSleepNet's VQ path runs under `no_grad`. Foundation
66
+ encoders and frequency-domain (DFT/STFT) LRP are planned follow-ups.
67
+
68
+ ## 2.0.0 — 2026-07-15
69
+
70
+ Ground-up release: raw-EDF lazy data layer with 12 dataset loaders, 9 pretrained
71
+ foundation encoders behind a uniform `encode()` interface, hand-written `Trainer`,
72
+ pytest suite and Sphinx documentation. Full notes in the
73
+ [v2.0.0 GitHub Release](https://github.com/guidogagl/physioex/releases/tag/v2.0.0).
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: physioex
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: A python package for explainable sleep staging via deep learning
5
5
  Project-URL: Homepage, https://github.com/guidogagl/physioex
6
6
  Project-URL: Documentation, https://guidogagl.github.io/physioex
@@ -42,6 +42,8 @@ Requires-Dist: sphinx-copybutton==0.5.2; extra == 'docs'
42
42
  Requires-Dist: sphinx-design==0.6.1; extra == 'docs'
43
43
  Requires-Dist: sphinx==8.1.3; extra == 'docs'
44
44
  Requires-Dist: sphinxcontrib-mermaid==1.0.0; extra == 'docs'
45
+ Provides-Extra: explain
46
+ Requires-Dist: zennit>=0.5.1; extra == 'explain'
45
47
  Provides-Extra: foundation
46
48
  Requires-Dist: transformers>=4.40.0; extra == 'foundation'
47
49
  Provides-Extra: gpu-monitor
@@ -62,7 +64,7 @@ Description-Content-Type: text/markdown
62
64
  # PhysioEx
63
65
 
64
66
  ![Python Version](https://img.shields.io/badge/python-3.11%2B-blue)
65
- ![PyPI Version](https://badge.fury.io/py/physioex.svg)
67
+ [![PyPI Version](https://img.shields.io/pypi/v/physioex?cacheSeconds=3600)](https://pypi.org/project/physioex/)
66
68
 
67
69
  **PhysioEx ( Physiological Signal Explainer )** is a versatile python library tailored for building, training, and explaining deep learning models for physiological signal analysis.
68
70
 
@@ -74,6 +76,12 @@ Beyond training classic architectures from scratch, PhysioEx wraps a family of
74
76
  **pretrained foundation encoders** behind a uniform interface, so you can extract
75
77
  embeddings, run linear probes, and apply the explainability tools on top of them.
76
78
 
79
+ The explainability toolkit (`physioex.explain`) offers gradient-based attribution
80
+ (Saliency, Input×Gradient, Integrated / Expected Gradients and their DFT/STFT
81
+ variants), concept and prototype methods, and — since 2.0.1 — **Layer-wise
82
+ Relevance Propagation** for CNN, recurrent and attention/transformer models
83
+ (`pip install "physioex[explain]"`), with built-in conservation diagnostics.
84
+
77
85
  ## Supported deep learning architectures
78
86
 
79
87
  PhysioEx ships two families of models under `physioex.models`.
@@ -164,8 +172,20 @@ Note: the github version of the library is kept updated weekly, the PiPy version
164
172
  $ pip install "physioex[foundation]" # transformers, for the REVE encoder
165
173
  $ pip install "physioex[datasets]" # mne, for the HOMEPAP EDF fallback reader
166
174
  $ pip install "physioex[tracking]" # tensorboard / wandb logging
175
+ $ pip install "physioex[explain]" # zennit, for Layer-wise Relevance Propagation
167
176
  ```
168
177
 
178
+ > **Note on `torchvision` (CPU-only installs).** The `explain` extra depends on
179
+ > `zennit`, which requires `torchvision`. If your `torch` comes from the PyTorch
180
+ > CPU index, install `torchvision` from the **same** index *before* the extra,
181
+ > otherwise pip pulls a CUDA build from PyPI that fails at import with
182
+ > `operator torchvision::nms does not exist`:
183
+ >
184
+ > ```bash
185
+ > $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
186
+ > $ pip install "physioex[explain]"
187
+ > ```
188
+
169
189
  ## Quickstart
170
190
 
171
191
  Set `PHYSIOEX_DATA` to the directory holding your datasets, then use the raw-EDF
@@ -5,7 +5,7 @@
5
5
  # PhysioEx
6
6
 
7
7
  ![Python Version](https://img.shields.io/badge/python-3.11%2B-blue)
8
- ![PyPI Version](https://badge.fury.io/py/physioex.svg)
8
+ [![PyPI Version](https://img.shields.io/pypi/v/physioex?cacheSeconds=3600)](https://pypi.org/project/physioex/)
9
9
 
10
10
  **PhysioEx ( Physiological Signal Explainer )** is a versatile python library tailored for building, training, and explaining deep learning models for physiological signal analysis.
11
11
 
@@ -17,6 +17,12 @@ Beyond training classic architectures from scratch, PhysioEx wraps a family of
17
17
  **pretrained foundation encoders** behind a uniform interface, so you can extract
18
18
  embeddings, run linear probes, and apply the explainability tools on top of them.
19
19
 
20
+ The explainability toolkit (`physioex.explain`) offers gradient-based attribution
21
+ (Saliency, Input×Gradient, Integrated / Expected Gradients and their DFT/STFT
22
+ variants), concept and prototype methods, and — since 2.0.1 — **Layer-wise
23
+ Relevance Propagation** for CNN, recurrent and attention/transformer models
24
+ (`pip install "physioex[explain]"`), with built-in conservation diagnostics.
25
+
20
26
  ## Supported deep learning architectures
21
27
 
22
28
  PhysioEx ships two families of models under `physioex.models`.
@@ -107,8 +113,20 @@ Note: the github version of the library is kept updated weekly, the PiPy version
107
113
  $ pip install "physioex[foundation]" # transformers, for the REVE encoder
108
114
  $ pip install "physioex[datasets]" # mne, for the HOMEPAP EDF fallback reader
109
115
  $ pip install "physioex[tracking]" # tensorboard / wandb logging
116
+ $ pip install "physioex[explain]" # zennit, for Layer-wise Relevance Propagation
110
117
  ```
111
118
 
119
+ > **Note on `torchvision` (CPU-only installs).** The `explain` extra depends on
120
+ > `zennit`, which requires `torchvision`. If your `torch` comes from the PyTorch
121
+ > CPU index, install `torchvision` from the **same** index *before* the extra,
122
+ > otherwise pip pulls a CUDA build from PyPI that fails at import with
123
+ > `operator torchvision::nms does not exist`:
124
+ >
125
+ > ```bash
126
+ > $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
127
+ > $ pip install "physioex[explain]"
128
+ > ```
129
+
112
130
  ## Quickstart
113
131
 
114
132
  Set `PHYSIOEX_DATA` to the directory holding your datasets, then use the raw-EDF
@@ -128,7 +128,7 @@ Metrics, statistics and progress helpers are documented at module level:
128
128
  ## `physioex.explain`
129
129
 
130
130
  Post-hoc attribution, the foundational Conceptual Spectral Decomposition (CSD),
131
- and prototype discovery.
131
+ prototype discovery, and Layer-wise Relevance Propagation (`explain` extra).
132
132
 
133
133
  ```{eval-rst}
134
134
  .. autosummary::
@@ -136,4 +136,5 @@ and prototype discovery.
136
136
 
137
137
  physioex.explain.foundational
138
138
  physioex.explain.prototypes.posthoc
139
+ physioex.explain.lrp
139
140
  ```
@@ -0,0 +1,158 @@
1
+ # Explain Module
2
+
3
+ PhysioEx ships an explainability (XAI) toolkit for sleep-staging and
4
+ foundation models, organized under `physioex.explain`: three core families
5
+ (post-hoc gradients, foundational/concept, prototypes) plus an optional
6
+ **Layer-wise Relevance Propagation** family (`explain` extra). For
7
+ the full class diagram see the
8
+ [explain-layer architecture](../architecture/library/explain.md) page.
9
+
10
+ ## Post-hoc attribution — `physioex.explain.posthoc`
11
+
12
+ Gradient-based attribution over a trained classifier, plus frequency- and
13
+ time-frequency-domain variants:
14
+
15
+ - **Time-domain**: `Saliency`, `InputXGradient`, `IntegratedGradients`,
16
+ `ExpectedGradients` (`physioex/explain/posthoc/gradients.py`). Each is a
17
+ `torch.nn.Module` built around a scalar-valued target function `f` (wrap a
18
+ classifier into one with `Funct` / `SeqFunct` from
19
+ `physioex/explain/posthoc/functionizer.py`); attribution is produced by
20
+ calling the attributor on the input.
21
+ - **Spectral**: `SpectralGradients` (`spectralgradients.py`) — path-integral
22
+ attribution across frequency bands.
23
+ - **Frequency-resolved**: DFT- and STFT-domain attributors, exported from
24
+ `vidft.py` and `vistdft.py`, wrap the same gradient methods around
25
+ differentiable (I)DFT/(I)STFT layers.
26
+ - **Faithfulness metrics** under `posthoc/metrics/` quantify attribution quality
27
+ (complexity, localization, infidelity, and time-frequency measures).
28
+
29
+ Refer to the [API Reference](../../api/index.md) for the exact constructor and
30
+ call signatures of each attributor before use.
31
+
32
+ ## Foundational explainability — `physioex.explain.foundational`
33
+
34
+ Concept-level analysis of foundation-model embeddings:
35
+
36
+ - **Conceptual Spectral Decomposition** — `ConceptualSpectralDecomposition`
37
+ (`csd.py`) decomposes embeddings over interpretable spectral concepts and
38
+ returns a `CSDResult` (a list of `ConceptAttribution`).
39
+ - **Specificity strategies** — a pluggable `SpecificityStrategy`
40
+ (`CohenDSpecificity`, `MarginSpecificity`, `SoftmaxSpecificity`,
41
+ `TopKSpecificity`, `NoFilter`) selects the concepts most specific to a class.
42
+ - **Sleep bands** (`sleep_bands.py`), **multi-channel spectral gradients**
43
+ (`multichannel_sg.py`), and reporting helpers (`report.py`).
44
+
45
+ ## Prototypes — `physioex.explain.prototypes`
46
+
47
+ Prototype/concept discovery and reconstruction:
48
+
49
+ - **Local relevance**: `PrototypeRelevance`, `get_prototypes` (`local.py`).
50
+ - **NMF** prototype discovery (`posthoc/nmf.py`:
51
+ `discover_prototypes_nmf`).
52
+ - **Vector-quantized** codebooks: `VQBottleneck` and the codebook helpers in
53
+ `posthoc/vq.py` (`learn_codebook_kmeans`, `quantize_embeddings`,
54
+ `train_codebook`).
55
+ - **Reconstruction** of learned concepts (`reconstruct.py`).
56
+
57
+ ## Layer-wise Relevance Propagation — `physioex.explain.lrp`
58
+
59
+ Conservation-based attribution (Bach et al. 2015): relevance is redistributed
60
+ backward through the network, conserving the total (`Σ R ≈ f(x)`). This family
61
+ is **optional** — install the extra:
62
+
63
+ ```bash
64
+ pip install "physioex[explain]"
65
+ ```
66
+
67
+ The extra pulls in `zennit`, and therefore `torchvision`. On a CPU-only `torch`
68
+ from the PyTorch index, install `torch torchvision` from that same index *first*
69
+ (`pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu`),
70
+ or pip will fetch a mismatched CUDA `torchvision` from PyPI that fails at import
71
+ (`operator torchvision::nms does not exist`).
72
+
73
+ Two entry points, both seeding the target neuron with its **logit value** so
74
+ that `Σ R ≈ f_c(x)` and returning relevance shaped like the input:
75
+
76
+ | Model family | Entry point | Rules |
77
+ |---|---|---|
78
+ | Feed-forward / CNN (Tsinalis, Chambon2018) | `LRP` (Zennit) | ε dense · γ conv · **w²** first layer (unbounded EEG, *not* the pixel z-box) · BatchNorm canonized · αβ/flat/z-box configurable via `physioex_composite` |
79
+ | Recurrent (TinySleepNet, SeqSleepNet, L-SeqSleepNet, ProtoSleepNet-seq) | `ModelLRP` | **Arras signal-take** LSTM/GRU (`LRPLSTM`/`LRPGRU`, cell-level, forward identical to the fused module) · ε on the learnable filterbank / linears · CP-LRP on the attention poolings |
80
+ | Attention / transformer (SleepTransformer, CoReSleep, ProtoSleepNet-tf) | `ModelLRP` | **CP-LRP** attention (value path; Ali et al. 2022) · identity rule on LayerNorm/GELU · proportional rule on residuals · ε on projections/FFN |
81
+
82
+ ```python
83
+ from physioex.explain.lrp import LRP, ModelLRP
84
+
85
+ relevance = LRP(cnn_model, out_index=2)(x) # Zennit path
86
+ relevance, report = ModelLRP(seq_model, in_index=10, out_index=2)(x, return_report=True)
87
+ print(report) # Σ R / f per sample — 1.0 = exact conservation
88
+ relevance = ModelLRP(coresleep, output_key="combined")(x) # dict outputs
89
+ ```
90
+
91
+ **How `ModelLRP` works.** The trained model is deep-copied and rewritten
92
+ (`prepare_model_for_lrp`): fused `nn.LSTM`/`nn.GRU` become `LRPLSTM`/`LRPGRU`
93
+ (subclasses, so `isinstance` checks and the `(output, states)` interface keep
94
+ working), `nn.TransformerEncoder`/`MultiheadAttention` become their CP-LRP
95
+ versions, PhysioEx's softmax poolings (`AttentionPooling`, `AttentionLayer`,
96
+ `ChannelMixer`) and the `LearnableFilterbank` get dedicated adapters, remaining
97
+ `Linear`/`Conv` leaves get ε-LRP, BatchNorm is folded into the preceding layer,
98
+ and norms/activations use the identity rule. Plain `+` residuals written in a
99
+ model's own `forward` are redirected at runtime to the proportional rule
100
+ (`patch_residuals=True`) — a plain add would give both branches the full
101
+ relevance and over-count. Register your own blocks with
102
+ `register_lrp_adapter(cls, factory)`; `audit_lrp_coverage(model)` (also
103
+ `ModelLRP(...).uncovered`, and `strict=True`) lists parametric leaves left on
104
+ plain autograd, which would propagate *gradient* instead of relevance.
105
+
106
+ **Reading the numbers.** Conservation is exact (up to ε) for bias-free
107
+ networks; **biases absorb relevance** (LRP-ε convention), so on real models
108
+ `Σ R / f` is below 1 — `ConservationReport.absorbed` shows how much. CP-LRP
109
+ treats the attention matrix as constant: features acting only through *where
110
+ to attend* (query/key path) receive zero relevance by design; full AttnLRP
111
+ (Achtibat et al. 2024) is non-conserving and not implemented. All stabilisers
112
+ are signed (`z + ε·sign z`), as in Arras et al. Not supported (raise): attention
113
+ masks, `batch_first=False` transformers, explicit RNN initial states, `proj_size`;
114
+ ProtoSleepNet's VQ path (`quantize=True`) runs under `no_grad`, so no relevance can
115
+ flow through it — `ModelLRP` explains the default `quantize=False` forward.
116
+
117
+ **Choosing ε.** The stabiliser must be scaled to the activations: on the
118
+ pretrained SeqSleepNet (dB-scale spectrograms, 4-layer BiLSTM/BiGRU) ε = 1e-6
119
+ inflates total relevance 2–3× through near-zero state sums, ε = 1e-2 conserves
120
+ (`ΣR/f ≈ 0.96`) and ε = 1e-1 over-absorbs. Sweep ε and read the
121
+ `ConservationReport`; `examples/explain/lrp_seqsleepnet_n3.py` does this on a
122
+ real model and compares the LRP rule assignments with Saliency, Input×Gradient
123
+ and Integrated Gradients.
124
+
125
+ **Related methods — LRP vs path-based attribution.** LRP is *not* path-based:
126
+ by Deep Taylor Decomposition (Montavon et al. 2017) every rule is a first-order
127
+ Taylor expansion of each neuron at a closed-form *root point* (ε/LRP-0 → root at
128
+ 0, hence LRP-0 = Gradient×Input; z⁺ → along the positive contributions; z^B →
129
+ on the input box), applied layer by layer in one backward pass — the baseline
130
+ is implicit in the rule. Integrated Gradients (Sundararajan et al. 2017)
131
+ instead integrates the gradient along the straight path from an explicit
132
+ baseline x′ to x; both satisfy completeness (Σ R = f(x) − f(x′)) by different
133
+ routes. The propagation-family counterpart of a *baseline* method is
134
+ **DeepLIFT** (Shrikumar et al. 2017): finite-difference multipliers
135
+ Δy/Δx w.r.t. a reference, i.e. Integrated Gradients applied layer-wise in a
136
+ single backward pass; Ancona et al. (2018) prove ε-LRP ≡ DeepLIFT-Rescale with a
137
+ zero baseline (ReLU nets without bias, ε → 0) and cast both as
138
+ modified-gradient × input, and **DeepSHAP** (Lundberg & Lee 2017) adds an
139
+ expectation over baselines — the analogue of `ExpectedGradients`. Averaging LRP
140
+ relevance along the x′→x path ("integrated LRP", e.g. Zennit's
141
+ `IntegratedGradients` attributor combined with a composite) is a supported
142
+ construct: it reduces exactly to IG for LRP-0 but carries no completeness
143
+ guarantee for ε/γ/CP rules and should be read as an ensemble. On LSTMs, Arras
144
+ et al. (2019) found gate-rule LRP the best-performing method, with DeepLIFT
145
+ close and IG/gradients behind.
146
+
147
+ - **Composites** (`lrp/composites.py`): `physioex_composite` (ε/γ/w²),
148
+ `epsilon_composite` (pure-ε reference). **Canonizers** (`lrp/canonizers.py`):
149
+ `default_canonizers` (BatchNorm merge).
150
+ - **Blocks**: `lrp/recurrent.py`, `lrp/transformer.py`, `lrp/pooling.py`;
151
+ shared primitives in `lrp/_functional.py`; diagnostics in `lrp/diagnostics.py`.
152
+ - **Backends**: [Zennit](https://github.com/chr5tphr/zennit) for the composites
153
+ and BatchNorm canonization; the recurrent, attention and pooling rules are
154
+ implemented in PhysioEx (`_functional.py`, `_rules.py`) following Arras et al.,
155
+ Ali et al. (CP-LRP) and the LXT conventions — no LXT dependency.
156
+
157
+ See the [API Reference](../../api/index.md) for verified signatures across all
158
+ families.
@@ -0,0 +1,187 @@
1
+ """LRP vs post-hoc explanations of the pretrained single-channel SeqSleepNet.
2
+
3
+ Explains the N3 logit of the pretrained ``seqsleepnet-phan`` (MASS, EEG 1 ch) on
4
+ training-set sequences whose central epoch is N3, side by side with Saliency,
5
+ Input×Gradient and Integrated Gradients, and shows how the LRP **rule
6
+ assignment** changes the attribution (gate rule, filterbank input rule,
7
+ attention handling, stabiliser ε), always reporting the conservation ratio
8
+ ``ΣR / f``. CPU is enough (a few minutes; the physioex cache makes re-runs
9
+ instant).
10
+
11
+ Requirements: ``pip install "physioex[explain]"``, the MASS EDFs under
12
+ ``$PHYSIOEX_DATA/MASS/Original/SS0{1..5}`` and a writable ``PHYSIOEX_CACHE_DIR``.
13
+ Set ``SEQSLEEPNET_DIR`` to a folder holding ``config.json`` + ``model.pt`` to run
14
+ offline (otherwise the weights are pulled from the HF hub).
15
+
16
+ Usage::
17
+
18
+ python examples/explain/lrp_seqsleepnet_n3.py --out ./n3_out [--n 8] [--eps 1e-2]
19
+ """
20
+
21
+ import argparse
22
+ import importlib
23
+ import json
24
+ import os
25
+ import time
26
+ import warnings
27
+
28
+ import numpy as np
29
+ import torch
30
+ import torch.nn as nn
31
+
32
+ from physioex.explain.lrp import LRPGRU, LRPLSTM, ModelLRP
33
+ from physioex.explain.lrp import model as lrpmodel
34
+ from physioex.explain.lrp.pooling import _PoolAdapter
35
+ from physioex.explain.posthoc import InputXGradient, IntegratedGradients, Saliency
36
+ from physioex.explain.posthoc.functionizer import SeqFunct
37
+
38
+ CLASSES = ["W", "N1", "N2", "N3", "REM"]
39
+ FBQ = "physioex.models.seqsleepnet.LearnableFilterbank"
40
+ ATQ = "physioex.models.seqsleepnet.AttentionLayer"
41
+
42
+
43
+ def load_model(model_dir):
44
+ """Pretrained SeqSleepNet from a local folder (config.json + model.pt) or the hub."""
45
+ if model_dir:
46
+ cfg = json.load(open(os.path.join(model_dir, "config.json")))
47
+ mod, cls = cfg["model_class"].split(":")
48
+ model = getattr(importlib.import_module(mod), cls)(**cfg["model_kwargs"])
49
+ state = torch.load(os.path.join(model_dir, "model.pt"), map_location="cpu", weights_only=True)
50
+ model.load_state_dict(state)
51
+ return model.eval(), cfg
52
+ from huggingface_hub import hf_hub_download
53
+ from physioex.models import load_from_pretrained
54
+
55
+ cfg = json.load(open(hf_hub_download("4rooms/physioex", "seqsleepnet-phan/config.json")))
56
+ return load_from_pretrained("seqsleepnet-phan", device="cpu").eval(), cfg
57
+
58
+
59
+ def n3_training_sequences(cfg, n, target_idx, log):
60
+ """``n`` training-split sequences (fold 0, all cohorts) whose target epoch is N3."""
61
+ from physioex.data.datasets import get_dataset
62
+ from physioex.data.multi import MultiDataset
63
+
64
+ MASS = get_dataset("mass")
65
+ L = cfg["training"]["sequence_length"]
66
+ cohorts = []
67
+ for c in cfg["training"]["dataset_cohorts"]:
68
+ cohorts.append(MASS(cohort=c, channels=cfg["training"]["channels"],
69
+ pipelines=cfg["training"]["pipeline_preset"], sequence_length=L))
70
+ log(f"cohort {c} indexed ({len(cohorts[-1])} sequences)")
71
+ ds = MultiDataset(cohorts)
72
+ train_idx = np.array(ds.split(fold=cfg["training"]["fold"])[0])
73
+ order = np.random.default_rng(0).permutation(len(train_idx))
74
+ xs, ys = [], []
75
+ for k in order:
76
+ item = ds[int(train_idx[k])]
77
+ if int(item["labels"][target_idx]) != CLASSES.index("N3"):
78
+ continue
79
+ xs.append(item["signals"][item["channel_order"][0]].unsqueeze(1).float()) # (L, 1, T, F)
80
+ ys.append(item["labels"].clone())
81
+ if len(xs) == n:
82
+ break
83
+ return torch.stack(xs), torch.stack(ys)
84
+
85
+
86
+ # --- alternative input-layer rules for the filterbank (demo of a *wrong* choice) ---
87
+
88
+
89
+ class _FBLinearRule(torch.autograd.Function):
90
+ @staticmethod
91
+ def forward(ctx, x, Wfb, mode):
92
+ ctx.save_for_backward(Wfb)
93
+ ctx.mode = mode
94
+ return torch.matmul(x, Wfb)
95
+
96
+ @staticmethod
97
+ def backward(ctx, R):
98
+ (Wfb,) = ctx.saved_tensors
99
+ if ctx.mode == "w2": # squared weights, independent of the input value
100
+ W2 = Wfb ** 2
101
+ W2 = W2 / (W2.sum(dim=-2, keepdim=True) + 1e-12)
102
+ return torch.matmul(R, W2.transpose(-1, -2)), None, None
103
+ F = Wfb.shape[-2] # flat: uniform over the frequency bins
104
+ return R.sum(-1, keepdim=True).expand(*R.shape[:-1], F) / F, None, None
105
+
106
+
107
+ class FilterbankAltRule(_PoolAdapter):
108
+ def __init__(self, orig, mode):
109
+ super().__init__(orig)
110
+ self.mode = mode
111
+
112
+ def forward(self, x):
113
+ with torch.no_grad():
114
+ Wfb = torch.mul(torch.sigmoid(self.orig.W), self.orig.S)
115
+ return _FBLinearRule.apply(x, Wfb, self.mode)
116
+
117
+
118
+ def run_lrp(model, x, target_idx, epsilon, gate_rule="signal_take", fb_mode=None, attn_naive=False):
119
+ saved = dict(lrpmodel._BUILTIN_BY_QUALNAME)
120
+ try:
121
+ if fb_mode:
122
+ lrpmodel._BUILTIN_BY_QUALNAME[FBQ] = lambda m, e, _mode=fb_mode: FilterbankAltRule(m, _mode)
123
+ if attn_naive:
124
+ lrpmodel._BUILTIN_BY_QUALNAME.pop(ATQ, None)
125
+ with warnings.catch_warnings():
126
+ warnings.simplefilter("ignore")
127
+ explainer = ModelLRP(model, in_index=target_idx, out_index=CLASSES.index("N3"), epsilon=epsilon)
128
+ for m in explainer.model.modules():
129
+ if isinstance(m, (LRPLSTM, LRPGRU)):
130
+ m.gate_rule = gate_rule
131
+ rel, report = explainer(x, return_report=True)
132
+ return rel.detach(), report.ratio.tolist(), explainer.uncovered
133
+ finally:
134
+ lrpmodel._BUILTIN_BY_QUALNAME.clear()
135
+ lrpmodel._BUILTIN_BY_QUALNAME.update(saved)
136
+
137
+
138
+ def main():
139
+ ap = argparse.ArgumentParser()
140
+ ap.add_argument("--out", default="n3_out")
141
+ ap.add_argument("--n", type=int, default=8)
142
+ ap.add_argument("--eps", type=float, default=1e-2)
143
+ ap.add_argument("--model-dir", default=os.environ.get("SEQSLEEPNET_DIR"))
144
+ args = ap.parse_args()
145
+ os.makedirs(args.out, exist_ok=True)
146
+ torch.set_num_threads(4)
147
+ t0 = time.time()
148
+ log = lambda *a: print(f"[{time.time() - t0:6.0f}s]", *a, flush=True) # noqa: E731
149
+
150
+ model, cfg = load_model(args.model_dir)
151
+ L = cfg["training"]["sequence_length"]
152
+ mid, n3 = L // 2, CLASSES.index("N3")
153
+ x, y = n3_training_sequences(cfg, args.n, mid, log)
154
+ with torch.no_grad():
155
+ logits = model(x)
156
+ log("x", tuple(x.shape), "N3 logits", [round(v, 2) for v in logits[:, mid, n3].tolist()])
157
+
158
+ f = SeqFunct(model, in_index=mid, out_index=n3)
159
+ results = {
160
+ "Saliency": Saliency(f)(x).detach(),
161
+ "Input x Gradient": InputXGradient(f)(x).detach(),
162
+ "Integrated Gradients (32 steps)": IntegratedGradients(f, steps=32)(x).detach(),
163
+ }
164
+ conserv = {}
165
+ variants = {
166
+ "LRP recommended": {},
167
+ "LRP gate uniform": {"gate_rule": "uniform"},
168
+ "LRP filterbank w2": {"fb_mode": "w2"},
169
+ "LRP filterbank flat": {"fb_mode": "flat"},
170
+ "LRP attention not ruled": {"attn_naive": True},
171
+ }
172
+ for eps in (1e-6, 1e-4, 1e-3, 1e-2, 1e-1):
173
+ variants[f"LRP eps={eps:g}"] = {"epsilon": eps}
174
+ for name, kw in variants.items():
175
+ rel, ratio, uncovered = run_lrp(model, x, mid, kw.pop("epsilon", args.eps), **kw)
176
+ results[name], conserv[name] = rel, ratio
177
+ log(f"{name}: sumR/f={[round(v, 3) for v in ratio]} uncovered={uncovered}")
178
+
179
+ freqs = np.arange(x.shape[-1]) * cfg["model_kwargs"]["fs"] / cfg["model_kwargs"]["nfft"]
180
+ torch.save({"x": x, "labels": y, "logits": logits, "results": results, "conservation": conserv,
181
+ "freqs": freqs, "target_index": mid}, os.path.join(args.out, "attributions.pt"))
182
+ json.dump({k: v for k, v in conserv.items()}, open(os.path.join(args.out, "conservation.json"), "w"), indent=1)
183
+ log("saved", args.out)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "2.0.2"
@@ -0,0 +1,34 @@
1
+ """PhysioEx explainability subsystem.
2
+
3
+ Sub-packages:
4
+ - ``posthoc`` — gradient-based attribution (Saliency, InputXGradient,
5
+ Integrated / Expected Gradients, and their DFT/STFT spectral variants).
6
+ - ``foundational`` — spectral-concept methods (CSD, specificity, ...).
7
+ - ``prototypes`` — prototype / concept discovery and reconstruction.
8
+ - ``lrp`` — Layer-wise Relevance Propagation (needs the ``explain``
9
+ extra: ``pip install "physioex[explain]"``).
10
+
11
+ The LRP entry points are re-exported lazily so that ``import physioex.explain``
12
+ does not pull in the optional ``zennit`` dependency.
13
+ """
14
+
15
+ _LAZY = {
16
+ "LRP": "physioex.explain.lrp.attributor",
17
+ "ModelLRP": "physioex.explain.lrp.model",
18
+ "prepare_model_for_lrp": "physioex.explain.lrp.model",
19
+ }
20
+
21
+ __all__ = sorted(_LAZY)
22
+
23
+
24
+ def __getattr__(name):
25
+ # PEP 562 lazy attribute — only import the LRP stack on first access.
26
+ if name in _LAZY:
27
+ import importlib
28
+
29
+ return getattr(importlib.import_module(_LAZY[name]), name)
30
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
31
+
32
+
33
+ def __dir__():
34
+ return sorted(set(globals()) | set(_LAZY))