arrayview 0.40.0__tar.gz → 0.41.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 (119) hide show
  1. {arrayview-0.40.0 → arrayview-0.41.1}/PKG-INFO +1 -1
  2. {arrayview-0.40.0 → arrayview-0.41.1}/pyproject.toml +2 -1
  3. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/ARCHITECTURE.md +1 -1
  4. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_analysis.py +1 -0
  5. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_io.py +39 -30
  6. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_launch_plan.py +9 -1
  7. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_launcher.py +413 -84
  8. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_lifecycle.py +20 -4
  9. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_platform.py +11 -2
  10. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_loading.py +100 -9
  11. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_query.py +75 -1
  12. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_state.py +55 -41
  13. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_vectorfield.py +41 -0
  14. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_server.py +107 -21
  15. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_session.py +20 -2
  16. arrayview-0.41.1/src/arrayview/_source_safety.py +775 -0
  17. arrayview-0.41.1/src/arrayview/_tutorial.py +194 -0
  18. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_viewer.html +960 -173
  19. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_vscode_browser.py +38 -23
  20. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_vscode_extension.py +202 -26
  21. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_vscode_signal.py +12 -3
  22. arrayview-0.41.1/src/arrayview/_watch.py +57 -0
  23. arrayview-0.41.1/src/arrayview/arrayview-opener.vsix +0 -0
  24. {arrayview-0.40.0 → arrayview-0.41.1}/tests/lifecycle_matrix.py +1 -1
  25. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_api.py +47 -10
  26. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_browser.py +50 -74
  27. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_display_opener_results.py +40 -1
  28. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_launch_blackbox.py +275 -3
  29. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_launch_plan.py +36 -3
  30. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_lifecycle_contract.py +299 -18
  31. arrayview-0.41.1/tests/test_nifti_meta.py +174 -0
  32. arrayview-0.41.1/tests/test_source_safety.py +716 -0
  33. arrayview-0.41.1/tests/test_tutorial_browser.py +402 -0
  34. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_vscode_ack_protocol.py +53 -0
  35. {arrayview-0.40.0 → arrayview-0.41.1}/tests/visual_smoke.py +23 -11
  36. arrayview-0.40.0/src/arrayview/_tutorial.py +0 -106
  37. arrayview-0.40.0/src/arrayview/arrayview-opener.vsix +0 -0
  38. arrayview-0.40.0/tests/test_command_reachability.py +0 -140
  39. arrayview-0.40.0/tests/test_cross_mode_parametrized.py +0 -214
  40. arrayview-0.40.0/tests/test_interactions.py +0 -2132
  41. arrayview-0.40.0/tests/test_nifti_meta.py +0 -59
  42. arrayview-0.40.0/tests/test_tutorial_browser.py +0 -197
  43. arrayview-0.40.0/tests/test_view_component_integration.py +0 -1675
  44. arrayview-0.40.0/tests/test_view_component_unit.py +0 -165
  45. {arrayview-0.40.0 → arrayview-0.41.1}/.gitignore +0 -0
  46. {arrayview-0.40.0 → arrayview-0.41.1}/LICENSE +0 -0
  47. {arrayview-0.40.0 → arrayview-0.41.1}/README.md +0 -0
  48. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/__init__.py +0 -0
  49. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/__main__.py +0 -0
  50. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_app.py +0 -0
  51. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_config.py +0 -0
  52. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_diagnostics.py +0 -0
  53. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_dicom.py +0 -0
  54. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_diff.py +0 -0
  55. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_icon.png +0 -0
  56. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_imaging.py +0 -0
  57. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_instance_registry.py +0 -0
  58. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_launch_trace.py +0 -0
  59. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_overlays.py +0 -0
  60. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_render.py +0 -0
  61. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_analysis.py +0 -0
  62. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_drop.py +0 -0
  63. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_export.py +0 -0
  64. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_persistence.py +0 -0
  65. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_preferences.py +0 -0
  66. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_preload.py +0 -0
  67. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_rendering.py +0 -0
  68. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_segmentation.py +0 -0
  69. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_routes_websocket.py +0 -0
  70. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_segmentation.py +0 -0
  71. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_session_spec.py +0 -0
  72. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_shell.html +0 -0
  73. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_synthetic_mri.py +0 -0
  74. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_torch.py +0 -0
  75. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_vectorfield.py +0 -0
  76. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/_vscode.py +0 -0
  77. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/gsap.min.js +0 -0
  78. {arrayview-0.40.0 → arrayview-0.41.1}/src/arrayview/todo.md +0 -0
  79. {arrayview-0.40.0 → arrayview-0.41.1}/tests/capture_patient_loading.py +0 -0
  80. {arrayview-0.40.0 → arrayview-0.41.1}/tests/capture_v_animation.py +0 -0
  81. {arrayview-0.40.0 → arrayview-0.41.1}/tests/conftest.py +0 -0
  82. {arrayview-0.40.0 → arrayview-0.41.1}/tests/make_vectorfield_test_arrays.py +0 -0
  83. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_1d_arrays.py +0 -0
  84. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_backend_shared.py +0 -0
  85. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_cli.py +0 -0
  86. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_colorbar_hover_highlight.py +0 -0
  87. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_config.py +0 -0
  88. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_dicom.py +0 -0
  89. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_doctor_commands.py +0 -0
  90. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_drop_import.py +0 -0
  91. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_instance_registry.py +0 -0
  92. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_invocation_contract_matrix.py +0 -0
  93. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_io.py +0 -0
  94. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_large_arrays.py +0 -0
  95. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_launch_diagnostics.py +0 -0
  96. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_launch_execution_ownership.py +0 -0
  97. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_launch_trace.py +0 -0
  98. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_loading_server.py +0 -0
  99. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_mode_consistency.py +0 -0
  100. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_mode_matrix.py +0 -0
  101. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_mode_roundtrip.py +0 -0
  102. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_nifti_series.py +0 -0
  103. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_preferences_ui.py +0 -0
  104. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_public_invocation_regressions.py +0 -0
  105. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_render_failure_reporting.py +0 -0
  106. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_render_progress.py +0 -0
  107. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_rgb_pixel_art.py +0 -0
  108. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_safe_server_stop.py +0 -0
  109. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_server_identity_status.py +0 -0
  110. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_session_spec.py +0 -0
  111. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_spawn_ownership.py +0 -0
  112. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_torch.py +0 -0
  113. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_tutorial.py +0 -0
  114. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_value_formatting.py +0 -0
  115. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_view_handle_lifecycle.py +0 -0
  116. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_view_launch_execution.py +0 -0
  117. {arrayview-0.40.0 → arrayview-0.41.1}/tests/test_window_level_preview.py +0 -0
  118. {arrayview-0.40.0 → arrayview-0.41.1}/tests/ui_audit.py +0 -0
  119. {arrayview-0.40.0 → arrayview-0.41.1}/tests/v_anim_frames/.gitkeep +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.40.0
