arrayview 0.30.0__tar.gz → 0.31.1__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 (232) hide show
  1. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/project-state.md +18 -1
  2. {arrayview-0.30.0 → arrayview-0.31.1}/PKG-INFO +1 -1
  3. {arrayview-0.30.0 → arrayview-0.31.1}/docs/comparing.md +1 -1
  4. {arrayview-0.30.0 → arrayview-0.31.1}/docs/loading.md +24 -2
  5. {arrayview-0.30.0 → arrayview-0.31.1}/pyproject.toml +1 -1
  6. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_analysis.py +9 -2
  7. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_io.py +209 -40
  8. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_launcher.py +250 -20
  9. arrayview-0.31.1/src/arrayview/_lifecycle.py +64 -0
  10. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_overlays.py +16 -1
  11. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_render.py +58 -20
  12. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_loading.py +9 -2
  13. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_query.py +13 -1
  14. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_rendering.py +5 -0
  15. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_websocket.py +16 -0
  16. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_server.py +1 -0
  17. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_session.py +96 -0
  18. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_viewer.html +473 -60
  19. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_vscode_extension.py +1 -1
  20. arrayview-0.31.1/src/arrayview/arrayview-opener.vsix +0 -0
  21. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_api.py +199 -2
  22. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_backend_shared.py +57 -0
  23. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_browser.py +152 -4
  24. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_cli.py +162 -1
  25. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_lifecycle_contract.py +8 -0
  26. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_nifti_series.py +232 -1
  27. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_view_component_integration.py +11 -0
  28. {arrayview-0.30.0 → arrayview-0.31.1}/tests/visual_smoke.py +10 -0
  29. {arrayview-0.30.0 → arrayview-0.31.1}/uv.lock +1 -1
  30. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/extension.js +7 -2
  31. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/lifecycle_helpers.js +12 -0
  32. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/package.json +1 -1
  33. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/test_lifecycle_helpers.js +7 -0
  34. arrayview-0.30.0/src/arrayview/_lifecycle.py +0 -39
  35. arrayview-0.30.0/src/arrayview/arrayview-opener.vsix +0 -0
  36. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/frontend-designer/SKILL.md +0 -0
  37. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/invocation-consistency/SKILL.md +0 -0
  38. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/modes-consistency/SKILL.md +0 -0
  39. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/SKILL.md +0 -0
  40. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
  41. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
  42. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
  43. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/running-code.md +0 -0
  44. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/session-management.md +0 -0
  45. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
  46. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
  47. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/tracing.md +0 -0
  48. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
  49. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/todo-workflow/SKILL.md +0 -0
  50. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
  51. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
  52. {arrayview-0.30.0 → arrayview-0.31.1}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
  53. {arrayview-0.30.0 → arrayview-0.31.1}/.github/copilot-instructions.md +0 -0
  54. {arrayview-0.30.0 → arrayview-0.31.1}/.github/workflows/docs.yml +0 -0
  55. {arrayview-0.30.0 → arrayview-0.31.1}/.github/workflows/launch-contracts.yml +0 -0
  56. {arrayview-0.30.0 → arrayview-0.31.1}/.github/workflows/python-publish.yml +0 -0
  57. {arrayview-0.30.0 → arrayview-0.31.1}/.gitignore +0 -0
  58. {arrayview-0.30.0 → arrayview-0.31.1}/.ignore +0 -0
  59. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/AGENTS.md +0 -0
  60. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/ROUTER.md +0 -0
  61. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/SETUP.md +0 -0
  62. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/SYNC.md +0 -0
  63. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/architecture.md +0 -0
  64. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/conventions.md +0 -0
  65. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/decisions.md +0 -0
  66. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/frontend.md +0 -0
  67. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/lifecycle.md +0 -0
  68. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/render-pipeline.md +0 -0
  69. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/setup.md +0 -0
  70. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/context/stack.md +0 -0
  71. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/INDEX.md +0 -0
  72. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/README.md +0 -0
  73. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/add-file-format.md +0 -0
  74. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/add-server-endpoint.md +0 -0
  75. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/animation-verify.md +0 -0
  76. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/debug-render.md +0 -0
  77. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/debug-vscode-extension-python.md +0 -0
  78. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/extend-compare-mode.md +0 -0
  79. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/extract-server-route-module.md +0 -0
  80. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/patterns/frontend-change.md +0 -0
  81. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/setup.sh +0 -0
  82. {arrayview-0.30.0 → arrayview-0.31.1}/.mex/sync.sh +0 -0
  83. {arrayview-0.30.0 → arrayview-0.31.1}/.opencode/opencode.json +0 -0
  84. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
  85. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
  86. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
  87. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
  88. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
  89. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
  90. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
  91. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
  92. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
  93. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
  94. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
  95. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
  96. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
  97. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
  98. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
  99. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
  100. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
  101. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
  102. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
  103. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
  104. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
  105. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
  106. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
  107. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
  108. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
  109. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
  110. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
  111. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
  112. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
  113. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
  114. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
  115. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
  116. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
  117. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
  118. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
  119. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
  120. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
  121. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
  122. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
  123. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
  124. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
  125. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
  126. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
  127. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
  128. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
  129. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
  130. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
  131. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
  132. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
  133. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
  134. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
  135. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
  136. {arrayview-0.30.0 → arrayview-0.31.1}/.playwright-cli/theme-dark.yml +0 -0
  137. {arrayview-0.30.0 → arrayview-0.31.1}/.python-version +0 -0
  138. {arrayview-0.30.0 → arrayview-0.31.1}/.vscode/settings.json +0 -0
  139. {arrayview-0.30.0 → arrayview-0.31.1}/AGENTS.md +0 -0
  140. {arrayview-0.30.0 → arrayview-0.31.1}/CONTRIBUTING.md +0 -0
  141. {arrayview-0.30.0 → arrayview-0.31.1}/DESIGN.md +0 -0
  142. {arrayview-0.30.0 → arrayview-0.31.1}/LICENSE +0 -0
  143. {arrayview-0.30.0 → arrayview-0.31.1}/README.md +0 -0
  144. {arrayview-0.30.0 → arrayview-0.31.1}/docs/configuration.md +0 -0
  145. {arrayview-0.30.0 → arrayview-0.31.1}/docs/display.md +0 -0
  146. {arrayview-0.30.0 → arrayview-0.31.1}/docs/foreign-hosts.md +0 -0
  147. {arrayview-0.30.0 → arrayview-0.31.1}/docs/index.md +0 -0
  148. {arrayview-0.30.0 → arrayview-0.31.1}/docs/launch-routing-refactor.md +0 -0
  149. {arrayview-0.30.0 → arrayview-0.31.1}/docs/lifecycle.md +0 -0
  150. {arrayview-0.30.0 → arrayview-0.31.1}/docs/logo.png +0 -0
  151. {arrayview-0.30.0 → arrayview-0.31.1}/docs/measurement.md +0 -0
  152. {arrayview-0.30.0 → arrayview-0.31.1}/docs/remote.md +0 -0
  153. {arrayview-0.30.0 → arrayview-0.31.1}/docs/stylesheets/extra.css +0 -0
  154. {arrayview-0.30.0 → arrayview-0.31.1}/docs/viewing.md +0 -0
  155. {arrayview-0.30.0 → arrayview-0.31.1}/docs/vscode-window-routing-plan.md +0 -0
  156. {arrayview-0.30.0 → arrayview-0.31.1}/experiments/ws_tunnel_diag/ws_tunnel_diag.py +0 -0
  157. {arrayview-0.30.0 → arrayview-0.31.1}/matlab/arrayview.m +0 -0
  158. {arrayview-0.30.0 → arrayview-0.31.1}/mkdocs.yml +0 -0
  159. {arrayview-0.30.0 → arrayview-0.31.1}/plans/2026-04-14-immersive-animation.md +0 -0
  160. {arrayview-0.30.0 → arrayview-0.31.1}/plans/2026-05-07-unified-colormap-picker.md +0 -0
  161. {arrayview-0.30.0 → arrayview-0.31.1}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
  162. {arrayview-0.30.0 → arrayview-0.31.1}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
  163. {arrayview-0.30.0 → arrayview-0.31.1}/plans/egg-placement-mockup.html +0 -0
  164. {arrayview-0.30.0 → arrayview-0.31.1}/plans/refactoring_plan.md +0 -0
  165. {arrayview-0.30.0 → arrayview-0.31.1}/plans/roi-analysis-requirements.md +0 -0
  166. {arrayview-0.30.0 → arrayview-0.31.1}/plans/webview/LOG.md +0 -0
  167. {arrayview-0.30.0 → arrayview-0.31.1}/scripts/demo.py +0 -0
  168. {arrayview-0.30.0 → arrayview-0.31.1}/scripts/release.sh +0 -0
  169. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/ARCHITECTURE.md +0 -0
  170. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/__init__.py +0 -0
  171. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/__main__.py +0 -0
  172. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_app.py +0 -0
  173. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_codex_open.py +0 -0
  174. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_config.py +0 -0
  175. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_diff.py +0 -0
  176. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_icon.png +0 -0
  177. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_imaging.py +0 -0
  178. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_instance_registry.py +0 -0
  179. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_launch_plan.py +0 -0
  180. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_platform.py +0 -0
  181. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_analysis.py +0 -0
  182. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_export.py +0 -0
  183. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_persistence.py +0 -0
  184. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_preload.py +0 -0
  185. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_segmentation.py +0 -0
  186. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_state.py +0 -0
  187. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_routes_vectorfield.py +0 -0
  188. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_segmentation.py +0 -0
  189. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_session_spec.py +0 -0
  190. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_shell.html +0 -0
  191. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_synthetic_mri.py +0 -0
  192. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_torch.py +0 -0
  193. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_vectorfield.py +0 -0
  194. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_vscode.py +0 -0
  195. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_vscode_browser.py +0 -0
  196. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/_vscode_signal.py +0 -0
  197. {arrayview-0.30.0 → arrayview-0.31.1}/src/arrayview/gsap.min.js +0 -0
  198. {arrayview-0.30.0 → arrayview-0.31.1}/tests/capture_v_animation.py +0 -0
  199. {arrayview-0.30.0 → arrayview-0.31.1}/tests/conftest.py +0 -0
  200. {arrayview-0.30.0 → arrayview-0.31.1}/tests/lifecycle_matrix.py +0 -0
  201. {arrayview-0.30.0 → arrayview-0.31.1}/tests/make_vectorfield_test_arrays.py +0 -0
  202. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_colorbar_hover_highlight.py +0 -0
  203. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_command_reachability.py +0 -0
  204. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_config.py +0 -0
  205. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_cross_mode_parametrized.py +0 -0
  206. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_display_opener_results.py +0 -0
  207. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_doctor_commands.py +0 -0
  208. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_instance_registry.py +0 -0
  209. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_interactions.py +0 -0
  210. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_invocation_contract_matrix.py +0 -0
  211. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_io.py +0 -0
  212. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_large_arrays.py +0 -0
  213. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_launch_diagnostics.py +0 -0
  214. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_launch_plan.py +0 -0
  215. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_loading_server.py +0 -0
  216. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_mode_consistency.py +0 -0
  217. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_mode_matrix.py +0 -0
  218. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_mode_roundtrip.py +0 -0
  219. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_nifti_meta.py +0 -0
  220. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_rgb_pixel_art.py +0 -0
  221. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_safe_server_stop.py +0 -0
  222. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_server_identity_status.py +0 -0
  223. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_session_spec.py +0 -0
  224. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_torch.py +0 -0
  225. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_view_component_unit.py +0 -0
  226. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_view_handle_lifecycle.py +0 -0
  227. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_view_launch_execution.py +0 -0
  228. {arrayview-0.30.0 → arrayview-0.31.1}/tests/test_vscode_ack_protocol.py +0 -0
  229. {arrayview-0.30.0 → arrayview-0.31.1}/tests/ui_audit.py +0 -0
  230. {arrayview-0.30.0 → arrayview-0.31.1}/tests/v_anim_frames/.gitkeep +0 -0
  231. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/AGENTS.md +0 -0
  232. {arrayview-0.30.0 → arrayview-0.31.1}/vscode-extension/LICENSE +0 -0
