Skip to content

Commit

Permalink
Merge pull request #88 from Carifio24/utils-tests
Browse files Browse the repository at this point in the history
Add tests of utility functions
  • Loading branch information
Carifio24 authored Nov 19, 2024
2 parents ecfa6c5 + 3abcf5d commit 97adb39
Show file tree
Hide file tree
Showing 2 changed files with 363 additions and 3 deletions.
360 changes: 360 additions & 0 deletions glue_ar/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
from itertools import product
from numpy import arange, array, array_equal, nan, ones
import pytest

from glue.core import Data
from glue.viewers.common.viewer import LayerArtist
from glue_vispy_viewers.volume.volume_viewer import Vispy3DVolumeViewerState

from glue_ar.utils import alpha_composite, binned_opacity, clamp, clamped_opacity, \
clip_linear_transformations, clip_sides, data_count, data_for_layer, \
export_label_for_layer, get_resolution, hex_to_components, is_volume_viewer, \
iterable_has_nan, iterator_count, layer_color, mask_for_bounds, ndarray_has_nan, \
offset_triangles, slope_intercept_between, unique_id, xyz_bounds


def package_installed(package):
from importlib.util import find_spec
return find_spec(package) is not None


GLUE_QT_INSTALLED = package_installed("glue_qt")
GLUE_JUPYTER_INSTALLED = package_installed("glue_jupyter")


try:
from glue_qt.app import GlueApplication
from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer
from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer
except ImportError:
pass

try:
from glue_jupyter.app import JupyterApplication
from glue_jupyter.ipyvolume import IpyvolumeScatterView, IpyvolumeVolumeView
from glue_vispy_viewers.scatter.jupyter.scatter_viewer import JupyterVispyScatterViewer
from glue_vispy_viewers.volume.jupyter.volume_viewer import JupyterVispyVolumeViewer
except ImportError:
pass


def test_data_count():
data1 = Data(label="Data 1")
data2 = Data(label="Data 2")
viewer_state = Vispy3DVolumeViewerState()

layer1 = LayerArtist(viewer_state, layer=data1)
layer1_2 = LayerArtist(viewer_state, layer=data1)
layer2 = LayerArtist(viewer_state, layer=data2)

assert data_count((layer1,)) == 1
assert data_count((layer1, layer1_2)) == 1
assert data_count((layer1, layer2)) == 2

subset = data1.new_subset()
subset_layer = LayerArtist(viewer_state, layer=subset)

assert data_count((subset_layer,)) == 1
assert data_count((layer1, subset_layer)) == 1
assert data_count((layer2, subset_layer)) == 2


def test_export_label_for_layer():
data = Data(label="Data")
subset = data.new_subset(label="Subset")
viewer_state = Vispy3DVolumeViewerState()
data_layer = LayerArtist(viewer_state, layer=data)
subset_layer = LayerArtist(viewer_state, layer=subset)

assert export_label_for_layer(data_layer, add_data_label=True) == "Data"
assert export_label_for_layer(data_layer, add_data_label=False) == "Data"

assert export_label_for_layer(subset_layer, add_data_label=True) == "Subset (Data)"
assert export_label_for_layer(subset_layer, add_data_label=False) == "Subset"


def test_slope_intercept_between():
assert slope_intercept_between((3, 3), (1, 1)) == (1, 0)
assert slope_intercept_between((3, 4), (1, 2)) == (1, 1)
assert slope_intercept_between((-1, 5), (1, 5)) == (0, 5)
assert slope_intercept_between((-1, 5), (1, 15)) == (5, 10)


def test_clip_linear_transformations():
bounds = [(0, 2), (0, 8), (2, 6)]

assert clip_linear_transformations(bounds) == [
(0.25, -0.25),
(0.25, -1),
(0.25, -1)
]

assert clip_linear_transformations(bounds, clip_size=2) == [
(0.5, -0.5),
(0.5, -2),
(0.5, -2)
]