3
+ Version: 0.41.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.40.0"
7
+ version = "0.41.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"
@@ -63,6 +63,7 @@ test = [
63
63
  "httpx>=0.27",
64
64
  "pytest-playwright>=0.5",
65
65
  "pillow>=12.1.1",
66
+ "nbclassic>=1.3,<2",
66
67
  ]
67
68
  docs = [
68
69
  "mkdocs>=1.6",
@@ -169,7 +169,7 @@ A refactoring in progress that introduces a View/Layer/Slicer/LayoutStrategy/Mod
169
169
  **Status:** Phases 1–15 complete. Phase 12 (keybind collapse) and Phase 17 (final cleanup) pending completion of render-pipeline migration from legacy globals to `displayState`.
170
170
 
171
171
  ### Command Registry
172
- All keybinds flow through a VS Code-style command registry in `_viewer.html`. Three tables: `commands` (id → `{title, when, run}`), `keybinds` (key+modifiers → command id), and `makeContext(state)` (mode/state flag bag). `dispatchCommand(e)` is wired as a prefix to the keydown handler; on a match it evaluates `when` against the context and runs the command, otherwise falls through. The help overlay is rendered at runtime from the `GUIDE_TABS` static data structure in `_viewer.html` — when adding or changing a keybind, update `GUIDE_TABS` manually. A `/`-triggered command palette fuzzy-searches all commands. Cross-mode enablement is guarded by `tests/test_command_reachability.py`.
172
+ All keybinds flow through a VS Code-style command registry in `_viewer.html`. Three tables: `commands` (id → `{title, when, run}`), `keybinds` (key+modifiers → command id), and `makeContext(state)` (mode/state flag bag). `dispatchCommand(e)` is wired as a prefix to the keydown handler; on a match it evaluates `when` against the context and runs the command, otherwise falls through. The help overlay is rendered at runtime from the `GUIDE_TABS` static data structure in `_viewer.html` — when adding or changing a keybind, update `GUIDE_TABS` manually. A `/`-triggered command palette fuzzy-searches all commands.
173
173
 
174
174
  ### Reconcilers
175
175
  Functions in the "UI Validation and Reconciliation" section (~line 13666) that enforce consistent UI state. When mode changes happen, reconcilers update visibility of containers, colorbars, dynamic islands, and compare sub-mode UI. There are four:
@@ -51,6 +51,7 @@ def _build_metadata(session) -> dict:
51
51
  "voxel_sizes": list(sm["voxel_sizes"]),
52
52
  "axis_labels": list(sm["axis_labels"]),
53
53
  "is_oblique": bool(sm["is_oblique"]),
54
+ "is_canonical": bool(sm.get("is_canonical", True)),
54
55
  }