@@ -32,8 +32,25 @@ last_updated: 2026-06-30
32
32
  - qMRI map toggle (`_islandToggleQmriMap`) fade animation now covers dimbar and array-name in addition to panes and colorbars
33
33
  - Segmentation menu shares the ROI layout (yellow accent, magnifier action icon, common `#export-overlay` modal). Pre-activation shows a pulsing "nnInteractive · connecting" row that morphs into the normal shape toolbar once `/seg/activate` resolves
34
34
  - Overlays are a plugin tile (`OV`): per-overlay row with colour swatch, editable label, eye toggle, × delete; shared opacity slider; `+ add overlay` opens a filesystem picker rooted at the launched file's directory
35
+ - The floating overlay HUD is reconciled as soon as startup loading ends, so it
36
+ is visible on first load and `/ o` toggles it correctly from the first press.
37
+ Its drag grip allows viewport-clamped repositioning that survives mode and
38
+ layout reconciliation for the current viewer session. Hovering a visible row
39
+ focuses that mask and dims the other visible overlays across normal, compare,
40
+ multiview, and qMRI render paths. The HUD and overlay drawer can switch masks
41
+ between filled regions and outline-only contours.
35
42
  - Vector field is a plugin tile (`VF`): row with visibility toggle + density/length sliders bi-directionally wired to the `[ ] { }` keyboard commands
