arrayview 0.17.0__tar.gz → 0.18.0__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.
- {arrayview-0.17.0 → arrayview-0.18.0}/PKG-INFO +1 -1
- {arrayview-0.17.0 → arrayview-0.18.0}/pyproject.toml +1 -1
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_server.py +51 -16
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_stdio_server.py +56 -12
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_viewer.html +587 -404
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_browser.py +7 -10
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_interactions.py +167 -63
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_mode_roundtrip.py +5 -3
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/visual_smoke.py +29 -22
- {arrayview-0.17.0 → arrayview-0.18.0}/uv.lock +1 -1
- {arrayview-0.17.0 → arrayview-0.18.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.gitignore +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.ignore +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/ROUTER.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/SETUP.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/SYNC.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/architecture.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/project-state.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/setup.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/context/stack.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.opencode/opencode.json +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.python-version +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/.vscode/settings.json +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/IMMERSIVE_ANIMATION.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/LICENSE +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/README.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/comparing.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/configuration.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/display.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/index.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/loading.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/logo.png +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/measurement.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/remote.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/docs/viewing.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/matlab/arrayview.m +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/mkdocs.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/scripts/demo.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/scripts/release.sh +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/conftest.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_api.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_cli.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_config.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_torch.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/tests/ui_audit.py +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.17.0 → arrayview-0.18.0}/vscode-extension/package.json +0 -0
|
@@ -2130,21 +2130,24 @@ def get_volume_histogram(
|
|
|
2130
2130
|
sid: str,
|
|
2131
2131
|
dim_x: int,
|
|
2132
2132
|
dim_y: int,
|
|
2133
|
-
scroll_dim: int,
|
|
2133
|
+
scroll_dim: int = -1,
|
|
2134
|
+
scroll_dims: str = "",
|
|
2134
2135
|
fixed_indices: str = "",
|
|
2135
2136
|
complex_mode: int = 0,
|
|
2136
2137
|
bins: int = 64,
|
|
2137
2138
|
session: "Session" = Depends(get_session_or_404),
|
|
2138
2139
|
):
|
|
2139
|
-
"""Return a histogram sampled across
|
|
2140
|
+
"""Return a histogram sampled across one or more aggregation dims.
|
|
2140
2141
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2142
|
+
*scroll_dims* is a comma-separated list of dim indices to aggregate
|
|
2143
|
+
over (in addition to *dim_x* / *dim_y*). When unset, falls back to
|
|
2144
|
+
the legacy single *scroll_dim* param. The sampler enumerates index
|
|
2145
|
+
combinations across all aggregation dims but caps total samples at
|
|
2146
|
+
~16 via stride-subsampling.
|
|
2144
2147
|
|
|
2145
2148
|
*fixed_indices* is a comma-separated list of ``dim:idx`` pairs that
|
|
2146
|
-
pin non-display, non-
|
|
2147
|
-
first parameter map in qMRI mode).
|
|
2149
|
+
pin non-display, non-aggregation dimensions (e.g. ``"3:0"`` to
|
|
2150
|
+
select the first parameter map in qMRI mode).
|
|
2148
2151
|
"""
|
|
2149
2152
|
|
|
2150
2153
|
# Parse fixed indices
|
|
@@ -2155,27 +2158,59 @@ def get_volume_histogram(
|
|
|
2155
2158
|
d, v = pair.split(":", 1)
|
|
2156
2159
|
fixed[int(d)] = int(v)
|
|
2157
2160
|
|
|
2161
|
+
# Resolve aggregation dims: prefer scroll_dims, fall back to scroll_dim.
|
|
2162
|
+
agg_dims: list[int] = []
|
|
2163
|
+
if scroll_dims:
|
|
2164
|
+
for tok in scroll_dims.split(","):
|
|
2165
|
+
tok = tok.strip()
|
|
2166
|
+
if not tok:
|
|
2167
|
+
continue
|
|
2168
|
+
try:
|
|
2169
|
+
d = int(tok)
|
|
2170
|
+
except ValueError:
|
|
2171
|
+
continue
|
|
2172
|
+
if 0 <= d < len(session.shape) and d != dim_x and d != dim_y and d not in agg_dims:
|
|
2173
|
+
agg_dims.append(d)
|
|
2174
|
+
if not agg_dims and scroll_dim >= 0 and scroll_dim != dim_x and scroll_dim != dim_y:
|
|
2175
|
+
agg_dims = [scroll_dim]
|
|
2176
|
+
|
|
2158
2177
|
# Check cache
|
|
2159
|
-
cache_key = (dim_x, dim_y,
|
|
2178
|
+
cache_key = (dim_x, dim_y, tuple(agg_dims), tuple(sorted(fixed.items())), complex_mode)
|
|
2160
2179
|
if not hasattr(session, "_volume_hist_cache"):
|
|
2161
2180
|
session._volume_hist_cache = {}
|
|
2162
2181
|
cached = session._volume_hist_cache.get(cache_key)
|
|
2163
2182
|
if cached is not None and cached.get("_data_version") == session.data_version:
|
|
2164
2183
|
return cached["result"]
|
|
2165
2184
|
|
|
2166
|
-
#
|
|
2167
|
-
|
|
2185
|
+
# Build sample index lists per aggregation dim, stride-subsampled so
|
|
2186
|
+
# their Cartesian product stays around ~16 samples total.
|
|
2168
2187
|
max_samples = 16
|
|
2169
|
-
if
|
|
2170
|
-
|
|
2188
|
+
if not agg_dims:
|
|
2189
|
+
# No aggregation dims — use the current middle slice only.
|
|
2190
|
+
sample_combos = [tuple()]
|
|
2171
2191
|
else:
|
|
2172
|
-
|
|
2173
|
-
|
|
2192
|
+
per_dim_counts = [session.shape[d] for d in agg_dims]
|
|
2193
|
+
# Target roughly equal sample count per dim so the product ≈ max_samples.
|
|
2194
|
+
# Ceil to avoid zero; clamp to dim size.
|
|
2195
|
+
k = len(agg_dims)
|
|
2196
|
+
per_dim_target = max(1, int(round(max_samples ** (1.0 / k))))
|
|
2197
|
+
sample_per_dim: list[list[int]] = []
|
|
2198
|
+
for n in per_dim_counts:
|
|
2199
|
+
m = min(n, per_dim_target)
|
|
2200
|
+
if n <= m:
|
|
2201
|
+
sample_per_dim.append(list(range(n)))
|
|
2202
|
+
else:
|
|
2203
|
+
step = n / m
|
|
2204
|
+
sample_per_dim.append([int(i * step) for i in range(m)])
|
|
2205
|
+
# Cartesian product with a hard cap.
|
|
2206
|
+
import itertools as _it
|
|
2207
|
+
sample_combos = list(_it.islice(_it.product(*sample_per_dim), max_samples))
|
|
2174
2208
|
|
|
2175
2209
|
pixels = []
|
|
2176
|
-
for
|
|
2210
|
+
for combo in sample_combos:
|
|
2177
2211
|
idx_list = [s // 2 for s in session.shape]
|
|
2178
|
-
|
|
2212
|
+
for d, si in zip(agg_dims, combo):
|
|
2213
|
+
idx_list[d] = si
|
|
2179
2214
|
for d, v in fixed.items():
|
|
2180
2215
|
idx_list[d] = v
|
|
2181
2216
|
raw = extract_slice(session, dim_x, dim_y, idx_list)
|
|
@@ -12,6 +12,7 @@ The binary payload has the same format as the WebSocket binary response for
|
|
|
12
12
|
slice requests, and is length-prefixed JSON for metadata/register/sessions.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
import concurrent.futures as _futures
|
|
15
16
|
import json
|
|
16
17
|
import io
|
|
17
18
|
import arrayview._session as _session_mod
|
|
@@ -798,13 +799,35 @@ def _handle_fetch_proxy(msg: dict) -> None:
|
|
|
798
799
|
_write_error(f"unsupported endpoint: {endpoint}")
|
|
799
800
|
|
|
800
801
|
|
|
801
|
-
|
|
802
|
-
|
|
802
|
+
# Slice render pool: renders multi-pane modes (qMRI, multiview, compare) in
|
|
803
|
+
# parallel instead of serializing on the stdin read loop. Responses are still
|
|
804
|
+
# written to stdout in arrival order by the caller. Per-cache locks on the
|
|
805
|
+
# Session protect raw/rgba/mosaic caches from concurrent access.
|
|
806
|
+
_SLICE_POOL: _futures.ThreadPoolExecutor | None = None
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _get_slice_pool() -> _futures.ThreadPoolExecutor:
|
|
810
|
+
global _SLICE_POOL
|
|
811
|
+
if _SLICE_POOL is None:
|
|
812
|
+
from arrayview._session import _RENDER_WORKERS
|
|
813
|
+
_SLICE_POOL = _futures.ThreadPoolExecutor(
|
|
814
|
+
max_workers=_RENDER_WORKERS,
|
|
815
|
+
thread_name_prefix="arrayview-stdio-render",
|
|
816
|
+
)
|
|
817
|
+
return _SLICE_POOL
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _build_slice_payload(msg: dict) -> bytes:
|
|
821
|
+
"""Render a slice and return its binary payload.
|
|
822
|
+
|
|
823
|
+
Raises on missing session / bad input; does NOT write to stdout so this is
|
|
824
|
+
safe to call from a thread pool. The caller is responsible for writing
|
|
825
|
+
the returned bytes (or the error) in arrival order.
|
|
826
|
+
"""
|
|
803
827
|
sid = msg["sid"]
|
|
804
828
|
session = SESSIONS.get(sid)
|
|
805
829
|
if not session:
|
|
806
|
-
|
|
807
|
-
return
|
|
830
|
+
raise RuntimeError("session not found")
|
|
808
831
|
|
|
809
832
|
seq = int(msg.get("seq", 0))
|
|
810
833
|
dim_x = int(msg["dim_x"])
|
|
@@ -908,7 +931,12 @@ def _handle_slice(msg: dict) -> None:
|
|
|
908
931
|
).tobytes()
|
|
909
932
|
payload += vf_hdr + vf_scale + arrows.tobytes()
|
|
910
933
|
|
|
911
|
-
|
|
934
|
+
return payload
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _handle_slice(msg: dict) -> None:
|
|
938
|
+
"""Render a slice and write the binary response (single-slice path)."""
|
|
939
|
+
_write_response(_build_slice_payload(msg))
|
|
912
940
|
|
|
913
941
|
|
|
914
942
|
def _handle_get_viewer_html(msg: dict) -> None:
|
|
@@ -1085,17 +1113,33 @@ def run_stdio_server() -> None:
|
|
|
1085
1113
|
pane_key = (s.get("dim_x", -1), s.get("dim_y", -1))
|
|
1086
1114
|
latest_for_pane[pane_key] = i
|
|
1087
1115
|
|
|
1088
|
-
#
|
|
1116
|
+
# Submit non-stale renders to the thread pool so multi-pane modes
|
|
1117
|
+
# (qMRI, multiview, compare) don't serialize on the read loop.
|
|
1118
|
+
# Per-cache locks on the Session make concurrent renders safe.
|
|
1119
|
+
pool = _get_slice_pool() if len(pending_slices) > 1 else None
|
|
1120
|
+
payload_futures: dict[int, _futures.Future] = {}
|
|
1121
|
+
for i, s in enumerate(pending_slices):
|
|
1122
|
+
pane_key = (s.get("dim_x", -1), s.get("dim_y", -1))
|
|
1123
|
+
if latest_for_pane[pane_key] != i:
|
|
1124
|
+
continue
|
|
1125
|
+
payload_futures[i] = (
|
|
1126
|
+
pool.submit(_build_slice_payload, s) if pool is not None else None
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
# Write responses in arrival order to match the extension's FIFO
|
|
1130
|
+
# callback matching.
|
|
1089
1131
|
for i, s in enumerate(pending_slices):
|
|
1090
1132
|
pane_key = (s.get("dim_x", -1), s.get("dim_y", -1))
|
|
1091
1133
|
if latest_for_pane[pane_key] != i:
|
|
1092
1134
|
_write_skip_response(s)
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1135
|
+
continue
|
|
1136
|
+
try:
|
|
1137
|
+
fut = payload_futures.get(i)
|
|
1138
|
+
payload = fut.result() if fut is not None else _build_slice_payload(s)
|
|
1139
|
+
_write_response(payload)
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
traceback.print_exc(file=sys.stderr)
|
|
1142
|
+
_write_error(str(e))
|
|
1099
1143
|
|
|
1100
1144
|
for def_type, def_msg in deferred:
|
|
1101
1145
|
try:
|