55
56
  meta["ras_resample_active"] = bool(
56
57
  getattr(session, "ras_resample_active", False)
@@ -139,8 +139,13 @@ def _select_npz_array(npz, filepath):
139
139
  return npz[keys[0]]
140
140
 
141
141
 
142
- def _nifti_header_with_meta(filepath):
143
- """Return a NIfTI image and canonical spatial metadata without reading voxels."""
142
+ def _nifti_header_with_meta(filepath, *, native=False):
143
+ """Return a NIfTI image and spatial metadata without reading voxels.
144
+
145
+ Directory collections use canonical metadata because their lazy series
146
+ objects expose canonical axes. A single volume uses native metadata so an
147
+ uncompressed NIfTI can keep its proxy and storage-friendly axis order.
148
+ """
144
149
  nib = _nib()
145
150
  img = nib.load(filepath)
146
151
  original_affine = np.asarray(img.affine, dtype=np.float64)
@@ -151,30 +156,40 @@ def _nifti_header_with_meta(filepath):
151
156
  transform, img.shape[:3]
152
157
  )
153
158
 
154
- rot = affine_canonical[:3, :3]
159
+ data_affine = original_affine if native else affine_canonical
160
+ rot = data_affine[:3, :3]
155
161
  voxel_sizes = tuple(float(np.linalg.norm(rot[:, i])) for i in range(3))
162
+ canonical_rot = affine_canonical[:3, :3]
163
+ canonical_voxel_sizes = tuple(
164
+ float(np.linalg.norm(canonical_rot[:, i])) for i in range(3)
165
+ )
156
166
  norm_rot = np.zeros((3, 3))
157
167
  for i in range(3):
158
- if voxel_sizes[i] > 0:
159
- norm_rot[:, i] = rot[:, i] / voxel_sizes[i]
160
- pos_labels = ("R", "A", "S")
161
- neg_labels = ("L", "P", "I")
162
- axis_labels = tuple(
163
- pos_labels[i] if norm_rot[i, i] >= 0 else neg_labels[i] for i in range(3)
164
- )
168
+ if canonical_voxel_sizes[i] > 0:
169
+ norm_rot[:, i] = canonical_rot[:, i] / canonical_voxel_sizes[i]
170
+ axis_labels = tuple(str(v) for v in nib.aff2axcodes(data_affine))
165
171
  off_diag_max = max(
166
172
  (abs(norm_rot[i, j]) for i in range(3) for j in range(3) if i != j),
167
173
  default=0.0,
168
174
  )
169
175
  canonical_shape = tuple(int(img.shape[int(axis)]) for axis in transform[:, 0])