36
- - CLI `--overlay FILE` can be repeated to load multiple overlays at launch
43
+ - CLI `--overlay FILE` can be repeated to load multiple overlays at launch;
44
+ unnamed overlays use the filename stem and `NAME=FILE` supplies an explicit
45
+ HUD label.
46
+ - Stack collections support `--overlay-dir PATTERN` to discover per-case mask
47
+ filenames as sparse overlay roles; missing masks render as empty and common
48
+ patient/modality directory layouts pair automatically. `--case-regex` remains
49
+ an override for unusual layouts. CLI header scans show an in-place file counter
50
+ and elapsed time on interactive terminals.
51
+ - Reused collection sessions are protected by per-tab leases, including their
52
+ overlay sessions, and VS Code opener readiness now verifies the requested
53
+ session metadata before acknowledging `backend_ready`.
37
54
  - Filesystem picker endpoint (`GET /fs/list`) clamped to `$HOME`. Accepts `base_sid` + `mode` (`overlay` | `vectorfield`) to filter entries by a cheap header-shape peek (`.npy`, `.nii`/`.nii.gz`, `.h5`, `.zarr`); overlay mode requires identical shape, vectorfield mode requires base shape plus one axis of size 3
38
55
  - Island collapse affordance: inline `~` at the island's top-right animates the panel into the bottom-left `~` hint circle; external `~` hint only visible while the island is actually collapsed. New `/` hint circle at bottom-right opens the tool menu
