arrayview 0.47.0__tar.gz → 0.47.3__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 (118) hide show
  1. {arrayview-0.47.0 → arrayview-0.47.3}/PKG-INFO +1 -1
  2. {arrayview-0.47.0 → arrayview-0.47.3}/pyproject.toml +1 -1
  3. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_io.py +41 -0
  4. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_launcher.py +6 -0
  5. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_loading.py +13 -0
  6. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_preferences.py +1 -1
  7. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_session.py +6 -0
  8. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_viewer.html +6 -0
  9. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_vscode_extension.py +1 -1
  10. arrayview-0.47.3/src/arrayview/arrayview-opener.vsix +0 -0
  11. {arrayview-0.47.0 → arrayview-0.47.3}/tests/conftest.py +15 -0
  12. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_api.py +626 -4092
  13. arrayview-0.47.3/tests/test_browser.py +1010 -0
  14. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_launch_blackbox.py +69 -283
  15. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_nifti_meta.py +23 -0
  16. arrayview-0.47.3/tests/test_vsix_bundle.py +19 -0
  17. arrayview-0.47.0/src/arrayview/arrayview-opener.vsix +0 -0
  18. arrayview-0.47.0/tests/lifecycle_matrix.py +0 -416
  19. arrayview-0.47.0/tests/test_bootstrap_app.py +0 -114
  20. arrayview-0.47.0/tests/test_browser.py +0 -7533
  21. arrayview-0.47.0/tests/test_colorbar_hover_highlight.py +0 -156
  22. arrayview-0.47.0/tests/test_display_opener_results.py +0 -536
  23. arrayview-0.47.0/tests/test_doctor_commands.py +0 -104
  24. arrayview-0.47.0/tests/test_invocation_contract_matrix.py +0 -232
  25. arrayview-0.47.0/tests/test_launch_diagnostics.py +0 -137
  26. arrayview-0.47.0/tests/test_launch_execution_ownership.py +0 -283
  27. arrayview-0.47.0/tests/test_launch_plan.py +0 -816
  28. arrayview-0.47.0/tests/test_launch_trace.py +0 -231
  29. arrayview-0.47.0/tests/test_lifecycle_contract.py +0 -2511
  30. arrayview-0.47.0/tests/test_loading_server.py +0 -107
  31. arrayview-0.47.0/tests/test_mode_consistency.py +0 -1248
  32. arrayview-0.47.0/tests/test_mode_matrix.py +0 -658
  33. arrayview-0.47.0/tests/test_preferences_ui.py +0 -185
  34. arrayview-0.47.0/tests/test_public_invocation_regressions.py +0 -350
  35. arrayview-0.47.0/tests/test_render_failure_reporting.py +0 -74
  36. arrayview-0.47.0/tests/test_render_progress.py +0 -275
  37. arrayview-0.47.0/tests/test_roi_hud.py +0 -200
  38. arrayview-0.47.0/tests/test_server_identity_status.py +0 -82
  39. arrayview-0.47.0/tests/test_session_spec.py +0 -93
  40. arrayview-0.47.0/tests/test_spawn_ownership.py +0 -175
  41. arrayview-0.47.0/tests/test_tunnel_warm_path.py +0 -160
  42. arrayview-0.47.0/tests/test_tutorial_browser.py +0 -481
  43. arrayview-0.47.0/tests/test_value_formatting.py +0 -126
  44. arrayview-0.47.0/tests/test_view_handle_lifecycle.py +0 -110
  45. arrayview-0.47.0/tests/test_view_launch_execution.py +0 -311
  46. arrayview-0.47.0/tests/test_vscode_ack_protocol.py +0 -485
  47. arrayview-0.47.0/tests/test_window_level_preview.py +0 -101
  48. {arrayview-0.47.0 → arrayview-0.47.3}/.gitignore +0 -0
  49. {arrayview-0.47.0 → arrayview-0.47.3}/LICENSE +0 -0
  50. {arrayview-0.47.0 → arrayview-0.47.3}/README.md +0 -0
  51. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/ARCHITECTURE.md +0 -0
  52. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/__init__.py +0 -0
  53. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/__main__.py +0 -0
  54. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_analysis.py +0 -0
  55. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_app.py +0 -0
  56. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_bootstrap_app.py +0 -0
  57. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_config.py +0 -0
  58. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_diagnostics.py +0 -0
  59. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_dicom.py +0 -0
  60. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_diff.py +0 -0
  61. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_extra_ports.py +0 -0
  62. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_icon.png +0 -0
  63. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_imaging.py +0 -0
  64. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_instance_registry.py +0 -0
  65. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_launch_plan.py +0 -0
  66. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_launch_trace.py +0 -0
  67. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_lifecycle.py +0 -0
  68. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_overlays.py +0 -0
  69. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_platform.py +0 -0
  70. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_render.py +0 -0
  71. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_analysis.py +0 -0
  72. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_drop.py +0 -0
  73. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_export.py +0 -0
  74. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_persistence.py +0 -0
  75. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_preload.py +0 -0
  76. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_query.py +0 -0
  77. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_rendering.py +0 -0
  78. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_segmentation.py +0 -0
  79. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_state.py +0 -0
  80. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_vectorfield.py +0 -0
  81. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_routes_websocket.py +0 -0
  82. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_segmentation.py +0 -0
  83. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_server.py +0 -0
  84. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_session_spec.py +0 -0
  85. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_shell.html +0 -0
  86. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_source_safety.py +0 -0
  87. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_synthetic_mri.py +0 -0
  88. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_torch.py +0 -0
  89. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_tutorial.py +0 -0
  90. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_vectorfield.py +0 -0
  91. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_vscode.py +0 -0
  92. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_vscode_browser.py +0 -0
  93. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_vscode_signal.py +0 -0
  94. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/_watch.py +0 -0
  95. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/gsap.min.js +0 -0
  96. {arrayview-0.47.0 → arrayview-0.47.3}/src/arrayview/todo.md +0 -0
  97. {arrayview-0.47.0 → arrayview-0.47.3}/tests/capture_patient_loading.py +0 -0
  98. {arrayview-0.47.0 → arrayview-0.47.3}/tests/capture_v_animation.py +0 -0
  99. {arrayview-0.47.0 → arrayview-0.47.3}/tests/make_vectorfield_test_arrays.py +0 -0
  100. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_1d_arrays.py +0 -0
  101. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_backend_shared.py +0 -0
  102. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_cli.py +0 -0
  103. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_config.py +0 -0
  104. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_dicom.py +0 -0
  105. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_drop_import.py +0 -0
  106. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_instance_registry.py +0 -0
  107. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_io.py +0 -0
  108. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_large_arrays.py +0 -0
  109. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_mode_roundtrip.py +0 -0
  110. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_nifti_series.py +0 -0
  111. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_rgb_pixel_art.py +0 -0
  112. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_safe_server_stop.py +0 -0
  113. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_source_safety.py +0 -0
  114. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_torch.py +0 -0
  115. {arrayview-0.47.0 → arrayview-0.47.3}/tests/test_tutorial.py +0 -0
  116. {arrayview-0.47.0 → arrayview-0.47.3}/tests/ui_audit.py +0 -0
  117. {arrayview-0.47.0 → arrayview-0.47.3}/tests/v_anim_frames/.gitkeep +0 -0
  118. {arrayview-0.47.0 → arrayview-0.47.3}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arrayview