170
- return img, {
176
+ meta = {
171
177
  "affine": original_affine,
172
178
  "affine_canonical": affine_canonical,
179
+ "data_affine": data_affine,
173
180
  "voxel_sizes": voxel_sizes,
174
181
  "axis_labels": axis_labels,
175
182
  "is_oblique": bool(off_diag_max > 1e-3),
176
- "canonical_shape": canonical_shape,
183
+ "is_canonical": bool(
184
+ np.array_equal(transform, np.asarray([[0, 1], [1, 1], [2, 1]]))
185
+ ),
186
+ "to_canonical": transform,
177
187
  }
188
+ if native:
189
+ meta["spatial_shape"] = tuple(int(v) for v in img.shape[:3])
190
+ else:
191
+ meta["canonical_shape"] = canonical_shape
192
+ return img, meta
178
193
 
179
194
 
180
195
  def _nifti_display_array(proxy):
@@ -373,25 +388,19 @@ def _get_progressive_nifti_pool(*, overlay_prefetch=False):
373
388
 
374
389
 
375
390
  def _load_nifti_with_meta(filepath):
376
- """Load a NIfTI file, canonical-reorient, return (array, meta).
377
-
378
- meta is a dict with keys:
379
- affine : original 4x4 affine (RAS+ mm)
380
- affine_canonical : 4x4 affine after as_closest_canonical
381
- voxel_sizes : tuple (sx, sy, sz) in mm, post-reorient
382
- axis_labels : tuple of 3 strs from {"R","L","A","P","S","I"}
383
- — positive direction of each canonical axis
384
- is_oblique : bool — True if rotation part has off-diagonal magnitude > 1e-3
385
- after normalizing voxel sizes
386
- """
387
- img, meta = _nifti_header_with_meta(filepath)
388
- canon = _nib().as_closest_canonical(img)
389
-
390
- # NOTE: reorient requires materializing axis permutes/flips. .nii.gz is
391
- # already eager (gzip not seekable), so this is free; .nii loses mmap as a
392
- # necessary cost to apply the reorient.
393
- arr = np.asarray(canon.dataobj)
391
+ """Load a NIfTI file in native voxel order and return (array, meta).
394
392
 
393
+ Uncompressed .nii stays proxy-backed. This preserves cheap native-plane
394
+ reads even when the affine describes permuted or flipped anatomical axes.
395
+ Compressed .nii.gz still has to be decoded eagerly because gzip is not
396
+ random-access, but it is no longer silently reordered.
397
+ """
398
+ img, meta = _nifti_header_with_meta(filepath, native=True)
399
+ arr = (
400
+ img.dataobj
401
+ if filepath.endswith(".nii")
402
+ else _nifti_display_array(img.dataobj)
403
+ )
395
404
  return arr, meta
396
405
 
397
406
 
@@ -267,6 +267,8 @@ def _completion_target(
267
267
  if plan.display is Display.BROWSER:
268
268
  if _placement(evidence) is Placement.SSH:
269
269
  return CompletionTarget.GUIDANCE_PRINTED
270
+ if _placement(evidence) is Placement.VSCODE_REMOTE:
271
+ return CompletionTarget.FRAME_READY
270
272
  return CompletionTarget.DISPATCH_ACCEPTED
271
273
  if invocation in {Invocation.CLI, Invocation.VSCODE_EXPLORER}:
272
274
  return CompletionTarget.FRAME_READY
@@ -412,7 +414,9 @@ def plan_launch(
412
414
  window=None if remote_jupyter else window,
413
415
  inline=False if remote_jupyter else intent.inline,
414
416
  environment=environment,
415
- native_available=facts.native_backend is not None, reasons=reasons,
417
+ native_available=facts.native_backend is not None,
418
+ in_vscode_tunnel=facts.in_vscode_tunnel,
419
+ reasons=reasons,
416
420
  )
417
421
  if display is Display.VSCODE and environment not in {
418
422
  Environment.VSCODE_LOCAL, Environment.VSCODE_REMOTE
@@ -436,6 +440,7 @@ def _display_policy(
436
440
  inline: bool | None,
437
441
  environment: Environment,
438
442
  native_available: bool,
443
+ in_vscode_tunnel: bool,
439
444
  reasons: list[str],
440
445
  ) -> tuple[Display, Display | None, bool]:
441
446
  if window == "none":
@@ -461,6 +466,9 @@ def _display_policy(
461
466
  return Display.VSCODE, None, False
462
467
  return Display.VSCODE, Display.BROWSER, True
463
468
  if window == "browser":
469
+ if environment is Environment.VSCODE_REMOTE and in_vscode_tunnel:
470
+ reasons.append("remote_external_browser")
471
+ return Display.BROWSER, None, False
464
472
  if environment is Environment.VSCODE_REMOTE:
465
473
  reasons.append("remote_browser_redirected_to_vscode")
466
474
  return Display.VSCODE, None, False