39
56
  - Compare center tool menu: `/` now re-opens the last-used compare center mode from the tool menu, while compare pane header buttons select diff / overlay / wipe directly. Eligible two-array compare layouts can switch into a big-left arrangement with a wider center pane, and the diff colorbar now matches that center-pane width.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.30.0
3
+ Version: 0.31.1
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -37,7 +37,7 @@ Or use the file picker (`Cmd/Ctrl+O`) or drag-and-drop.
37
37
  uvx arrayview volume.nii.gz --overlay mask.nii.gz
38
38
  ```
39
39
 
40
- Binary mask (0/1), same spatial shape. `[`/`]` adjusts blend opacity. Multiple overlays get automatic palette colors.
40
+ Binary mask (0/1), same spatial shape. `[`/`]` adjusts blend opacity. Multiple overlays get automatic palette colors. Hover an overlay HUD row to focus that mask while dimming the others. Use the overlay HUD or `/ o` to switch between filled masks and outline-only contours.
41
41
 
42
42
  For cohort review, use `--stack` so image and overlay patterns are paired into
43
43
  aligned stacks:
@@ -85,8 +85,18 @@ Native windows also show a static preview while the live viewer warms up.
85
85
  ```bash
86
86
  uvx arrayview base.npy moving.npy # compare mode
87
87
  uvx arrayview volume.nii.gz --overlay mask.nii.gz
88
+ uvx arrayview volume.nii.gz --overlay "ground truth=mask.nii.gz"
88
89
  ```
89
90
 
91
+ An unnamed overlay uses the filename stem as its display name (`mask` above).
92
+ Use `NAME=FILE` when a more descriptive label is useful.
93
+ The overlay list opens at the top-right of the image and can be repositioned
94
+ with its small drag grip. Hover a visible row to focus that mask; the other
95
+ visible masks are dimmed until the pointer leaves the list. Use the HUD
96
+ `filled`/`outline` button, or the same control in `/ o`, to switch masks between
97
+ filled regions and contour-only outlines. The eye in the HUD header hides or
98
+ shows all overlays at once.
99
+
90
100
  ## Directory Pattern Collections
91
101
 
92
102
  Use `--stack` when reviewing many arrays. Passing a directory scans supported
@@ -121,6 +131,18 @@ uvx arrayview --stack \
121
131
  --overlay "pred=data/**/pred/**/*.nii.gz"
122
132
  ```
123
133
 
134
+ When every case has a directory containing separately named masks,
135
+ `--overlay-dir` discovers one overlay role per filename. Masks that are absent
136
+ for a case are shown as empty. ArrayView infers the shared case directory from
137
+ layouts such as `sub-0001/T1_W/` and `sub-0001/masks/`:
138
+
139
+ ```bash
140
+ uvx arrayview --stack "data/*/T1_W/*.nii.gz" \
141
+ --overlay-dir "data/*/masks"
142
+ ```
143
+
144
+ Repeat `--overlay-dir` to combine masks from multiple per-case directories.
145
+
124
146
  ArrayView pairs files by sorted match order. The first match from each pattern
125
147
  belongs to the first case, the second match belongs to the second case, and so
126
148
  on. Use `--dry-run` to inspect matches without opening a viewer.
@@ -129,8 +151,8 @@ on. Use `--dry-run` to inspect matches without opening a viewer.
129
151
  uvx arrayview --stack "data/**/*_0000.nii.gz" --overlay "gt=data/**/labels/*.nii.gz" --dry-run
130
152
  ```
131
153
 
132
- If sorted order is not enough, pass a regex with a `case` group to pair files
133
- by case id instead:
154
+ For unusual or ambiguous layouts, pass a regex with a `case` group to override
155
+ automatic directory inference:
134
156
 
135
157
  ```bash
