Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support slices like '3:10:mean(2)' #391

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tiled/_tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,17 @@ def test_array_interface():
# smoke test
v.chunks
v.dims


def test_slices():
client = from_tree(cube_tree)
ac = client["tiny_cube"]
ac[:]
ac[:, :, :]
ac[..., 0, :1]
ac[1:4, -5:]
ac[-100:4, :-3]
ac[0, 3, 8]
ac[0, 3, :]
with pytest.raises(IndexError):
ac[100]
66 changes: 51 additions & 15 deletions tiled/server/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import builtins
import collections
import re
from functools import lru_cache
from typing import Optional

Expand Down Expand Up @@ -116,20 +119,53 @@ def expected_shape(
return tuple(map(int, expected_shape.split(",")))


# Accept numpy-style mutlidimesional slices and special constructs 'a:b:mean'
# and 'a:b:mean(c)' to represent downsampling, inspired by
# https://uhi.readthedocs.io/en/latest/
# Note: if you need to debug this, the interactive tool at https://regex101.com/ is your friend!
DIM_REGEX = r"(?:(-?[0-9]+)|(?:([0-9]*|-[0-9]+):(?:([0-9]*|-[0-9]+))?(?::(mean|mean\([0-9]+\)|[0-9]*|-[0-9]+))?))"
SLICE_REGEX = rf"^{DIM_REGEX}(,{DIM_REGEX})*$"
DIM_PATTERN = re.compile(rf"^{DIM_REGEX}$")
MEAN_PATTERN = re.compile(r"(mean|mean\(([0-9]+)\))")


# This object is meant to be placed at slice.step and used by the consumer to
# detect that it should agggregate, using
# numpy.mean or skimage.transform.downscale_local_mean.
Mean = collections.namedtuple("Mean", ["parameter"])


def _int_or_none(s):
return int(s) if s else None


def _mean_int_or_none(s):
if s is None:
return None
m = MEAN_PATTERN.match(s)
if m.group(0):
return Mean(m.group(1))
return _int_or_none(s)


def slice_(
slice: str = Query(None, regex="^[-0-9,:]*$"),
slice: str = Query("", regex=SLICE_REGEX),
):
"Specify and parse a block index parameter."
import numpy

# IMPORTANT We are eval-ing a user-provider string here so we need to be
# very careful about locking down what can be in it. The regex above
# excludes any letters or operators, so it is not possible to execute
# functions or expensive arithmetic.
return tuple(
[
eval(f"numpy.s_[{dim!s}]", {"numpy": numpy})
for dim in (slice or "").split(",")
if dim
]
)
"Specify and parse a slice parameter."
slices = []
for dim in slice.split(","):
if dim:
match = DIM_PATTERN.match(dim)
# Group 1 matches an int, as in arr[i].
if match.group(1) is not None:
s = int(match.group(1))
else:
# Groups 2 through 4 match a slice, as in arr[i:j:k].
s = builtins.slice(
_int_or_none(match.group(2)),
_int_or_none(match.group(3)),
_mean_int_or_none(match.group(4)),
)
slices.append(s)
print("slices", slices)
return tuple(slices)