assert clip_linear_transformations(bounds, stretches=(4, 0.5, 0.25)) == [
(1, -1),
(0.125, -0.5),
(0.0625, -0.25)
]

assert clip_linear_transformations(bounds, clip_size=4,
stretches=(4, 0.5, 0.25)) == [
(4, -4),
(0.5, -2),
(0.25, -1)
]


def test_layer_color():
data = Data(label="Data")
viewer_state = Vispy3DVolumeViewerState()
layer = LayerArtist(viewer_state, layer=data)
layer.state.color = "#abcdef"

assert layer_color(layer.state) == "#abcdef"

layer.state.color = "0.35"
assert layer_color(layer.state) == "#808080"

layer.state.color = "0.75"
assert layer_color(layer.state) == "#808080"


def test_clip_sides_non_native():
viewer_state = Vispy3DVolumeViewerState()
viewer_state.native_aspect = False

viewer_state.x_min = 0
viewer_state.x_max = 8
viewer_state.y_min = -2
viewer_state.y_max = 2
viewer_state.z_min = -1
viewer_state.z_max = 1

resolutions = (32, 64, 128, 256, 512)
clip_sizes = (1, 2, 3, 5, 10)
for resolution, clip_size in product(resolutions, clip_sizes):
viewer_state.resolution = resolution
size = 2 * clip_size / resolution
assert clip_sides(viewer_state, clip_size=clip_size) == (size, size, size)


def test_clip_sides_native():
viewer_state = Vispy3DVolumeViewerState()
viewer_state.native_aspect = True

viewer_state.x_min = 0
viewer_state.x_max = 8
viewer_state.y_min = -2
viewer_state.y_max = 2
viewer_state.z_min = -1
viewer_state.z_max = 1

resolutions = (32, 64, 128, 256, 512)
clip_sizes = (1, 2, 3, 5, 10)
for resolution, clip_size in product(resolutions, clip_sizes):
viewer_state.resolution = resolution
max_size = 2 * clip_size / resolution
assert clip_sides(viewer_state, clip_size=clip_size) == (max_size, max_size / 2, max_size / 4)


def test_mask_for_bounds():
x_values = range(10, 25)
y_values = range(130, 145)
z_values = range(-50, -35)
data = Data(x=x_values, y=y_values, z=z_values)
try:
app = GlueApplication()
app.add_data(data)
viewer = app.new_data_viewer(VispyScatterViewer, data=data)
except NameError:
app = JupyterApplication()
app.add_data(data)
viewer = app.new_data_viewer(JupyterVispyScatterViewer, data=data)
viewer.state.x_att = data.id['x']
viewer.state.y_att = data.id['y']
viewer.state.z_att = data.id['z']
viewer.add_data(data)
layer_state = viewer.layers[0].state

viewer.state.x_min = 12 # Cuts off the first two points
viewer.state.x_max = 25
viewer.state.y_min = 130
viewer.state.y_max = 141 # Cuts off the last three points
viewer.state.z_min = -70
viewer.state.z_max = -30

bounds = xyz_bounds(viewer.state, with_resolution=False)
mask = array([i not in (0, 1, 12, 13, 14) for i in range(15)])
assert array_equal(mask_for_bounds(viewer.state, layer_state, bounds), mask)


def test_hex_to_components():
assert hex_to_components("#7f11e0") == [127, 17, 224]
assert hex_to_components("#abcdef47") == [171, 205, 239, 71]
assert hex_to_components("#000000") == [0, 0, 0]
assert hex_to_components("#00000000") == [0, 0, 0, 0]
assert hex_to_components("#26e04a") == [38, 224, 74]
assert hex_to_components("#ff021706") == [255, 2, 23, 6]


def test_unique_id():
ids = [unique_id() for _ in range(25)]
assert all(len(id) == 32 for id in ids)
assert len(set(ids)) == 25