136
158
  uvx arrayview --stack "data/**/*.nii.gz" --case-regex "(?P<case>patient-[0-9]+)"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.30.0"
7
+ version = "0.31.1"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -34,7 +34,15 @@ def _build_metadata(session) -> dict:
34
34
  "is_rgb": session.rgb_axis is not None,
35
35
  "has_source_file": bool(getattr(session, "filepath", None)),
36
36
  }
37
- default_dims = _session_mod._startup_dims_for_data(session.data, target_shape)
37
+ collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
38
+ default_shape = target_shape
39
+ if collection_spatial_ndim is not None:
40
+ spatial_ndim = int(collection_spatial_ndim)
41
+ if 2 <= spatial_ndim <= len(target_shape):
42
+ # Collection axes are appended after the image dimensions. They
43
+ # select a case; they must not influence the startup plane.
44
+ default_shape = target_shape[:spatial_ndim]
45
+ default_dims = _session_mod._startup_dims_for_data(session.data, default_shape)
38
46
  if default_dims is not None:
39
47
  meta["default_dims"] = [int(default_dims[0]), int(default_dims[1])]
40
48
  if getattr(session, "spatial_meta", None) is not None:
@@ -49,7 +57,6 @@ def _build_metadata(session) -> dict:
49
57
  )
50
58
  if getattr(session, "array_keys", None):
51
59
  meta["array_keys"] = session.array_keys
52
- collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
53
60
  if collection_spatial_ndim is not None:
54
61
  meta["collection_spatial_ndim"] = int(collection_spatial_ndim)
55
62
  if getattr(session.data, "_av_ragged", False):
@@ -7,6 +7,7 @@ import glob
7
7
  import os
8
8
  import re
9
9
  import threading
10
+ from concurrent.futures import ThreadPoolExecutor
10
11
  from collections import OrderedDict
11
12
 
12
13
  import numpy as np
@@ -249,6 +250,8 @@ class _NiftiSeries:
249
250
  self._vol_cache.move_to_end(cache_key)
250
251
  return self._vol_cache[cache_key]
251
252
  filepath = self._file_matrix[p_idx][m_idx]
253
+ if filepath is None:
254
+ return np.zeros(self._vol_shape, dtype=self._dtype)
252
255
  nib = _nib()
253
256
  img = nib.load(filepath)
254
257
  canon = nib.as_closest_canonical(img)
@@ -347,6 +350,8 @@ class _FileSeries:
347
350
  self._vol_cache.move_to_end(cache_key)
348
351
  return self._vol_cache[cache_key]
349
352
  filepath = self._file_matrix[p_idx][m_idx]
353
+ if filepath is None:
354
+ return np.zeros(self._vol_shape, dtype=self._dtype)
350
355
  vol = load_data(filepath)
351
356
  with self._cache_lock:
352
357
  self._vol_cache[cache_key] = vol
@@ -493,6 +498,8 @@ class _RaggedFileSeries:
493
498
  self._vol_cache.move_to_end(cache_key)
494
499
  return self._vol_cache[cache_key]
495
500
  filepath = self._file_matrix[p_idx][m_idx]
501
+ if filepath is None:
502
+ return np.zeros(self._spatial_shapes[p_idx][m_idx], dtype=self._dtype)
496
503
  if self._all_nifti:
497
504
  nib = _nib()
498
505
  img = nib.load(filepath)
@@ -612,7 +619,43 @@ def _collection_pattern_map(pattern, case_regex):
612
619
  return by_case
613
620
 
614
621
 
615
- def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
622
+ def _collection_ancestor_case_key(filepath, depth):
623
+ parent = os.path.abspath(filepath)
624
+ for _ in range(depth):
625
+ parent = os.path.dirname(parent)
626
+ return os.path.basename(parent)
627
+
628
+
629
+ def _collection_paths_map(paths, pattern, *, ancestor_depth):
630
+ by_case = {}
631
+ for path in paths:
632
+ case = _collection_ancestor_case_key(path, ancestor_depth)
633
+ if not case or case in by_case:
634
+ return None
635
+ by_case[case] = path
636
+ return by_case
637
+
638
+
639
+ def _infer_collection_layout_maps(path_lists, patterns):
640
+ """Infer a shared per-case directory depth for collection patterns."""
641
+ if not path_lists or len(path_lists[0]) < 2:
642
+ return None, None
643
+ for depth in (1, 2, 3):
644
+ maps = [
645
+ _collection_paths_map(paths, pattern, ancestor_depth=depth)
646
+ for paths, pattern in zip(path_lists, patterns)
647
+ ]
648
+ if any(by_case is None for by_case in maps):
649
+ continue
650
+ case_ids = set(maps[0])
651
+ if case_ids and all(set(by_case) == case_ids for by_case in maps[1:]):
652
+ return maps, depth
653
+ return None, None
654
+
655
+
656
+ def _series_from_file_matrix(
657
+ file_matrix, *, load="lazy", stack="auto", scan_progress=None, scan_label=None
658
+ ):
616
659
  if load not in {"lazy", "eager"}:
617
660
  raise ValueError("load must be 'lazy' or 'eager'.")
618
661
  if stack not in {"auto", "dense", "ragged"}:
@@ -620,17 +663,19 @@ def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
620
663
  if not file_matrix or not file_matrix[0]:
621
664
  raise ValueError("Cannot build an empty file series.")
622
665
 
623
- all_paths = [path for row in file_matrix for path in row]
666
+ all_paths = [path for row in file_matrix for path in row if path is not None]
667
+ if not all_paths:
668
+ raise ValueError("Cannot build a file series without any files.")
624
669
  all_nifti = all(_is_nifti_path(path) for path in all_paths)
625
670
  ref_shape = None
626
671
  ref_dtype = None
627
672
  spatial_meta = None
628
673
 
629
674
  if all_nifti:
630
- nib = _nib()
631
675
  spatial_shapes = []
632
- for fpath in all_paths:
633
- img, item_meta = _nifti_header_with_meta(fpath)
676
+ for fpath, img, item_meta in _iter_nifti_headers(
677
+ all_paths, scan_progress=scan_progress, scan_label=scan_label
678
+ ):
634
679
  shape = tuple(item_meta["canonical_shape"])
635
680
  dtype = img.get_data_dtype()
636
681
  spatial_shapes.append(shape)
@@ -651,8 +696,14 @@ def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
651
696
  shape_matrix = []
652
697
  pos = 0
653
698
  for row in file_matrix:
654
- shape_matrix.append(spatial_shapes[pos : pos + len(row)])
655
- pos += len(row)
699
+ row_shapes = []
700
+ for path in row:
701
+ if path is None:
702
+ row_shapes.append(ref_shape)
703
+ else:
704
+ row_shapes.append(spatial_shapes[pos])
705
+ pos += 1
706
+ shape_matrix.append(row_shapes)
656
707
  shapes_differ = any(shape != ref_shape for shape in spatial_shapes)
657
708
  if stack == "dense" and shapes_differ:
658
709
  raise ValueError("Dense stacking requires every file to have the same shape.")
@@ -668,8 +719,9 @@ def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
668
719
  series = _NiftiSeries(file_matrix, ref_shape, ref_dtype, spatial_meta)
669
720
  if load == "eager":
670
721
  for p, row in enumerate(file_matrix):
671
- for m, _path in enumerate(row):
672
- series._get_volume(p, m)
722
+ for m, path in enumerate(row):
723
+ if path is not None:
724
+ series._get_volume(p, m)
673
725
  return series, spatial_meta
674
726
 
675
727
  spatial_shapes = []
@@ -691,11 +743,19 @@ def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
691
743
  f"Dtype mismatch: {os.path.basename(fpath)!r} has dtype "
692
744
  f"{dtype}, expected {ref_dtype}."
693
745
  )
746
+ if scan_progress is not None:
747
+ scan_progress(scan_label, fpath)
694
748
  shape_matrix = []
695
749
  pos = 0
696
750
  for row in file_matrix:
697
- shape_matrix.append(spatial_shapes[pos : pos + len(row)])
698
- pos += len(row)
751
+ row_shapes = []
752
+ for path in row:
753
+ if path is None:
754
+ row_shapes.append(ref_shape)
755
+ else:
756
+ row_shapes.append(spatial_shapes[pos])
757
+ pos += 1
758
+ shape_matrix.append(row_shapes)
699
759
  shapes_differ = any(shape != ref_shape for shape in spatial_shapes)
700
760
  if stack == "dense" and shapes_differ:
701
761
  raise ValueError("Dense stacking requires every file to have the same shape.")
@@ -705,23 +765,63 @@ def _series_from_file_matrix(file_matrix, *, load="lazy", stack="auto"):
705
765
  series = _FileSeries(file_matrix, ref_shape, ref_dtype, spatial_meta=None)
706
766
  if load == "eager":
707
767
  for p, row in enumerate(file_matrix):
708
- for m, _path in enumerate(row):
709
- series._get_volume(p, m)
768
+ for m, path in enumerate(row):
769
+ if path is not None:
770
+ series._get_volume(p, m)
710
771
  return series, None
711
772
 
712
773
 