3
- Version: 0.47.0
3
+ Version: 0.47.3
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.47.0"
7
+ version = "0.47.3"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -1863,3 +1863,44 @@ def _peek_file_shape(fpath: str, ext: str):
1863
1863
  except Exception:
1864
1864
  pass
1865
1865
  return None
1866
+
1867
+
1868
+ def nifti_grid_meta(filepath):
1869
+ """Return a NIfTI file's native-order spatial metadata (header only), or None."""
1870
+ if not (filepath.endswith(".nii") or filepath.endswith(".nii.gz")):
1871
+ return None
1872
+ return _nifti_header_with_meta(filepath, native=True)[1]
1873
+
1874
+
1875
+ def align_overlay_to_base(ov_path, ov_data, base_meta):
1876
+ """Resample a NIfTI overlay onto the base volume's voxel grid.
1877
+
1878
+ Only used when the overlay's spatial shape differs from the base, which
1879
+ otherwise leaves the overlay undisplayable. Both files' affines place the
1880
+ voxels in world space; label data uses nearest-neighbour so mask values
1881
+ stay exact. Returns ``ov_data`` unchanged when alignment does not apply.
1882
+ """
1883
+ if not base_meta or not (ov_path.endswith(".nii") or ov_path.endswith(".nii.gz")):
1884
+ return ov_data
1885
+ base_shape = base_meta.get("spatial_shape")
1886
+ base_affine = base_meta.get("data_affine")
1887
+ if base_shape is None or base_affine is None:
1888
+ return ov_data
1889
+ ov_shape = tuple(int(v) for v in ov_data.shape)
1890
+ if len(ov_shape) != 3 or ov_shape == tuple(base_shape):
1891
+ return ov_data
1892
+ from scipy.ndimage import affine_transform
1893
+
1894
+ ov_affine = np.asarray(_nib().load(ov_path).affine, dtype=np.float64)
1895
+ base_to_ov = np.linalg.inv(ov_affine) @ np.asarray(base_affine, dtype=np.float64)
1896
+ src = np.asarray(ov_data)
1897
+ is_label = np.issubdtype(src.dtype, np.integer) or src.dtype == bool
1898
+ return affine_transform(
1899
+ src.astype(np.float32) if not is_label else src,
1900
+ base_to_ov[:3, :3],
1901
+ offset=base_to_ov[:3, 3],
1902
+ output_shape=tuple(base_shape),
1903
+ order=0 if is_label else 1,
1904
+ mode="constant",
1905
+ cval=0,
1906
+ )
@@ -1314,6 +1314,7 @@ def _load_session_from_filepath(
1314
1314
  watch: bool = False,
1315
1315
  initial_view: dict | None = None,
1316
1316
  code_source: dict | None = None,
1317
+ align_to: str | None = None,
1317
1318
  ) -> dict:
1318
1319
  requested_sid = uuid.uuid4().hex
1319
1320
  payload = {
@@ -1329,6 +1330,8 @@ def _load_session_from_filepath(
1329
1330
  }
1330
1331
  if source_staging_dir:
1331
1332
  payload["source_staging_dir"] = source_staging_dir
1333
+ if align_to:
1334
+ payload["align_to"] = align_to
1332
1335
  if watch:
1333
1336
  payload["watch"] = True
1334
1337
  if initial_view:
@@ -1686,6 +1689,7 @@ def _register_cli_session_with_existing_server_impl(
1686
1689
  ov_name,
1687
1690
  expected_server_id=expected_server_id,
1688
1691
  source_staging_dir=(source_staging_dirs or {}).get(ov_path),
1692
+ align_to=os.path.abspath(base_file),
1689
1693
  )
1690
1694
  if "error" in ov_result:
1691
1695
  raise RuntimeError(
@@ -4803,6 +4807,7 @@ def _serve_daemon(
4803
4807
  array_choice_placeholder,
4804
4808
  load_data,
4805
4809
  load_data_with_meta,
4810
+ align_overlay_to_base,
4806
4811
  load_dir_collection,
4807
4812
  list_array_keys,
4808
4813
  )
@@ -4928,6 +4933,7 @@ def _serve_daemon(
4928
4933
  ):
4929
4934
  try:
4930
4935
  ov_data = load_data(ov_path)
4936
+ ov_data = align_overlay_to_base(ov_path, ov_data, spatial_meta)
4931
4937
  ov_session = _session_mod.Session(
4932
4938
  ov_data, filepath=ov_path, name=ov_name
4933
4939
  )
@@ -277,6 +277,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
277
277
  and os.path.abspath(existing.filepath) == abs_path
278
278
  and current_signature is not None
279
279
  and getattr(existing, "file_signature", None) == current_signature
280
+ and getattr(existing, "aligned_to", None) == body.get("align_to")
280
281
  ):
281
282
  if acquire_session_leases([existing.sid]):
282
283
  return {"sid": existing.sid, "name": existing.name, "notified": False}
@@ -458,6 +459,17 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
458
459
  key=_key,
459
460
  select=body.get("select"),
460
461
  )
462
+ if body.get("align_to"):
463
+ from ._io import align_overlay_to_base, nifti_grid_meta
464
+
465
+ base_meta = await asyncio.to_thread(
466
+ nifti_grid_meta, str(body["align_to"])
467
+ )
468
+ aligned = await asyncio.to_thread(
469
+ align_overlay_to_base, filepath, data, base_meta
470
+ )
471
+ if aligned is not data:
472
+ data, spatial_meta = aligned, base_meta
461
473
  except Exception as e:
462
474
  if staging_dir:
463
475
  cleanup_staging_directory(staging_dir)
@@ -479,6 +491,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
479
491
  str(value) for value in body.get("related_sids", [])
480
492
  ]
481
493
  apply_launch_view(session, body)
494
+ session.aligned_to = body.get("align_to")
482
495
  if staging_dir:
483
496
  session._source_staging_dirs = [str(staging_dir)]
484
497
  if not dir_patterns:
@@ -37,7 +37,7 @@ def _response_payload() -> dict:
37
37
  if not native_available:
38
38
  defaults["window"]["default"] = "browser"
39
39
  defaults["window"]["terminal"] = "browser"
40
- for key in ("default", "terminal", "vscode", "jupyter", "julia"):
40
+ for key in ("terminal", "vscode", "jupyter", "julia"):
41
41
  schema["window"][key] = [
42
42
  value for value in schema["window"][key] if value != "native"
43
43
  ]
@@ -765,6 +765,12 @@ def upgrade_memmap_in_background(session) -> bool:
765
765
  from ._io import _nifti_display_array
766
766
 
767
767
  loaded = _nifti_display_array(data)
768
+ # An uncompressed proxy whose dtype needs no conversion comes
769
+ # back as a view of the file map, so nothing was read yet and
770
+ # every new orientation would still fault from disk. Copy in
771
+ # file order to make it one sequential read.
772
+ if not loaded.flags.owndata:
773
+ loaded = np.array(loaded, copy=True, order="K")
768
774
  except Exception:
769
775
  return # keep serving from the lazy source
770
776
  # A /reload or a released session must not be clobbered by a read that
@@ -7086,6 +7086,7 @@
7086
7086
  let _mvPrevZoom = 1.0;
7087
7087
  let _cmvPrevSquareStretch = false;
7088
7088
  let _qmriPrevZoom = 1.0; // saved on enterQmri, restored on exitQmri (mirrors _mvPrevZoom)
7089
+ let _qmriPrevActiveDim = null; // scroll dim before qMRI moved it off the map dim; null while rebuilding keeps the original
7089
7090
  let _comparePrevZoom = 1.0; // saved on enterCompareMode, restored on exitCompareMode
7090
7091
  let _comparePrevNormalView = null; // Phase 4: saved primary View, restored on exitCompareMode
7091
7092
  let _eggsVisible = true;
@@ -35472,6 +35473,7 @@
35472
35473
  if (multiViewActive) exitMultiView();
35473
35474
  if (stackMosaicActive) exitStackMosaic();
35474
35475
  _qmriPrevZoom = userZoom; // save prior zoom so exitQmri can restore (mirrors enterMultiView)
35476
+ if (_qmriPrevActiveDim === null) _qmriPrevActiveDim = activeDim;
35475
35477
  userZoom = 1.0;
35476
35478
  qvPan.reset();
35477
35479
  // Opening straight into qMRI never draws the main canvas, so its
@@ -35994,6 +35996,10 @@
35994
35996
  wrap.innerHTML = '';
35995
35997
  wrap.className = '';
35996
35998
  userZoom = _qmriPrevZoom; // restore prior zoom (mirrors exitMultiView line 10521)
35999
+ if (!_qmriSyntheticRebuilding && _qmriPrevActiveDim !== null) {
36000
+ if (_qmriPrevActiveDim < shape.length) activeDim = _qmriPrevActiveDim;
36001
+ _qmriPrevActiveDim = null;
36002
+ }
35997
36003
  if (!_qmriSyntheticRebuilding) updateView();
35998
36004
  _reconcileUI();
35999
36005
  renderInfo();
@@ -58,7 +58,7 @@ _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
58
58
  _VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
59
59
  _VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
60
60
  _VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
61
- _VSCODE_EXT_VERSION = "0.15.62" # current bundled extension version
61
+ _VSCODE_EXT_VERSION = "0.15.63" # current bundled extension version
62
62
  _VSCODE_CONFIGURED_PORTS: set[int] = set()
63
63
  # Version-skew notices already printed, so a long-lived process (Jupyter, a
64
64
  # script opening several arrays) states the mismatch once instead of per call.
@@ -34,6 +34,21 @@ def isolated_runtime_directory(tmp_path_factory, monkeypatch):
34
34
  )
35
35
 
36
36
 
37
+ @pytest.fixture(autouse=True)
38
+ def no_leaked_fake_sessions():
39
+ """Drop anything a test put in the shared session table that isn't a session.
40
+
41
+ The test server is shared by the whole run, so one stray placeholder makes
42
+ every later load in every later file fail.
43
+ """
44
+ from arrayview._session import SESSIONS, Session
45
+
46
+ yield
47
+ for sid, value in list(SESSIONS.items()):
48
+ if not isinstance(value, Session):
49
+ SESSIONS.pop(sid, None)
50
+
51
+
37
52
  # ---------------------------------------------------------------------------
38
53
  # Server
39
54
  # ---------------------------------------------------------------------------