def test_alpha_composite():
over = [110, 206, 15, 0.3]
under = [89, 97, 202, 0.4]
alpha_combined = 0.3 + 0.4 * 0.7
rgb_new = [(o * 0.3 + u * 0.4 * 0.7) / alpha_combined for o, u in zip(over[:3], under[:3])]
assert alpha_composite(over, under) == rgb_new + [alpha_combined]

over = [110, 206, 15, 0.6]
under = [89, 97, 202, 0.7]
alpha_combined = 0.6 + 0.7 * 0.4
rgb_new = [(o * 0.6 + u * 0.7 * 0.4) / alpha_combined for o, u in zip(over[:3], under[:3])]
assert alpha_composite(over, under) == rgb_new + [alpha_combined]

# Here over has full opacity, so the composition should just be the over color
over = [255, 10.5, 176]
under = [12, 116, 175, 0.5]
assert alpha_composite(over, under) == over + [1]


def test_data_for_layer():
data = Data(label="Data")
subset = data.new_subset(label="Subset")
viewer_state = Vispy3DVolumeViewerState()
data_layer = LayerArtist(viewer_state, layer=data)
subset_layer = LayerArtist(viewer_state, layer=subset)

assert data_for_layer(data_layer) == data
assert data_for_layer(subset_layer) == data


def test_ndarray_has_nan():
assert ndarray_has_nan(array([3.0, nan, -4.7, 2, nan]))
assert not ndarray_has_nan(array([3.0, 2.6, -4.7, 2, -10.5]))


def test_iterable_has_nan():
assert iterable_has_nan((nan, 2.7, 3.5))
assert iterable_has_nan([2.1, nan, 11.0, 2.6])
assert not iterable_has_nan([2.1, -3.5, 4.6])
assert not iterable_has_nan((2.2, 4.6, -0.7))


def test_iterator_count():
assert iterator_count(iter([1, 2, 3, 4, 5])) == 5
assert iterator_count(iter(range(11))) == 11


@pytest.mark.skipif(not GLUE_QT_INSTALLED,
reason="Requires glue-qt to test Qt VisPy volume viewer")
def test_is_volume_viewer_qt():
qt_app = GlueApplication()
vispy_scatter = qt_app.new_data_viewer(VispyScatterViewer)
vispy_volume = qt_app.new_data_viewer(VispyVolumeViewer)
assert not is_volume_viewer(vispy_scatter)
assert is_volume_viewer(vispy_volume)


@pytest.mark.skipif(not GLUE_JUPYTER_INSTALLED,
reason="Requires glue-jupyter to test Jupyter VisPy and ipyvolume viewers")
def test_is_volume_viewer_jupyter():
jupyter_app = JupyterApplication()

vispy_scatter = jupyter_app.new_data_viewer(JupyterVispyScatterViewer)
vispy_volume = jupyter_app.new_data_viewer(JupyterVispyVolumeViewer)
assert not is_volume_viewer(vispy_scatter)
assert is_volume_viewer(vispy_volume)

ipv_scatter = jupyter_app.new_data_viewer(IpyvolumeScatterView)
ipv_volume = jupyter_app.new_data_viewer(IpyvolumeVolumeView)
assert not is_volume_viewer(ipv_scatter)
assert is_volume_viewer(ipv_volume)


@pytest.mark.skipif(not GLUE_QT_INSTALLED,
reason="Requires glue-qt to test Qt VisPy volume viewer")
def test_get_resolution_qt():
qt_app = GlueApplication()

vispy_volume = qt_app.new_data_viewer(VispyVolumeViewer)
vispy_volume.state.resolution = 64
assert get_resolution(vispy_volume.state) == 64

# Check default behavior
vispy_scatter = qt_app.new_data_viewer(VispyScatterViewer)
assert get_resolution(vispy_scatter) == 256