713
- def load_dir_collection(base_patterns, overlays=None, case_regex=None, *, load="lazy", stack="auto"):
774
+ def _collection_scan_workers():
775
+ raw = os.environ.get("ARRAYVIEW_COLLECTION_SCAN_WORKERS")
776
+ if raw is not None:
777
+ try:
778
+ return max(1, int(raw))
779
+ except ValueError:
780
+ return 1
781
+ cpu = os.cpu_count() or 4
782
+ return max(1, min(8, cpu))
783
+
784
+
785
+ def _iter_nifti_headers(paths, *, scan_progress=None, scan_label=None):
786
+ workers = _collection_scan_workers()
787
+ if workers <= 1 or len(paths) < 8:
788
+ for path in paths:
789
+ img, item_meta = _nifti_header_with_meta(path)
790
+ if scan_progress is not None:
791
+ scan_progress(scan_label, path)
792
+ yield path, img, item_meta
793
+ return
794
+
795
+ with ThreadPoolExecutor(max_workers=workers) as executor:
796
+ for path, result in zip(paths, executor.map(_nifti_header_with_meta, paths)):
797
+ img, item_meta = result
798
+ if scan_progress is not None:
799
+ scan_progress(scan_label, path)
800
+ yield path, img, item_meta
801
+
802
+
803
+ def load_dir_collection(
804
+ base_patterns,
805
+ overlays=None,
806
+ case_regex=None,
807
+ *,
808
+ load="lazy",
809
+ stack="auto",
810
+ scan_progress=None,
811
+ ):
714
812
  """Load recursive collection patterns as aligned lazy image/overlay stacks.
715
813
 
716
814
  *base_patterns* are image channel/modality patterns. *overlays* is an
717
- ordered list of ``(name, pattern)`` pairs. By default, each pattern's
718
- sorted matches are paired by position. With *case_regex*, files are paired
719
- by the regex's named ``case`` group instead.
815
+ ordered list of ``(name, pattern)`` pairs. A third true value marks a
816
+ sparse overlay whose missing cases should render as empty masks. By
817
+ default, each pattern's sorted matches are paired by position. With
818
+ *case_regex*, files are paired by the regex's named ``case`` group instead.
720
819
  """
721
820
  if not base_patterns:
722
821
  raise ValueError("--stack requires at least one positional image pattern.")
723
822
  overlays = overlays or []
724
823
 
824
+ inferred_case_depth = None
725
825
  if case_regex:
726
826
  base_maps = [
727
827
  _collection_pattern_map(pattern, case_regex=case_regex)
@@ -738,56 +838,125 @@ def load_dir_collection(base_patterns, overlays=None, case_regex=None, *, load="
738
838
  base_matrix = [[by_case[case] for by_case in base_maps] for case in case_ids]
739
839
  else:
740
840
  base_lists = [_collection_pattern_paths(pattern) for pattern in base_patterns]
741
- n_cases = len(base_lists[0])
742
- for idx, paths in enumerate(base_lists[1:], start=2):
743
- if len(paths) != n_cases:
744
- raise ValueError(
745
- f"Image pattern {idx} matched {len(paths)} file(s), "
746
- f"expected {n_cases} to match image pattern 1."
747
- )
748
- case_ids = [
749
- _default_collection_case_key(path)
750
- for path in base_lists[0]
751
- ]
752
- if len(set(case_ids)) != len(case_ids):
841
+ inferred_maps, inferred_case_depth = _infer_collection_layout_maps(
842
+ base_lists, base_patterns
843
+ )
844
+ if inferred_maps is not None:
845
+ case_ids = sorted(inferred_maps[0])
846
+ base_matrix = [
847
+ [by_case[case] for by_case in inferred_maps] for case in case_ids
848
+ ]
849
+ else:
850
+ n_cases = len(base_lists[0])
851
+ for idx, paths in enumerate(base_lists[1:], start=2):
852
+ if len(paths) != n_cases:
853
+ raise ValueError(
854
+ f"Image pattern {idx} matched {len(paths)} file(s), "
855
+ f"expected {n_cases} to match image pattern 1."
856
+ )
753
857
  case_ids = [
754
- os.path.basename(os.path.dirname(path)) or _default_collection_case_key(path)
858
+ _default_collection_case_key(path)
755
859
  for path in base_lists[0]
756
860
  ]
757
- base_matrix = [list(row) for row in zip(*base_lists)]
758
- data, spatial_meta = _series_from_file_matrix(base_matrix, load=load, stack=stack)
861
+ if len(set(case_ids)) != len(case_ids):
862
+ case_ids = [
863
+ os.path.basename(os.path.dirname(path))
864
+ or _default_collection_case_key(path)
865
+ for path in base_lists[0]
866
+ ]
867
+ base_matrix = [list(row) for row in zip(*base_lists)]
868
+ data, spatial_meta = _series_from_file_matrix(
869
+ base_matrix,
870
+ load=load,
871
+ stack=stack,
872
+ scan_progress=scan_progress,
873
+ scan_label="images",
874
+ )
759
875
  spatial_shape = data._vol_shape
760
876
 
761
877
  overlay_items = []
762
878
  overlay_reports = []
763
- for name, pattern in overlays:
764
- if case_regex:
879
+ for overlay_spec in overlays:
880
+ name, pattern = overlay_spec[:2]
881
+ allow_missing = len(overlay_spec) > 2 and bool(overlay_spec[2])
882
+ report_pattern = overlay_spec[3] if len(overlay_spec) > 3 else pattern
883
+ if isinstance(pattern, dict):
884
+ by_case = pattern
885
+ missing_cases = [case for case in case_ids if case not in by_case]
886
+ if missing_cases and not allow_missing:
887
+ raise ValueError(
888
+ f"Overlay {name!r} is missing case(s): "
889
+ f"{', '.join(repr(c) for c in missing_cases)}."
890
+ )
891
+ matrix = [[by_case.get(case)] for case in case_ids]
892
+ extras = sorted(set(by_case.keys()) - set(case_ids))
893
+ elif case_regex:
765
894
  by_case = _collection_pattern_map(pattern, case_regex=case_regex)
766
895
  missing_cases = [case for case in case_ids if case not in by_case]
767
- if missing_cases:
896
+ if missing_cases and not allow_missing:
768
897
  raise ValueError(
769
898
  f"Overlay {name!r} is missing case(s): "
770
899
  f"{', '.join(repr(c) for c in missing_cases)}."
771
900
  )
772
- matrix = [[by_case[case]] for case in case_ids]
901
+ matrix = [[by_case.get(case)] for case in case_ids]
773
902
  extras = sorted(set(by_case.keys()) - set(case_ids))
903
+ elif inferred_case_depth is not None:
904
+ paths = _collection_pattern_paths(pattern)
905
+ by_case = _collection_paths_map(
906
+ paths, pattern, ancestor_depth=inferred_case_depth
907
+ )
908
+ if by_case is None:
909
+ raise ValueError(
910
+ f"Overlay {name!r} has multiple files for an inferred case. "
911
+ "Use --case-regex to describe this layout explicitly."
912
+ )
913
+ missing_cases = [case for case in case_ids if case not in by_case]
914
+ if missing_cases and not allow_missing:
915
+ raise ValueError(
916
+ f"Overlay {name!r} is missing case(s): "
917
+ f"{', '.join(repr(c) for c in missing_cases)}."
918
+ )
919
+ matrix = [[by_case.get(case)] for case in case_ids]
920
+ extras = sorted(set(by_case) - set(case_ids))
774
921
  else:
775
922
  paths = _collection_pattern_paths(pattern)
776
- if len(paths) != len(case_ids):
923
+ if len(paths) != len(case_ids) and not allow_missing:
777
924
  raise ValueError(
778
925
  f"Overlay {name!r} matched {len(paths)} file(s), "
779
926
  f"expected {len(case_ids)} to match image pattern 1."
780
927
  )
928
+ if allow_missing:
929
+ raise ValueError(
930
+ "Could not infer case directories for sparse overlays. "
931
+ "Use a per-case directory layout or pass --case-regex."
932
+ )
781
933
  matrix = [[path] for path in paths]
782
934
  extras = []
783
- ov_data, _ov_meta = _series_from_file_matrix(matrix, load=load, stack=stack)
935
+ ov_data, _ov_meta = _series_from_file_matrix(
936
+ matrix,
937
+ load=load,
938
+ stack=stack,
939
+ scan_progress=scan_progress,
940
+ scan_label=name,
941
+ )
784
942
  if not getattr(data, "_av_ragged", False) and ov_data._vol_shape != spatial_shape:
785
943
  raise ValueError(
786
944
  f"Overlay {name!r} has spatial shape {ov_data._vol_shape}, "
787
945
  f"expected {spatial_shape}."
788
946
  )
789
- overlay_items.append({"name": name, "data": ov_data, "pattern": pattern})
790
- overlay_reports.append({"name": name, "pattern": pattern, "ignored_cases": extras})
947
+ overlay_items.append({"name": name, "data": ov_data, "pattern": report_pattern})
948
+ overlay_reports.append(
949
+ {
950
+ "name": name,
951
+ "pattern": report_pattern,
952
+ "ignored_cases": extras,
953
+ "missing_cases": (
954
+ missing_cases
955
+ if case_regex or inferred_case_depth is not None
956
+ else []
957
+ ),
958
+ }
959
+ )
791
960
 
792
961
  summary = {
793
962
  "cases": case_ids,