@pytest.mark.skipif(not GLUE_JUPYTER_INSTALLED,
reason="Requires glue-jupyter to test Jupyter VisPy and ipyvolume viewers")
def test_get_resolution_jupyter():
jupyter_app = JupyterApplication()
volume_data1 = Data(label='Volume Data',
x=arange(24).reshape((2, 3, 4)),
y=ones((2, 3, 4)),
z=arange(100, 124).reshape((2, 3, 4)))
jupyter_app.add_data(volume_data1)

vispy_volume = jupyter_app.new_data_viewer(JupyterVispyVolumeViewer)
vispy_volume.add_data(volume_data1)
vispy_volume.state.resolution = 32
assert get_resolution(vispy_volume.state) == 32

ipv_volume = jupyter_app.new_data_viewer(IpyvolumeVolumeView)
ipv_volume.add_data(volume_data1)
ipv_volume.layers[-1].state.max_resolution = 128
assert get_resolution(ipv_volume.state) == 128

volume_data2 = Data(label='Volume Data',
x=arange(24).reshape((2, 3, 4)),
y=ones((2, 3, 4)),
z=arange(100, 124).reshape((2, 3, 4)))
jupyter_app.add_data(volume_data2)
vispy_volume.add_data(volume_data2)
ipv_volume.add_data(volume_data2)
ipv_volume.layers[-1].state.max_resolution = 64
assert get_resolution(vispy_volume.state) == 32
assert get_resolution(ipv_volume.state) == 128

ipv_volume.layers[-1].state.max_resolution = 512
assert get_resolution(ipv_volume.state) == 512


def test_clamp():
assert clamp(2, 0, 1) == 1
assert clamp(-1, 0, 1) == 0
assert clamp(0.5, 0, 1) == 0.5
assert clamp(9, 5, 7) == 7
assert clamp(16.2, 20.5, 31.6) == 20.5
assert clamp(5.6, 4.8, 7.2) == 5.6


def test_clamped_opacity():
assert clamped_opacity(0.1) == 0.1
assert clamped_opacity(0.77) == 0.77
assert clamped_opacity(-2) == 0
assert clamped_opacity(1.6) == 1


def test_binned_opacity():
assert binned_opacity(0.13, 0.2) == 0.2
assert binned_opacity(0.3, 0.25) == 0.25
assert binned_opacity(-1.3, 0.1) == 0
assert binned_opacity(2.46, 0.01) == 1
assert binned_opacity(0.775, 0.02) == 0.78


def test_offset_triangles():
assert offset_triangles([[0, 1, 2], [1, 2, 3], [0, 2, 3]], 6) == [(6, 7, 8), (7, 8, 9), (6, 8, 9)]
assert offset_triangles([[2, 1, 6], [5, 7, 4]], 5) == [(7, 6, 11), (10, 12, 9)]
assert offset_triangles([[0, 1, 2], [2, 3, 0], [3, 1, 2]], 0) == [(0, 1, 2), (2, 3, 0), (3, 1, 2)]
6 changes: 3 additions & 3 deletions glue_ar/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def clip_sides(viewer_state: Viewer3DState,
return tuple(s * transform[0] for s, transform in zip(sides, clip_transforms))
else:
max_stretch = max(stretches)
return tuple(2 * stretch / (max_stretch * resolution) for stretch in stretches)
return tuple(2 * clip_size * stretch / (max_stretch * resolution) for stretch in stretches)


def bring_into_clip(data,
Expand Down Expand Up @@ -256,8 +256,8 @@ def unique_id() -> str:


def alpha_composite(over: List[float], under: List[float]) -> List[float]:
alpha_o = over[3] if len(over) == 4 else over[2]
alpha_u = under[3] if len(under) == 4 else under[2]
alpha_o = over[3] if len(over) == 4 else 1
alpha_u = under[3] if len(under) == 4 else 1
rgb_o = over[:3]
rgb_u = under[:3]
alpha_new = alpha_o + alpha_u * (1 - alpha_o)
Expand Down

0 comments on commit 97adb39

Please sign in to comment.