diff --git a/src/safeds/_config/__init__.py b/src/safeds/_config/__init__.py index 09d286a38..bd7b5188a 100644 --- a/src/safeds/_config/__init__.py +++ b/src/safeds/_config/__init__.py @@ -5,15 +5,17 @@ import apipkg if TYPE_CHECKING: - from ._device import _get_device + from ._device import _get_device, _init_default_device apipkg.initpkg( __name__, { "_get_device": "._device:_get_device", + "_init_default_device": "._device:_init_default_device", }, ) __all__ = [ "_get_device", + "_init_default_device", ] diff --git a/src/safeds/_config/_device.py b/src/safeds/_config/_device.py index 10c6ca8e7..3fc1db282 100644 --- a/src/safeds/_config/_device.py +++ b/src/safeds/_config/_device.py @@ -6,7 +6,29 @@ from torch.types import Device +_default_device: Device | None = None + + def _get_device() -> Device: import torch - return torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + return torch.get_default_device() + + +def _init_default_device() -> None: + import torch + + global _default_device + + if _default_device is None: + _default_device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + + torch.set_default_device(_default_device) + + +def _set_default_device(device: Device) -> None: + # This changes all future tensors, but not any tensor that already exists + global _default_device + + _default_device = device + _init_default_device() diff --git a/src/safeds/data/image/containers/_image.py b/src/safeds/data/image/containers/_image.py index 7a6a00830..867242d27 100644 --- a/src/safeds/data/image/containers/_image.py +++ b/src/safeds/data/image/containers/_image.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from safeds._config import _get_device +from safeds._config import _get_device, _init_default_device from safeds._utils import _structural_hash from safeds.data.image._utils._image_transformation_error_and_warning_checks import ( _check_add_noise_errors, @@ -24,7 +24,6 @@ if TYPE_CHECKING: from numpy import dtype, ndarray from torch import Tensor - from torch.types import Device class Image: @@ -43,6 +42,8 @@ class Image: def _filter_edges_kernel() -> Tensor: import torch + _init_default_device() + if Image._filter_edges_kernel_cache is None: Image._filter_edges_kernel_cache = ( torch.tensor([[-1.0, -1.0, -1.0], [-1.0, 8.0, -1.0], [-1.0, -1.0, -1.0]]) @@ -50,11 +51,13 @@ def _filter_edges_kernel() -> Tensor: .unsqueeze(dim=0) .to(_get_device()) ) + if Image._filter_edges_kernel_cache.device != _get_device(): + Image._filter_edges_kernel_cache = Image._filter_edges_kernel_cache.to(_get_device()) # pragma: no cover return Image._filter_edges_kernel_cache @staticmethod - def from_file(path: str | Path, device: Device = None) -> Image: + def from_file(path: str | Path) -> Image: """ Create an image from a file. @@ -62,8 +65,6 @@ def from_file(path: str | Path, device: Device = None) -> Image: ---------- path: The path to the image file. - device: - The device where the tensor will be saved on. Defaults to the default device Returns ------- @@ -78,13 +79,12 @@ def from_file(path: str | Path, device: Device = None) -> Image: from PIL.Image import open as pil_image_open from torchvision.transforms.functional import pil_to_tensor - if device is None: - device = _get_device() + _init_default_device() - return Image(image_tensor=pil_to_tensor(pil_image_open(path)), device=device) + return Image(image_tensor=pil_to_tensor(pil_image_open(path))) @staticmethod - def from_bytes(data: bytes, device: Device = None) -> Image: + def from_bytes(data: bytes) -> Image: """ Create an image from bytes. @@ -92,8 +92,6 @@ def from_bytes(data: bytes, device: Device = None) -> Image: ---------- data: The data of the image. - device: - The device where the tensor will be saved on. Defaults to the default device Returns ------- @@ -103,8 +101,7 @@ def from_bytes(data: bytes, device: Device = None) -> Image: import torch import torchvision - if device is None: - device = _get_device() + _init_default_device() with warnings.catch_warnings(): warnings.filterwarnings( @@ -113,13 +110,10 @@ def from_bytes(data: bytes, device: Device = None) -> Image: ) input_tensor = torch.frombuffer(data, dtype=torch.uint8) - return Image(image_tensor=torchvision.io.decode_image(input_tensor), device=device) - - def __init__(self, image_tensor: Tensor, device: Device = None) -> None: - if device is None: - device = _get_device() + return Image(image_tensor=torchvision.io.decode_image(input_tensor).to(_get_device())) - self._image_tensor: Tensor = image_tensor.to(device) + def __init__(self, image_tensor: Tensor) -> None: + self._image_tensor: Tensor = image_tensor def __eq__(self, other: object) -> bool: """ @@ -137,11 +131,13 @@ def __eq__(self, other: object) -> bool: """ import torch + _init_default_device() + if not isinstance(other, Image): return NotImplemented return (self is other) or ( self._image_tensor.size() == other._image_tensor.size() - and torch.all(torch.eq(self._image_tensor, other._set_device(self.device)._image_tensor)).item() + and torch.all(torch.eq(self._image_tensor, other._image_tensor)).item() ) def __hash__(self) -> int: @@ -200,6 +196,8 @@ def _repr_jpeg_(self) -> bytes | None: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + if self.channel == 4: return None buffer = io.BytesIO() @@ -223,6 +221,8 @@ def _repr_png_(self) -> bytes: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + buffer = io.BytesIO() if self.channel == 1: func2.to_pil_image(self._image_tensor, mode="L").save(buffer, format="png") @@ -231,17 +231,6 @@ def _repr_png_(self) -> bytes: buffer.seek(0) return buffer.read() - def _set_device(self, device: Device) -> Image: - """ - Set the device where the image will be saved on. - - Returns - ------- - result: - The image on the given device - """ - return Image(self._image_tensor, device) - # ------------------------------------------------------------------------------------------------------------------ # Properties # ------------------------------------------------------------------------------------------------------------------ @@ -294,18 +283,6 @@ def size(self) -> ImageSize: """ return ImageSize(self.width, self.height, self.channel) - @property - def device(self) -> Device: - """ - Get the device where the image is saved on. - - Returns - ------- - device: - The device of the image - """ - return self._image_tensor.device - # ------------------------------------------------------------------------------------------------------------------ # Conversion # ------------------------------------------------------------------------------------------------------------------ @@ -323,6 +300,8 @@ def to_jpeg_file(self, path: str | Path) -> None: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + if self.channel == 4: raise IllegalFormatError("png") Path(path).parent.mkdir(parents=True, exist_ok=True) @@ -344,6 +323,8 @@ def to_png_file(self, path: str | Path) -> None: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + Path(path).parent.mkdir(parents=True, exist_ok=True) if self.channel == 1: func2.to_pil_image(self._image_tensor, mode="L").save(path, format="png") @@ -377,6 +358,8 @@ def change_channel(self, channel: int) -> Image: """ import torch + _init_default_device() + if self.channel == channel: image_tensor = self._image_tensor elif self.channel == 1 and channel == 3: @@ -387,7 +370,7 @@ def change_channel(self, channel: int) -> Image: self._image_tensor, self._image_tensor, self._image_tensor, - torch.full(self._image_tensor.size(), 255).to(self.device), + torch.full(self._image_tensor.size(), 255), ], dim=0, ) @@ -395,14 +378,14 @@ def change_channel(self, channel: int) -> Image: image_tensor = self.convert_to_grayscale()._image_tensor[0:1] elif self.channel == 3 and channel == 4: image_tensor = torch.cat( - [self._image_tensor, torch.full(self._image_tensor[0:1].size(), 255).to(self.device)], + [self._image_tensor, torch.full(self._image_tensor[0:1].size(), 255)], dim=0, ) elif self.channel == 4 and channel == 3: image_tensor = self._image_tensor[0:3] else: raise ValueError(f"Channel {channel} is not a valid channel option. Use either 1, 3 or 4") - return Image(image_tensor, device=self._image_tensor.device) + return Image(image_tensor) def resize(self, new_width: int, new_height: int) -> Image: """ @@ -430,10 +413,11 @@ def resize(self, new_width: int, new_height: int) -> Image: from torchvision.transforms import InterpolationMode from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_resize_errors(new_width, new_height) return Image( func2.resize(self._image_tensor, size=[new_height, new_width], interpolation=InterpolationMode.NEAREST), - device=self._image_tensor.device, ) def convert_to_grayscale(self) -> Image: @@ -450,6 +434,8 @@ def convert_to_grayscale(self) -> Image: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + if self.channel == 4: return Image( torch.cat( @@ -458,12 +444,10 @@ def convert_to_grayscale(self) -> Image: self._image_tensor[3].unsqueeze(dim=0), ], ), - device=self.device, ) else: return Image( func2.rgb_to_grayscale(self._image_tensor[0:3], num_output_channels=self.channel), - device=self.device, ) def crop(self, x: int, y: int, width: int, height: int) -> Image: @@ -495,8 +479,10 @@ def crop(self, x: int, y: int, width: int, height: int) -> Image: """ from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_crop_errors_and_warnings(x, y, width, height, self.width, self.height, plural=False) - return Image(func2.crop(self._image_tensor, y, x, height, width), device=self.device) + return Image(func2.crop(self._image_tensor, y, x, height, width)) def flip_vertically(self) -> Image: """ @@ -511,7 +497,9 @@ def flip_vertically(self) -> Image: """ from torchvision.transforms.v2 import functional as func2 - return Image(func2.vertical_flip(self._image_tensor), device=self.device) + _init_default_device() + + return Image(func2.vertical_flip(self._image_tensor)) def flip_horizontally(self) -> Image: """ @@ -526,7 +514,9 @@ def flip_horizontally(self) -> Image: """ from torchvision.transforms.v2 import functional as func2 - return Image(func2.horizontal_flip(self._image_tensor), device=self.device) + _init_default_device() + + return Image(func2.horizontal_flip(self._image_tensor)) def adjust_brightness(self, factor: float) -> Image: """ @@ -556,6 +546,8 @@ def adjust_brightness(self, factor: float) -> Image: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_adjust_brightness_errors_and_warnings(factor, plural=False) if self.channel == 4: return Image( @@ -565,10 +557,9 @@ def adjust_brightness(self, factor: float) -> Image: self._image_tensor[3].unsqueeze(dim=0), ], ), - device=self.device, ) else: - return Image(func2.adjust_brightness(self._image_tensor, factor * 1.0), device=self.device) + return Image(func2.adjust_brightness(self._image_tensor, factor * 1.0)) def add_noise(self, standard_deviation: float) -> Image: """ @@ -593,10 +584,11 @@ def add_noise(self, standard_deviation: float) -> Image: """ import torch + _init_default_device() + _check_add_noise_errors(standard_deviation) return Image( - self._image_tensor + torch.normal(0, standard_deviation, self._image_tensor.size()).to(self.device) * 255, - device=self.device, + self._image_tensor + torch.normal(0, standard_deviation, self._image_tensor.size()).to(_get_device()) * 255, ) def adjust_contrast(self, factor: float) -> Image: @@ -626,6 +618,8 @@ def adjust_contrast(self, factor: float) -> Image: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_adjust_contrast_errors_and_warnings(factor, plural=False) if self.channel == 4: return Image( @@ -635,10 +629,9 @@ def adjust_contrast(self, factor: float) -> Image: self._image_tensor[3].unsqueeze(dim=0), ], ), - device=self.device, ) else: - return Image(func2.adjust_contrast(self._image_tensor, factor * 1.0), device=self.device) + return Image(func2.adjust_contrast(self._image_tensor, factor * 1.0)) def adjust_color_balance(self, factor: float) -> Image: """ @@ -667,7 +660,6 @@ def adjust_color_balance(self, factor: float) -> Image: _check_adjust_color_balance_errors_and_warnings(factor, self.channel, plural=False) return Image( self.convert_to_grayscale()._image_tensor * (1.0 - factor * 1.0) + self._image_tensor * (factor * 1.0), - device=self.device, ) def blur(self, radius: int) -> Image: @@ -694,8 +686,10 @@ def blur(self, radius: int) -> Image: """ from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_blur_errors_and_warnings(radius, min(self.width, self.height), plural=False) - return Image(func2.gaussian_blur(self._image_tensor, [radius * 2 + 1, radius * 2 + 1]), device=self.device) + return Image(func2.gaussian_blur(self._image_tensor, [radius * 2 + 1, radius * 2 + 1])) def sharpen(self, factor: float) -> Image: """ @@ -724,6 +718,8 @@ def sharpen(self, factor: float) -> Image: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_sharpen_errors_and_warnings(factor, plural=False) if self.channel == 4: return Image( @@ -733,10 +729,9 @@ def sharpen(self, factor: float) -> Image: self._image_tensor[3].unsqueeze(dim=0), ], ), - device=self.device, ) else: - return Image(func2.adjust_sharpness(self._image_tensor, factor * 1.0), device=self.device) + return Image(func2.adjust_sharpness(self._image_tensor, factor * 1.0)) def invert_colors(self) -> Image: """ @@ -752,13 +747,14 @@ def invert_colors(self) -> Image: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + if self.channel == 4: return Image( torch.cat([func2.invert(self._image_tensor[0:3]), self._image_tensor[3].unsqueeze(dim=0)]), - device=self.device, ) else: - return Image(func2.invert(self._image_tensor), device=self.device) + return Image(func2.invert(self._image_tensor)) def rotate_right(self) -> Image: """ @@ -773,7 +769,9 @@ def rotate_right(self) -> Image: """ from torchvision.transforms.v2 import functional as func2 - return Image(func2.rotate(self._image_tensor, -90, expand=True), device=self.device) + _init_default_device() + + return Image(func2.rotate(self._image_tensor, -90, expand=True)) def rotate_left(self) -> Image: """ @@ -788,7 +786,9 @@ def rotate_left(self) -> Image: """ from torchvision.transforms.v2 import functional as func2 - return Image(func2.rotate(self._image_tensor, 90, expand=True), device=self.device) + _init_default_device() + + return Image(func2.rotate(self._image_tensor, 90, expand=True)) def find_edges(self) -> Image: """ @@ -803,26 +803,22 @@ def find_edges(self) -> Image: """ import torch - kernel = ( - Image._filter_edges_kernel() - if self.device.type == _get_device() - else Image._filter_edges_kernel().to(self.device) - ) + _init_default_device() + edges_tensor = torch.clamp( torch.nn.functional.conv2d( self.convert_to_grayscale()._image_tensor.float()[0].unsqueeze(dim=0), - kernel, + Image._filter_edges_kernel(), padding="same", ).squeeze(dim=1), 0, 255, ).to(torch.uint8) if self.channel == 3: - return Image(edges_tensor.repeat(3, 1, 1), device=self.device) + return Image(edges_tensor.repeat(3, 1, 1)) elif self.channel == 4: return Image( torch.cat([edges_tensor.repeat(3, 1, 1), self._image_tensor[3].unsqueeze(dim=0)]), - device=self.device, ) else: - return Image(edges_tensor, device=self.device) + return Image(edges_tensor) diff --git a/src/safeds/data/image/containers/_image_list.py b/src/safeds/data/image/containers/_image_list.py index 9224a908a..fa10c5cac 100644 --- a/src/safeds/data/image/containers/_image_list.py +++ b/src/safeds/data/image/containers/_image_list.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Literal, overload +from safeds._config import _init_default_device from safeds.data.image.containers._image import Image if TYPE_CHECKING: @@ -132,6 +133,8 @@ def from_files( from PIL.Image import open as pil_image_open from torchvision.transforms.v2.functional import pil_to_tensor + _init_default_device() + from safeds.data.image.containers._empty_image_list import _EmptyImageList from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList @@ -266,6 +269,8 @@ def _repr_png_(self) -> bytes: import torch from torchvision.utils import make_grid, save_image + _init_default_device() + from safeds.data.image.containers._empty_image_list import _EmptyImageList if isinstance(self, _EmptyImageList): diff --git a/src/safeds/data/image/containers/_multi_size_image_list.py b/src/safeds/data/image/containers/_multi_size_image_list.py index 00cadf17f..1a1f89314 100644 --- a/src/safeds/data/image/containers/_multi_size_image_list.py +++ b/src/safeds/data/image/containers/_multi_size_image_list.py @@ -5,6 +5,7 @@ import sys from typing import TYPE_CHECKING +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.image._utils._image_transformation_error_and_warning_checks import ( _check_blur_errors_and_warnings, @@ -389,6 +390,8 @@ def _remove_image_by_index_ignore_invalid(self, index: int | list[int]) -> Image def remove_images_with_size(self, width: int, height: int) -> ImageList: import torch + _init_default_device() + _check_remove_images_with_size_errors(width, height) if (width, height) not in self._image_list_dict: return self @@ -460,6 +463,8 @@ def shuffle_images(self) -> ImageList: def resize(self, new_width: int, new_height: int) -> ImageList: import torch + _init_default_device() + from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList image_list_tensors = [] @@ -483,6 +488,8 @@ def convert_to_grayscale(self) -> ImageList: def crop(self, x: int, y: int, width: int, height: int) -> ImageList: import torch + _init_default_device() + from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList image_list_tensors = [] diff --git a/src/safeds/data/image/containers/_single_size_image_list.py b/src/safeds/data/image/containers/_single_size_image_list.py index 972f1edb5..a7b71fd7a 100644 --- a/src/safeds/data/image/containers/_single_size_image_list.py +++ b/src/safeds/data/image/containers/_single_size_image_list.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from safeds._config import _init_default_device, _get_device from safeds._utils import _structural_hash from safeds.data.image._utils._image_transformation_error_and_warning_checks import ( _check_add_noise_errors, @@ -50,6 +51,8 @@ class _SingleSizeImageList(ImageList): def __init__(self) -> None: import torch + _init_default_device() + self._next_batch_index = 0 self._batch_size = 1 @@ -61,6 +64,8 @@ def __init__(self) -> None: def _create_image_list(images: list[Tensor], indices: list[int]) -> ImageList: import torch + _init_default_device() + from safeds.data.image.containers._empty_image_list import _EmptyImageList if len(images) == 0: @@ -127,6 +132,8 @@ def __next__(self) -> Tensor: def _get_batch(self, batch_number: int, batch_size: int | None = None) -> Tensor: import torch + _init_default_device() + if batch_size is None: batch_size = self._batch_size if batch_size * batch_number >= len(self): @@ -175,6 +182,8 @@ def _calc_new_indices_to_tensor_positions(self) -> dict[int, int]: def __eq__(self, other: object) -> bool: import torch + _init_default_device() + if not isinstance(other, ImageList): return NotImplemented if not isinstance(other, _SingleSizeImageList): @@ -261,6 +270,8 @@ def to_jpeg_files(self, path: str | Path | list[str | Path]) -> None: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + if self.channel == 4: raise IllegalFormatError("png") path_str: str | Path @@ -308,6 +319,8 @@ def to_png_files(self, path: str | Path | list[str | Path]) -> None: from torchvision.transforms.v2 import functional as func2 from torchvision.utils import save_image + _init_default_device() + path_str: str | Path if isinstance(path, list): if len(path) == self.number_of_images: @@ -369,6 +382,8 @@ def change_channel(self, channel: int) -> ImageList: def _change_channel_of_tensor(tensor: Tensor, channel: int) -> Tensor: import torch + _init_default_device() + """ Change the channel of a tensor to the given channel. @@ -407,6 +422,8 @@ def _change_channel_of_tensor(tensor: Tensor, channel: int) -> Tensor: def _add_image_tensor(self, image_tensor: Tensor, index: int) -> ImageList: import torch + _init_default_device() + from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList if index in self._indices_to_tensor_positions: @@ -470,6 +487,8 @@ def _add_image_tensor(self, image_tensor: Tensor, index: int) -> ImageList: def add_images(self, images: list[Image] | ImageList) -> ImageList: import torch + _init_default_device() + from safeds.data.image.containers._empty_image_list import _EmptyImageList from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList @@ -624,6 +643,8 @@ def resize(self, new_width: int, new_height: int) -> ImageList: from torchvision.transforms import InterpolationMode from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_resize_errors(new_width, new_height) image_list = self._clone_without_tensor() image_list._tensor = func2.resize( @@ -643,6 +664,8 @@ def _convert_tensor_to_grayscale(tensor: Tensor) -> Tensor: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + if tensor.size(dim=-3) == 4: return torch.cat( [func2.rgb_to_grayscale(tensor[:, 0:3], num_output_channels=3), tensor[:, 3].unsqueeze(dim=1)], @@ -654,6 +677,8 @@ def _convert_tensor_to_grayscale(tensor: Tensor) -> Tensor: def crop(self, x: int, y: int, width: int, height: int) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_crop_errors_and_warnings(x, y, width, height, self.widths[0], self.heights[0], plural=True) image_list = self._clone_without_tensor() image_list._tensor = func2.crop(self._tensor, x, y, height, width) @@ -662,6 +687,8 @@ def crop(self, x: int, y: int, width: int, height: int) -> ImageList: def flip_vertically(self) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + image_list = self._clone_without_tensor() image_list._tensor = func2.vertical_flip(self._tensor) return image_list @@ -669,6 +696,8 @@ def flip_vertically(self) -> ImageList: def flip_horizontally(self) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + image_list = self._clone_without_tensor() image_list._tensor = func2.horizontal_flip(self._tensor) return image_list @@ -677,6 +706,8 @@ def adjust_brightness(self, factor: float) -> ImageList: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_adjust_brightness_errors_and_warnings(factor, plural=True) image_list = self._clone_without_tensor() if self.channel == 4: @@ -691,15 +722,21 @@ def adjust_brightness(self, factor: float) -> ImageList: def add_noise(self, standard_deviation: float) -> ImageList: import torch + _init_default_device() + _check_add_noise_errors(standard_deviation) image_list = self._clone_without_tensor() - image_list._tensor = self._tensor + torch.normal(0, standard_deviation, self._tensor.size()) * 255 + image_list._tensor = ( + self._tensor + torch.normal(0, standard_deviation, self._tensor.size()).to(_get_device()) * 255 + ) return image_list def adjust_contrast(self, factor: float) -> ImageList: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_adjust_contrast_errors_and_warnings(factor, plural=True) image_list = self._clone_without_tensor() if self.channel == 4: @@ -722,6 +759,8 @@ def adjust_color_balance(self, factor: float) -> ImageList: def blur(self, radius: int) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_blur_errors_and_warnings(radius, min(self.widths[0], self.heights[0]), plural=True) image_list = self._clone_without_tensor() image_list._tensor = func2.gaussian_blur(self._tensor, [radius * 2 + 1, radius * 2 + 1]) @@ -731,6 +770,8 @@ def sharpen(self, factor: float) -> ImageList: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + _check_sharpen_errors_and_warnings(factor, plural=True) image_list = self._clone_without_tensor() if self.channel == 4: @@ -746,6 +787,8 @@ def invert_colors(self) -> ImageList: import torch from torchvision.transforms.v2 import functional as func2 + _init_default_device() + image_list = self._clone_without_tensor() if self.channel == 4: image_list._tensor = torch.cat( @@ -759,6 +802,8 @@ def invert_colors(self) -> ImageList: def rotate_right(self) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + image_list = self._clone_without_tensor() image_list._tensor = func2.rotate(self._tensor, -90, expand=True) return image_list @@ -766,6 +811,8 @@ def rotate_right(self) -> ImageList: def rotate_left(self) -> ImageList: from torchvision.transforms.v2 import functional as func2 + _init_default_device() + image_list = self._clone_without_tensor() image_list._tensor = func2.rotate(self._tensor, 90, expand=True) return image_list @@ -773,7 +820,9 @@ def rotate_left(self) -> ImageList: def find_edges(self) -> ImageList: import torch - kernel = Image._filter_edges_kernel().to("cpu") + _init_default_device() + + kernel = Image._filter_edges_kernel() edges_tensor = torch.clamp( torch.nn.functional.conv2d( self.convert_to_grayscale()._as_single_size_image_list()._tensor.float()[:, 0].unsqueeze(dim=1), diff --git a/src/safeds/data/labeled/containers/_image_dataset.py b/src/safeds/data/labeled/containers/_image_dataset.py index 1b2f72a8c..97b95551e 100644 --- a/src/safeds/data/labeled/containers/_image_dataset.py +++ b/src/safeds/data/labeled/containers/_image_dataset.py @@ -5,6 +5,7 @@ import warnings from typing import TYPE_CHECKING, Generic, TypeVar +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.image.containers import ImageList from safeds.data.image.containers._empty_image_list import _EmptyImageList @@ -47,6 +48,8 @@ class ImageDataset(Generic[T]): def __init__(self, input_data: ImageList, output_data: T, batch_size: int = 1, shuffle: bool = False) -> None: import torch + _init_default_device() + self._shuffle_tensor_indices: torch.LongTensor = torch.LongTensor(list(range(len(input_data)))) self._shuffle_after_epoch: bool = shuffle self._batch_size: int = batch_size @@ -224,6 +227,8 @@ def get_output(self) -> T: def _get_batch(self, batch_number: int, batch_size: int | None = None) -> tuple[Tensor, Tensor]: import torch + _init_default_device() + if batch_size is None: batch_size = self._batch_size if batch_size < 1: @@ -274,6 +279,8 @@ def shuffle(self) -> ImageDataset[T]: """ import torch + _init_default_device() + im_dataset: ImageDataset[T] = copy.copy(self) im_dataset._shuffle_tensor_indices = torch.randperm(len(self)) im_dataset._next_batch_index = 0 @@ -285,6 +292,8 @@ class _TableAsTensor: def __init__(self, table: Table) -> None: import torch + _init_default_device() + self._column_names = table.column_names self._tensor = torch.Tensor(table._data.to_numpy(copy=True)).to(torch.get_default_device()) @@ -296,6 +305,8 @@ def __init__(self, table: Table) -> None: def __eq__(self, other: object) -> bool: import torch + _init_default_device() + if not isinstance(other, _TableAsTensor): return NotImplemented return (self is other) or ( @@ -334,6 +345,8 @@ class _ColumnAsTensor: def __init__(self, column: Column) -> None: import torch + _init_default_device() + self._column_name = column.name column_as_table = Table.from_columns([column]) with warnings.catch_warnings(): @@ -350,6 +363,8 @@ def __init__(self, column: Column) -> None: def __eq__(self, other: object) -> bool: import torch + _init_default_device() + if not isinstance(other, _ColumnAsTensor): return NotImplemented return (self is other) or ( diff --git a/src/safeds/data/labeled/containers/_tabular_dataset.py b/src/safeds/data/labeled/containers/_tabular_dataset.py index 87495812f..77dfc8c54 100644 --- a/src/safeds/data/labeled/containers/_tabular_dataset.py +++ b/src/safeds/data/labeled/containers/_tabular_dataset.py @@ -3,6 +3,7 @@ import sys from typing import TYPE_CHECKING +from safeds._config import _init_default_device, _get_device from safeds._utils import _structural_hash from safeds.data.tabular.containers import Column, Table @@ -178,23 +179,29 @@ def _into_dataloader_with_classes(self, batch_size: int, num_of_classes: int) -> import torch from torch.utils.data import DataLoader + _init_default_device() + if num_of_classes <= 2: return DataLoader( dataset=_create_dataset( - torch.Tensor(self.features._data.values), - torch.Tensor(self.target._data).unsqueeze(dim=-1), + torch.Tensor(self.features._data.values).to(_get_device()), + torch.Tensor(self.target._data).to(_get_device()).unsqueeze(dim=-1), ), batch_size=batch_size, shuffle=True, + generator=torch.Generator(device=_get_device()), ) else: return DataLoader( dataset=_create_dataset( - torch.Tensor(self.features._data.values), - torch.nn.functional.one_hot(torch.LongTensor(self.target._data), num_classes=num_of_classes), + torch.Tensor(self.features._data.values).to(_get_device()), + torch.nn.functional.one_hot( + torch.LongTensor(self.target._data).to(_get_device()), num_classes=num_of_classes + ), ), batch_size=batch_size, shuffle=True, + generator=torch.Generator(device=_get_device()), ) # ------------------------------------------------------------------------------------------------------------------ @@ -217,6 +224,8 @@ def _create_dataset(features: Tensor, target: Tensor) -> Dataset: import torch from torch.utils.data import Dataset + _init_default_device() + class _CustomDataset(Dataset): def __init__(self, features: Tensor, target: Tensor): self.X = features.to(torch.float32) diff --git a/src/safeds/data/labeled/containers/_time_series_dataset.py b/src/safeds/data/labeled/containers/_time_series_dataset.py index 33d941541..529f789d4 100644 --- a/src/safeds/data/labeled/containers/_time_series_dataset.py +++ b/src/safeds/data/labeled/containers/_time_series_dataset.py @@ -3,8 +3,10 @@ import sys from typing import TYPE_CHECKING +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.tabular.containers import Column, Table +from safeds.exceptions import OutOfBoundsError, ClosedBound if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -193,8 +195,11 @@ def _into_dataloader_with_window(self, window_size: int, forecast_horizon: int, The size of data batches that should be loaded at one time. Raises + ------ + OutOfBoundsError: + If window_size or forecast_horizon is below 1 ValueError: - If the size is smaller or even than forecast_horizon+window_size + If the size is smaller or even than forecast_horizon + window_size Returns ------- @@ -204,6 +209,8 @@ def _into_dataloader_with_window(self, window_size: int, forecast_horizon: int, import torch from torch.utils.data import DataLoader + _init_default_device() + target_tensor = torch.tensor(self.target._data.values, dtype=torch.float32) x_s = [] @@ -211,9 +218,9 @@ def _into_dataloader_with_window(self, window_size: int, forecast_horizon: int, size = target_tensor.size(0) if window_size < 1: - raise ValueError("window_size must be greater than or equal to 1") + raise OutOfBoundsError(actual=window_size, name="window_size", lower_bound=ClosedBound(1)) if forecast_horizon < 1: - raise ValueError("forecast_horizon must be greater than or equal to 1") + raise OutOfBoundsError(actual=forecast_horizon, name="forecast_horizon", lower_bound=ClosedBound(1)) if size <= forecast_horizon + window_size: raise ValueError("Can not create windows with window size less then forecast horizon + window_size") # create feature windows and for that features targets lagged by forecast len @@ -252,6 +259,13 @@ def _into_dataloader_with_window_predict( batch_size: The size of data batches that should be loaded at one time. + Raises + ------ + OutOfBoundsError: + If window_size or forecast_horizon is below 1 + ValueError: + If the size is smaller or even than forecast_horizon + window_size + Returns ------- result: @@ -260,10 +274,19 @@ def _into_dataloader_with_window_predict( import torch from torch.utils.data import DataLoader + _init_default_device() + target_tensor = torch.tensor(self.target._data.values, dtype=torch.float32) x_s = [] size = target_tensor.size(0) + if window_size < 1: + raise OutOfBoundsError(actual=window_size, name="window_size", lower_bound=ClosedBound(1)) + if forecast_horizon < 1: + raise OutOfBoundsError(actual=forecast_horizon, name="forecast_horizon", lower_bound=ClosedBound(1)) + if size <= forecast_horizon + window_size: + raise ValueError("Can not create windows with window size less then forecast horizon + window_size") + feature_cols = self.features.to_columns() for i in range(size - (forecast_horizon + window_size)): window = target_tensor[i : i + window_size] @@ -296,6 +319,8 @@ def _repr_html_(self) -> str: def _create_dataset(features: torch.Tensor, target: torch.Tensor) -> Dataset: from torch.utils.data import Dataset + _init_default_device() + class _CustomDataset(Dataset): def __init__(self, features_dataset: torch.Tensor, target_dataset: torch.Tensor): self.X = features_dataset @@ -314,6 +339,8 @@ def __len__(self) -> int: def _create_dataset_predict(features: torch.Tensor) -> Dataset: from torch.utils.data import Dataset + _init_default_device() + class _CustomDataset(Dataset): def __init__(self, features: torch.Tensor): self.X = features diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index c62ba7c9b..67aa79e0c 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, TypeVar +from safeds._config import _init_default_device, _get_device from safeds._utils import _structural_hash from safeds.data.image.containers import Image from safeds.data.tabular.typing import ColumnType, Schema @@ -28,6 +29,7 @@ import numpy as np import pandas as pd + import torch from torch.utils.data import DataLoader, Dataset from safeds.data.labeled.containers import TabularDataset, TimeSeriesDataset @@ -2636,8 +2638,11 @@ def _into_dataloader(self, batch_size: int) -> DataLoader: """ import numpy as np + import torch from torch.utils.data import DataLoader + _init_default_device() + features = self.to_rows() all_rows = [] for row in features: @@ -2645,7 +2650,11 @@ def _into_dataloader(self, batch_size: int) -> DataLoader: for column_name in row: new_item.append(row.get_value(column_name)) all_rows.append(new_item.copy()) - return DataLoader(dataset=_create_dataset(np.array(all_rows)), batch_size=batch_size) + return DataLoader( + dataset=_create_dataset(np.array(all_rows)), + batch_size=batch_size, + generator=torch.Generator(device=_get_device()), + ) def _create_dataset(features: np.array) -> Dataset: @@ -2653,9 +2662,11 @@ def _create_dataset(features: np.array) -> Dataset: import torch from torch.utils.data import Dataset + _init_default_device() + class _CustomDataset(Dataset): def __init__(self, features: np.array): - self.X = torch.from_numpy(features.astype(np.float32)) + self.X = torch.from_numpy(features.astype(np.float32)).to(_get_device()) self.len = self.X.shape[0] def __getitem__(self, item: int) -> torch.Tensor: diff --git a/src/safeds/ml/nn/_convolutional2d_layer.py b/src/safeds/ml/nn/_convolutional2d_layer.py index 59e0c2dde..f483e668b 100644 --- a/src/safeds/ml/nn/_convolutional2d_layer.py +++ b/src/safeds/ml/nn/_convolutional2d_layer.py @@ -4,6 +4,7 @@ import sys from typing import TYPE_CHECKING, Any, Literal +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.image.typing import ImageSize @@ -25,6 +26,8 @@ def _create_internal_model( ) -> nn.Module: from torch import nn + _init_default_device() + class _InternalLayer(nn.Module): def __init__( self, diff --git a/src/safeds/ml/nn/_flatten_layer.py b/src/safeds/ml/nn/_flatten_layer.py index 50269b448..ea10faa64 100644 --- a/src/safeds/ml/nn/_flatten_layer.py +++ b/src/safeds/ml/nn/_flatten_layer.py @@ -3,6 +3,7 @@ import sys from typing import TYPE_CHECKING, Any +from safeds._config import _init_default_device from safeds._utils import _structural_hash if TYPE_CHECKING: @@ -16,6 +17,8 @@ def _create_internal_model() -> nn.Module: from torch import nn + _init_default_device() + class _InternalLayer(nn.Module): def __init__(self) -> None: super().__init__() diff --git a/src/safeds/ml/nn/_forward_layer.py b/src/safeds/ml/nn/_forward_layer.py index 5c3268802..39f07bfa3 100644 --- a/src/safeds/ml/nn/_forward_layer.py +++ b/src/safeds/ml/nn/_forward_layer.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any +from safeds._config import _init_default_device from safeds.data.image.typing import ImageSize if TYPE_CHECKING: @@ -15,6 +16,8 @@ def _create_internal_model(input_size: int, output_size: int, activation_function: str) -> nn.Module: from torch import nn + _init_default_device() + class _InternalLayer(nn.Module): def __init__(self, input_size: int, output_size: int, activation_function: str): super().__init__() diff --git a/src/safeds/ml/nn/_lstm_layer.py b/src/safeds/ml/nn/_lstm_layer.py index 4b7053892..33f29fa0c 100644 --- a/src/safeds/ml/nn/_lstm_layer.py +++ b/src/safeds/ml/nn/_lstm_layer.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any +from safeds._config import _init_default_device from safeds.data.image.typing import ImageSize if TYPE_CHECKING: @@ -17,6 +18,8 @@ def _create_internal_model(input_size: int, output_size: int, activation_function: str) -> nn.Module: from torch import nn + _init_default_device() + class _InternalLayer(nn.Module): def __init__(self, input_size: int, output_size: int, activation_function: str): super().__init__() diff --git a/src/safeds/ml/nn/_model.py b/src/safeds/ml/nn/_model.py index 569444b9c..8dfbc9f74 100644 --- a/src/safeds/ml/nn/_model.py +++ b/src/safeds/ml/nn/_model.py @@ -3,8 +3,9 @@ import copy from typing import TYPE_CHECKING, Generic, Self, TypeVar +from safeds._config import _init_default_device from safeds.data.image.containers import ImageList -from safeds.data.labeled.containers import TabularDataset, TimeSeriesDataset, ImageDataset +from safeds.data.labeled.containers import ImageDataset, TabularDataset, TimeSeriesDataset from safeds.data.tabular.containers import Table from safeds.exceptions import ( ClosedBound, @@ -31,10 +32,8 @@ from torch import Tensor, nn - from safeds.ml.nn._input_conversion import InputConversion - from safeds.ml.nn._layer import Layer - from safeds.ml.nn._output_conversion import OutputConversion from safeds.data.image.typing import ImageSize + from safeds.ml.nn import InputConversion, Layer, OutputConversion IFT = TypeVar("IFT", TabularDataset, TimeSeriesDataset, ImageDataset) # InputFitType @@ -159,6 +158,8 @@ def fit( import torch from torch import nn + _init_default_device() + if not self._input_conversion._is_fit_data_valid(train_data): raise FeatureDataMismatchError if epoch_size < 1: @@ -229,6 +230,8 @@ def predict(self, test_data: IPT) -> OT: """ import torch + _init_default_device() + if not self._is_fitted: raise ModelNotFittedError if not self._input_conversion._is_predict_data_valid(test_data): @@ -371,6 +374,8 @@ def fit( import torch from torch import nn + _init_default_device() + if not self._input_conversion._is_fit_data_valid(train_data): raise FeatureDataMismatchError if epoch_size < 1: @@ -394,6 +399,7 @@ def fit( loss_fn = nn.CrossEntropyLoss() else: loss_fn = nn.BCELoss() + optimizer = torch.optim.SGD(copied_model._model.parameters(), lr=learning_rate) for _ in range(epoch_size): loss_sum = 0.0 @@ -401,6 +407,7 @@ def fit( for x, y in iter(dataloader): optimizer.zero_grad() pred = copied_model._model(x) + loss = loss_fn(pred, y) loss_sum += loss.item() amount_of_loss_values_calculated += 1 @@ -446,6 +453,8 @@ def predict(self, test_data: IPT) -> OT: """ import torch + _init_default_device() + if not self._is_fitted: raise ModelNotFittedError if not self._input_conversion._is_predict_data_valid(test_data): @@ -478,6 +487,8 @@ def _create_internal_model( ) -> nn.Module: from torch import nn + _init_default_device() + class _InternalModel(nn.Module): def __init__(self, layers: list[Layer], is_for_classification: bool) -> None: diff --git a/src/safeds/ml/nn/_output_conversion_image.py b/src/safeds/ml/nn/_output_conversion_image.py index 1973d9473..fd19ff322 100644 --- a/src/safeds/ml/nn/_output_conversion_image.py +++ b/src/safeds/ml/nn/_output_conversion_image.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.image.containers import ImageList from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList @@ -69,6 +70,8 @@ class OutputConversionImageToColumn(_OutputConversionImage): def _data_conversion(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset[Column]: import torch + _init_default_device() + if not isinstance(input_data, _SingleSizeImageList): raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 if "column_name" not in kwargs or not isinstance(kwargs.get("column_name"), str): @@ -101,6 +104,8 @@ class OutputConversionImageToTable(_OutputConversionImage): def _data_conversion(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset[Table]: import torch + _init_default_device() + if not isinstance(input_data, _SingleSizeImageList): raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 if ( @@ -137,6 +142,8 @@ def _data_conversion( ) -> ImageDataset[ImageList]: import torch + _init_default_device() + if not isinstance(input_data, _SingleSizeImageList): raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 diff --git a/src/safeds/ml/nn/_pooling2d_layer.py b/src/safeds/ml/nn/_pooling2d_layer.py index f3d777b01..dd4584b2e 100644 --- a/src/safeds/ml/nn/_pooling2d_layer.py +++ b/src/safeds/ml/nn/_pooling2d_layer.py @@ -4,6 +4,7 @@ import sys from typing import TYPE_CHECKING, Any, Literal +from safeds._config import _init_default_device from safeds._utils import _structural_hash from safeds.data.image.typing import ImageSize @@ -16,6 +17,8 @@ def _create_internal_model(strategy: Literal["max", "avg"], kernel_size: int, padding: int, stride: int) -> nn.Module: from torch import nn + _init_default_device() + class _InternalLayer(nn.Module): def __init__(self, strategy: Literal["max", "avg"], kernel_size: int, padding: int, stride: int): super().__init__() diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 8dbe9ac54..883650a52 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -3,11 +3,11 @@ assert_that_tabular_datasets_are_equal, ) from ._devices import ( + configure_test_with_device, device_cpu, device_cuda, get_devices, get_devices_ids, - skip_if_device_not_available, ) from ._images import ( grayscale_jpg_id, @@ -38,6 +38,7 @@ __all__ = [ "assert_that_tables_are_close", "assert_that_tabular_datasets_are_equal", + "configure_test_with_device", "device_cpu", "device_cuda", "grayscale_jpg_id", @@ -62,7 +63,6 @@ "resolve_resource_path", "rgba_png_id", "rgba_png_path", - "skip_if_device_not_available", "skip_if_os", "test_images_folder", "white_square_jpg_id", diff --git a/tests/helpers/_devices.py b/tests/helpers/_devices.py index b4405bcc3..b371a85f7 100644 --- a/tests/helpers/_devices.py +++ b/tests/helpers/_devices.py @@ -2,6 +2,11 @@ import torch from torch.types import Device +from safeds._config import _init_default_device +from safeds._config._device import _set_default_device + +_init_default_device() + device_cpu = torch.device("cpu") device_cuda = torch.device("cuda") @@ -14,6 +19,11 @@ def get_devices_ids() -> list[str]: return ["cpu", "cuda"] -def skip_if_device_not_available(device: Device) -> None: +def configure_test_with_device(device: Device) -> None: + _skip_if_device_not_available(device) # This will end the function if device is not available + _set_default_device(device) + + +def _skip_if_device_not_available(device: Device) -> None: if device == device_cuda and not torch.cuda.is_available(): pytest.skip("This test requires cuda") diff --git a/tests/safeds/_config/test_device.py b/tests/safeds/_config/test_device.py index 1fecb246e..d99997757 100644 --- a/tests/safeds/_config/test_device.py +++ b/tests/safeds/_config/test_device.py @@ -1,9 +1,33 @@ +import pytest import torch -from safeds._config import _get_device +from torch.types import Device +from safeds._config import _get_device, _init_default_device +from safeds._config._device import _set_default_device +from tests.helpers import get_devices, get_devices_ids, configure_test_with_device, device_cuda, device_cpu +from tests.helpers._devices import _skip_if_device_not_available -def test_device() -> None: + +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_default_device(device: Device) -> None: + configure_test_with_device(device) + assert _get_device().type == device.type + assert torch.get_default_device().type == device.type + + +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_set_default_device(device: Device) -> None: + _skip_if_device_not_available(device) + _set_default_device(device) + assert _get_device().type == device.type + assert torch.get_default_device().type == device.type + + +def test_init_default_device() -> None: + _init_default_device() if torch.cuda.is_available(): - assert _get_device() == torch.device("cuda") + assert _get_device().type == device_cuda.type + assert torch.get_default_device().type == device_cuda.type else: - assert _get_device() == torch.device("cpu") + assert _get_device().type == device_cpu.type + assert torch.get_default_device().type == device_cpu.type diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-all-images].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path-cuda].png new file mode 100644 index 000000000..f87bcd39e Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[all-images-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cpu].png new file mode 100644 index 000000000..2490ac5e7 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cuda].png new file mode 100644 index 000000000..2490ac5e7 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[images_folder-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cpu].png new file mode 100644 index 000000000..fc925a7c9 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cuda].png new file mode 100644 index 000000000..fc925a7c9 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-jpg-grayscale-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cpu].png new file mode 100644 index 000000000..980ad65d6 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cuda].png new file mode 100644 index 000000000..980ad65d6 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-1-channel-png-grayscale-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cpu].png new file mode 100644 index 000000000..479b40a8d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cuda].png new file mode 100644 index 000000000..479b40a8d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-plane-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-jpg-white_square-path-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cpu].png new file mode 100644 index 000000000..146086a9d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cuda].png new file mode 100644 index 000000000..146086a9d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cpu].png new file mode 100644 index 000000000..146086a9d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cuda].png new file mode 100644 index 000000000..146086a9d Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-3-channel-png-white_square-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cpu].png new file mode 100644 index 000000000..0f0a23a1f Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cuda].png new file mode 100644 index 000000000..0f0a23a1f Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[opaque-4-channel-png-plane-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-cuda].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-cuda].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cpu].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cpu].png new file mode 100644 index 000000000..6da78e96e Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cpu].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cuda].png new file mode 100644 index 000000000..6da78e96e Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestFromFiles.test_from_files_creation[transparent-4-channel-png-rgba-path-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images-cuda].png new file mode 100644 index 000000000..e2c8c75fc Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[all-images-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes-cpu].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes-cpu].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes-cuda].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes-cuda].png new file mode 100644 index 000000000..15d96e3a7 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestShuffleImages.test_shuffle_images[planes-cuda].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cpu-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cpu-all-images].png new file mode 100644 index 000000000..f87bcd39e Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cpu-all-images].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cpu-planes].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-planes].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cpu-planes].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-all-images].png new file mode 100644 index 000000000..f87bcd39e Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-all-images].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-planes].png new file mode 100644 index 000000000..b0d74cc6b Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[minimum noise-cuda-planes].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cpu-all-images].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-all-images].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cpu-all-images].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cpu-planes].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-planes].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cpu-planes].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-all-images].png new file mode 100644 index 000000000..c20a7f4bc Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-all-images].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-planes].png new file mode 100644 index 000000000..64f1845d0 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[some noise-cuda-planes].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cpu-all-images].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-all-images].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cpu-all-images].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cpu-planes].png similarity index 100% rename from tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-planes].png rename to tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cpu-planes].png diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-all-images].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-all-images].png new file mode 100644 index 000000000..51889a982 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-all-images].png differ diff --git a/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-planes].png b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-planes].png new file mode 100644 index 000000000..5e67a2173 Binary files /dev/null and b/tests/safeds/data/image/containers/__snapshots__/test_image_list/TestTransforms.TestAddNoise.test_should_add_noise[very noisy-cuda-planes].png differ diff --git a/tests/safeds/data/image/containers/test_image.py b/tests/safeds/data/image/containers/test_image.py index a6a8a3ada..34d5bb37d 100644 --- a/tests/safeds/data/image/containers/test_image.py +++ b/tests/safeds/data/image/containers/test_image.py @@ -7,6 +7,8 @@ import PIL.Image import pytest import torch + +from safeds._config import _get_device from safeds.data.image.containers import Image from safeds.data.image.typing import ImageSize from safeds.data.tabular.containers import Table @@ -15,7 +17,7 @@ from torch.types import Device from tests.helpers import ( - device_cuda, + configure_test_with_device, get_devices, get_devices_ids, grayscale_jpg_id, @@ -34,12 +36,13 @@ resolve_resource_path, rgba_png_id, rgba_png_path, - skip_if_device_not_available, skip_if_os, white_square_jpg_id, white_square_jpg_path, white_square_png_id, white_square_png_path, + device_cpu, + device_cuda, ) @@ -60,8 +63,8 @@ class TestFromFile: ], ) def test_should_load_from_file(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert image != Image(torch.empty(1, 1, 1)) @pytest.mark.parametrize( @@ -75,9 +78,9 @@ def test_should_load_from_file(self, resource_path: str | Path, device: Device) ids=["missing_file_jpg", "missing_file_jpg_Path", "missing_file_png", "missing_file_png_Path"], ) def test_should_raise_if_file_not_found(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.raises(FileNotFoundError): - Image.from_file(resolve_resource_path(resource_path), device) + Image.from_file(resolve_resource_path(resource_path)) @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @@ -88,9 +91,9 @@ class TestFromBytes: ids=[plane_jpg_id, white_square_jpg_id, white_square_png_id, grayscale_jpg_id, grayscale_png_id], ) def test_should_write_and_load_bytes_jpeg(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) - image_copy = Image.from_bytes(typing.cast(bytes, image._repr_jpeg_()), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) + image_copy = Image.from_bytes(typing.cast(bytes, image._repr_jpeg_())) _assert_width_height_channel(image, image_copy) @pytest.mark.parametrize( @@ -99,9 +102,9 @@ def test_should_write_and_load_bytes_jpeg(self, resource_path: str | Path, devic ids=images_all_ids(), ) def test_should_write_and_load_bytes_png(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) - image_copy = Image.from_bytes(image._repr_png_(), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) + image_copy = Image.from_bytes(image._repr_png_()) assert image == image_copy @@ -114,8 +117,8 @@ class TestToNumpyArray: ids=images_all_ids(), ) def test_should_return_numpy_array(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image_safeds = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image_safeds = Image.from_file(resolve_resource_path(resource_path)) image_np = np.array(PIL.Image.open(resolve_resource_path(resource_path))) assert np.all(np.array(image_safeds).squeeze() == image_np) @@ -128,8 +131,8 @@ class TestReprJpeg: ids=[plane_jpg_id, white_square_jpg_id, white_square_png_id, grayscale_jpg_id, grayscale_png_id], ) def test_should_return_bytes(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert isinstance(image._repr_jpeg_(), bytes) @pytest.mark.parametrize( @@ -141,8 +144,8 @@ def test_should_return_bytes(self, resource_path: str | Path, device: Device) -> ids=[plane_png_id, rgba_png_id], ) def test_should_return_none_if_image_has_alpha_channel(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert image._repr_jpeg_() is None @@ -154,8 +157,8 @@ class TestReprPng: ids=images_all_ids(), ) def test_should_return_bytes(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert isinstance(image._repr_png_(), bytes) @@ -167,8 +170,8 @@ class TestToJpegFile: ids=[plane_jpg_id, white_square_jpg_id, white_square_png_id, grayscale_jpg_id, grayscale_png_id], ) def test_should_save_file(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with NamedTemporaryFile(suffix=".jpg") as tmp_jpeg_file: tmp_jpeg_file.close() with Path(tmp_jpeg_file.name).open("w", encoding="utf-8") as tmp_file: @@ -186,8 +189,8 @@ def test_should_save_file(self, resource_path: str | Path, device: Device) -> No ids=[plane_png_id, rgba_png_id], ) def test_should_raise_if_image_has_alpha_channel(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with NamedTemporaryFile(suffix=".jpg") as tmp_jpeg_file: tmp_jpeg_file.close() with Path(tmp_jpeg_file.name).open("w", encoding="utf-8") as tmp_file, pytest.raises( @@ -205,8 +208,8 @@ class TestToPngFile: ids=images_all_ids(), ) def test_should_save_file(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with NamedTemporaryFile(suffix=".png") as tmp_png_file: tmp_png_file.close() with Path(tmp_png_file.name).open("w", encoding="utf-8") as tmp_file: @@ -282,8 +285,8 @@ def test_should_return_image_properties( channel: int, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert image.width == width assert image.height == height assert image.channel == channel @@ -298,36 +301,17 @@ class TestEQ: ids=images_all_ids(), ) def test_should_be_equal(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) - image2 = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) + image2 = Image.from_file(resolve_resource_path(resource_path)) assert image == image2 @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_should_not_be_equal(self, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(plane_png_path), device) - image2 = Image.from_file(resolve_resource_path(white_square_png_path), device) - assert image != image2 - - @pytest.mark.parametrize( - "resource_path", - images_all(), - ids=images_all_ids(), - ) - def test_should_be_equal_different_devices(self, resource_path: str) -> None: - skip_if_device_not_available(device_cuda) - image = Image.from_file(resolve_resource_path(resource_path), torch.device("cpu")) - image2 = Image.from_file(resolve_resource_path(resource_path), torch.device("cuda")) - assert image == image2 - assert image2 == image - - def test_should_not_be_equal_different_devices(self) -> None: - skip_if_device_not_available(device_cuda) - image = Image.from_file(resolve_resource_path(plane_png_path), torch.device("cpu")) - image2 = Image.from_file(resolve_resource_path(white_square_png_path), torch.device("cuda")) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(plane_png_path)) + image2 = Image.from_file(resolve_resource_path(white_square_png_path)) assert image != image2 - assert image2 != image @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @pytest.mark.parametrize( @@ -336,8 +320,8 @@ def test_should_not_be_equal_different_devices(self) -> None: ids=images_all_ids(), ) def test_should_be_not_implemented(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) other = Table() assert (image.__eq__(other)) is NotImplemented @@ -350,33 +334,16 @@ class TestHash: ids=images_all_ids(), ) def test_should_hash_be_equal(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) - image2 = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) + image2 = Image.from_file(resolve_resource_path(resource_path)) assert hash(image) == hash(image2) @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_should_hash_not_be_equal(self, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(plane_png_path), device) - image2 = Image.from_file(resolve_resource_path(white_square_png_path), device) - assert hash(image) != hash(image2) - - @pytest.mark.parametrize( - "resource_path", - images_all(), - ids=images_all_ids(), - ) - def test_should_hash_be_equal_different_devices(self, resource_path: str) -> None: - skip_if_device_not_available(device_cuda) - image = Image.from_file(resolve_resource_path(resource_path), torch.device("cpu")) - image2 = Image.from_file(resolve_resource_path(resource_path), torch.device("cuda")) - assert hash(image) == hash(image2) - - def test_should_hash_not_be_equal_different_devices(self) -> None: - skip_if_device_not_available(device_cuda) - image = Image.from_file(resolve_resource_path(plane_png_path), torch.device("cpu")) - image2 = Image.from_file(resolve_resource_path(white_square_png_path), torch.device("cuda")) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(plane_png_path)) + image2 = Image.from_file(resolve_resource_path(white_square_png_path)) assert hash(image) != hash(image2) @@ -395,8 +362,8 @@ def test_should_change_channel( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) new_image = image.change_channel(channel) assert new_image.channel == channel assert new_image == snapshot_png_image @@ -408,8 +375,8 @@ def test_should_change_channel( ) @pytest.mark.parametrize("channel", [2], ids=["invalid-channel"]) def test_should_raise(self, resource_path: str, channel: int, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises(ValueError, match=rf"Channel {channel} is not a valid channel option. Use either 1, 3 or 4"): image.change_channel(channel) @@ -447,8 +414,8 @@ def test_should_return_resized_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) new_image = image.resize(new_width, new_height) assert new_image.width == new_width assert new_image.height == new_height @@ -467,8 +434,8 @@ def test_should_return_resized_image( ids=["invalid width", "invalid height", "invalid width and height"], ) def test_should_raise(self, resource_path: str, new_width: int, new_height: int, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, match=rf"At least one of the new sizes new_width and new_height \(={min(new_width, new_height)}\) is not inside \[1, \u221e\).", @@ -476,19 +443,6 @@ def test_should_raise(self, resource_path: str, new_width: int, new_height: int, image.resize(new_width, new_height) -class TestDevices: - @pytest.mark.parametrize( - "resource_path", - images_all(), - ids=images_all_ids(), - ) - def test_should_change_device(self, resource_path: str) -> None: - skip_if_device_not_available(device_cuda) - image = Image.from_file(resolve_resource_path(resource_path), torch.device("cpu")) - new_device = torch.device("cuda", 0) - assert image._set_device(new_device).device == new_device - - @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestConvertToGrayscale: @pytest.mark.parametrize( @@ -502,8 +456,8 @@ def test_convert_to_grayscale( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) grayscale_image = image.convert_to_grayscale() assert grayscale_image == snapshot_png_image _assert_width_height_channel(image, grayscale_image) @@ -522,8 +476,8 @@ def test_should_return_cropped_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_cropped = image.crop(0, 0, 100, 100) assert image_cropped == snapshot_png_image assert image_cropped.channel == image.channel @@ -545,8 +499,8 @@ def test_should_raise_invalid_size( new_height: int, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, match=rf"At least one of width and height \(={min(new_width, new_height)}\) is not inside \[1, \u221e\).", @@ -564,8 +518,8 @@ def test_should_raise_invalid_size( ids=["invalid x", "invalid y", "invalid x and y"], ) def test_should_raise_invalid_coordinates(self, resource_path: str, new_x: int, new_y: int, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, match=rf"At least one of the coordinates x and y \(={min(new_x, new_y)}\) is not inside \[0, \u221e\).", @@ -589,8 +543,8 @@ def test_should_warn_if_coordinates_outsize_image( new_y: int, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_blank_tensor = torch.zeros((image.channel, 1, 1), device=device) with pytest.warns( UserWarning, @@ -613,8 +567,8 @@ def test_should_flip_vertically( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_flip_v = image.flip_vertically() assert image != image_flip_v assert image_flip_v == snapshot_png_image @@ -626,8 +580,8 @@ def test_should_flip_vertically( ids=images_all_ids(), ) def test_should_be_original(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_flip_v_v = image.flip_vertically().flip_vertically() assert image == image_flip_v_v @@ -645,8 +599,8 @@ def test_should_flip_horizontally( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_flip_h = image.flip_horizontally() assert image != image_flip_h assert image_flip_h == snapshot_png_image @@ -658,8 +612,8 @@ def test_should_flip_horizontally( ids=images_all_ids(), ) def test_should_be_original(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_flip_h_h = image.flip_horizontally().flip_horizontally() assert image == image_flip_h_h @@ -679,8 +633,8 @@ def test_should_adjust_brightness( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_brightness = image.adjust_brightness(factor) assert image != image_adjusted_brightness assert image_adjusted_brightness == snapshot_png_image @@ -692,12 +646,12 @@ def test_should_adjust_brightness( ids=images_all_ids(), ) def test_should_not_brighten(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns( UserWarning, match="Brightness adjustment factor is 1.0, this will not make changes to the image.", ): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image2 = image.adjust_brightness(1) assert image == image2 @@ -707,8 +661,8 @@ def test_should_not_brighten(self, resource_path: str, device: Device) -> None: ids=images_all_ids(), ) def test_should_raise(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises(OutOfBoundsError, match=r"factor \(=-1\) is not inside \[0, \u221e\)."): image.adjust_brightness(-1) @@ -736,10 +690,10 @@ def test_should_add_noise( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) skip_if_os([os_mac]) torch.manual_seed(0) - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_noise = image.add_noise(standard_deviation) assert image_noise == snapshot_png_image _assert_width_height_channel(image, image_noise) @@ -760,8 +714,8 @@ def test_should_raise_standard_deviation( standard_deviation: float, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, match=rf"standard_deviation \(={standard_deviation}\) is not inside \[0, \u221e\)\.", @@ -784,8 +738,8 @@ def test_should_adjust_contrast( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_contrast = image.adjust_contrast(factor) assert image != image_adjusted_contrast assert image_adjusted_contrast == snapshot_png_image @@ -797,12 +751,12 @@ def test_should_adjust_contrast( ids=images_all_ids(), ) def test_should_not_adjust_contrast(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns( UserWarning, match="Contrast adjustment factor is 1.0, this will not make changes to the image.", ): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_contrast = image.adjust_contrast(1) assert image == image_adjusted_contrast @@ -812,9 +766,9 @@ def test_should_not_adjust_contrast(self, resource_path: str, device: Device) -> ids=images_all_ids(), ) def test_should_raise_negative_contrast(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.raises(OutOfBoundsError, match=r"factor \(=-1.0\) is not inside \[0, \u221e\)."): - Image.from_file(resolve_resource_path(resource_path), device).adjust_contrast(-1.0) + Image.from_file(resolve_resource_path(resource_path)).adjust_contrast(-1.0) @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @@ -832,8 +786,8 @@ def test_should_adjust_colors( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_color_balance = image.adjust_color_balance(factor) assert image != image_adjusted_color_balance assert image_adjusted_color_balance == snapshot_png_image @@ -844,12 +798,12 @@ def test_should_adjust_colors( ids=images_all_ids(), ) def test_should_not_adjust_colors_factor_1(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns( UserWarning, match="Color adjustment factor is 1.0, this will not make changes to the image.", ): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_color_balance = image.adjust_color_balance(1) assert image == image_adjusted_color_balance @@ -859,12 +813,12 @@ def test_should_not_adjust_colors_factor_1(self, resource_path: str, device: Dev ids=[grayscale_png_id, grayscale_jpg_id], ) def test_should_not_adjust_colors_channel_1(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns( UserWarning, match="Color adjustment will not have an affect on grayscale images with only one channel", ): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_adjusted_color_balance = image.adjust_color_balance(0.5) assert image == image_adjusted_color_balance @@ -874,9 +828,9 @@ def test_should_not_adjust_colors_channel_1(self, resource_path: str, device: De ids=images_all_ids(), ) def test_should_raise_negative_color_adjust(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.raises(OutOfBoundsError, match=r"factor \(=-1.0\) is not inside \[0, \u221e\)."): - Image.from_file(resolve_resource_path(resource_path), device).adjust_color_balance(-1.0) + Image.from_file(resolve_resource_path(resource_path)).adjust_color_balance(-1.0) @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @@ -892,9 +846,9 @@ def test_should_return_blurred_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) skip_if_os([os_mac]) - image = Image.from_file(resolve_resource_path(resource_path), device=device) + image = Image.from_file(resolve_resource_path(resource_path)) image_blurred = image.blur(2) assert image_blurred == snapshot_png_image _assert_width_height_channel(image, image_blurred) @@ -905,12 +859,12 @@ def test_should_return_blurred_image( ids=images_asymmetric_ids(), ) def test_should_not_blur_radius_0(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns( UserWarning, match="Blur radius is 0, this will not make changes to the image.", ): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_blurred = image.blur(0) assert image == image_blurred @@ -920,8 +874,8 @@ def test_should_not_blur_radius_0(self, resource_path: str, device: Device) -> N ids=images_asymmetric_ids(), ) def test_should_raise_blur_radius_out_of_bounds(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, match=rf"radius \(=-1\) is not inside \[0, {min(image.width, image.height) - 1}\].", @@ -949,8 +903,8 @@ def test_should_sharpen( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_sharpened = image.sharpen(factor) assert image != image_sharpened assert image_sharpened == snapshot_png_image @@ -962,9 +916,9 @@ def test_should_sharpen( ids=images_all_ids(), ) def test_should_raise_negative_sharpen(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.raises(OutOfBoundsError, match=r"factor \(=-1.0\) is not inside \[0, \u221e\)."): - Image.from_file(resolve_resource_path(resource_path), device).sharpen(-1.0) + Image.from_file(resolve_resource_path(resource_path)).sharpen(-1.0) @pytest.mark.parametrize( "resource_path", @@ -972,9 +926,9 @@ def test_should_raise_negative_sharpen(self, resource_path: str, device: Device) ids=images_all_ids(), ) def test_should_not_sharpen(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) + configure_test_with_device(device) with pytest.warns(UserWarning, match="Sharpen factor is 1.0, this will not make changes to the image."): - image = Image.from_file(resolve_resource_path(resource_path), device) + image = Image.from_file(resolve_resource_path(resource_path)) image_sharpened = image.sharpen(1) assert image == image_sharpened @@ -992,8 +946,8 @@ def test_should_invert_colors( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_inverted_colors = image.invert_colors() assert image_inverted_colors == snapshot_png_image _assert_width_height_channel(image, image_inverted_colors) @@ -1012,8 +966,8 @@ def test_should_return_clockwise_rotated_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_right_rotated = image.rotate_right() assert image_right_rotated == snapshot_png_image assert image.channel == image_right_rotated.channel @@ -1029,8 +983,8 @@ def test_should_return_counter_clockwise_rotated_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_left_rotated = image.rotate_left() assert image_left_rotated == snapshot_png_image assert image.channel == image_left_rotated.channel @@ -1041,8 +995,8 @@ def test_should_return_counter_clockwise_rotated_image( ids=images_all_ids(), ) def test_should_return_flipped_image(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_left_rotated = image.rotate_left().rotate_left() image_right_rotated = image.rotate_right().rotate_right() image_flipped_h_v = image.flip_horizontally().flip_vertically() @@ -1057,8 +1011,8 @@ def test_should_return_flipped_image(self, resource_path: str, device: Device) - ids=images_all_ids(), ) def test_should_be_original(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_left_right_rotated = image.rotate_left().rotate_right() image_right_left_rotated = image.rotate_right().rotate_left() image_left_l_l_l_l = image.rotate_left().rotate_left().rotate_left().rotate_left() @@ -1082,13 +1036,25 @@ def test_should_return_edges_of_image( snapshot_png_image: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device=device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) image_edges = image.find_edges() assert image_edges == snapshot_png_image _assert_width_height_channel(image, image_edges) +class TestFilterEdgesKernel: + + def test_should_kernel_change_device(self) -> None: + assert Image._filter_edges_kernel().device == _get_device() + configure_test_with_device(device_cpu) + assert Image._filter_edges_kernel().device == _get_device() + configure_test_with_device(device_cuda) + assert Image._filter_edges_kernel().device == _get_device() + configure_test_with_device(device_cpu) + assert Image._filter_edges_kernel().device == _get_device() + + @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestSizeof: @pytest.mark.parametrize( @@ -1097,6 +1063,6 @@ class TestSizeof: ids=images_all_ids(), ) def test_should_size_be_greater_than_normal_object(self, resource_path: str | Path, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) assert sys.getsizeof(image) >= image.width * image.height * image.channel diff --git a/tests/safeds/data/image/containers/test_image_list.py b/tests/safeds/data/image/containers/test_image_list.py index 9b6e15dbc..b936bb3cf 100644 --- a/tests/safeds/data/image/containers/test_image_list.py +++ b/tests/safeds/data/image/containers/test_image_list.py @@ -6,7 +6,8 @@ import pytest import torch -from safeds._config import _get_device +from torch.types import Device + from safeds.data.image.containers import Image, ImageList from safeds.data.image.containers._empty_image_list import _EmptyImageList from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList @@ -30,17 +31,21 @@ skip_if_os, test_images_folder, white_square_jpg_path, + get_devices, + get_devices_ids, + configure_test_with_device, ) +@pytest.mark.parametrize("resource_path3", images_all(), ids=images_all_ids()) +@pytest.mark.parametrize("resource_path2", images_all(), ids=images_all_ids()) +@pytest.mark.parametrize("resource_path1", images_all(), ids=images_all_ids()) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestAllImageCombinations: - @pytest.mark.parametrize("resource_path3", images_all(), ids=images_all_ids()) - @pytest.mark.parametrize("resource_path2", images_all(), ids=images_all_ids()) - @pytest.mark.parametrize("resource_path1", images_all(), ids=images_all_ids()) - def test_from_files(self, resource_path1: str, resource_path2: str, resource_path3: str) -> None: + def test_from_files(self, resource_path1: str, resource_path2: str, resource_path3: str, device: Device) -> None: # Setup - torch.set_default_device(_get_device()) + configure_test_with_device(device) image_list = ImageList.from_files( [ @@ -435,6 +440,7 @@ def test_from_files(self, resource_path1: str, resource_path2: str, resource_pat assert image_list == image_list_clone +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestFromFiles: @pytest.mark.parametrize( @@ -456,8 +462,10 @@ class TestFromFiles: *[s + "-path" for s in images_all_ids()], ], ) - def test_from_files_creation(self, resource_path: str | Path, snapshot_png_image_list: SnapshotAssertion) -> None: - torch.set_default_device(torch.device("cpu")) + def test_from_files_creation( + self, resource_path: str | Path, snapshot_png_image_list: SnapshotAssertion, device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) image_list_returned_filenames, filenames = ImageList.from_files( resolve_resource_path(resource_path), @@ -479,11 +487,13 @@ def test_from_files_creation(self, resource_path: str | Path, snapshot_png_image ], ids=["dir-str", "dir-path", "list-str", "list-path", "list-str-last-missing", "list-path-last-missing"], ) - def test_should_raise_if_one_file_or_directory_not_found(self, resource_path: str | Path) -> None: + def test_should_raise_if_one_file_or_directory_not_found(self, resource_path: str | Path, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(FileNotFoundError): ImageList.from_files(resolve_resource_path(resource_path)) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestToImages: @pytest.mark.parametrize( @@ -491,8 +501,8 @@ class TestToImages: [images_all(), [plane_png_path, plane_jpg_path] * 2], ids=["all-images", "planes"], ) - def test_should_return_images(self, resource_path: list[str]) -> None: - torch.set_default_device(torch.device("cpu")) + def test_should_return_images(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list_all = ImageList.from_files(resolve_resource_path(resource_path)) image_list_select = ImageList.from_files(resolve_resource_path(resource_path[::2])) assert image_list_all.to_images(list(range(0, len(image_list_all), 2))) == image_list_select.to_images() @@ -502,8 +512,8 @@ def test_should_return_images(self, resource_path: list[str]) -> None: [images_all(), [plane_png_path, plane_jpg_path]], ids=["all-images", "planes"], ) - def test_from_files_creation(self, resource_path: list[str]) -> None: - torch.set_default_device(torch.device("cpu")) + def test_from_files_creation(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) bracket_open = r"\[" bracket_close = r"\]" @@ -514,6 +524,7 @@ def test_from_files_creation(self, resource_path: list[str]) -> None: image_list.to_images(list(range(2, 2 + len(image_list)))) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestToJpegFiles: @pytest.mark.parametrize( @@ -521,7 +532,8 @@ class TestToJpegFiles: [images_all(), [plane_png_path, plane_jpg_path]], ids=["all-images", "planes"], ) - def test_should_raise_if_alpha_channel(self, resource_path: list[str]) -> None: + def test_should_raise_if_alpha_channel(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with ( tempfile.TemporaryDirectory() as tmpdir, @@ -546,7 +558,8 @@ def test_should_raise_if_alpha_channel(self, resource_path: list[str]) -> None: ], ids=["all-jpg-images", "jpg-planes", "jpg-grayscale"], ) - def test_should_raise_if_invalid_path(self, resource_path: list[str]) -> None: + def test_should_raise_if_invalid_path(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( ValueError, @@ -571,7 +584,8 @@ def test_should_raise_if_invalid_path(self, resource_path: list[str]) -> None: ], ids=["all-jpg-images", "jpg-planes", "jpg-grayscale"], ) - def test_should_save_images_in_directory(self, resource_path: list[str]) -> None: + def test_should_save_images_in_directory(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmpdir: image_list.to_jpeg_files(tmpdir) @@ -603,7 +617,10 @@ def test_should_save_images_in_directory(self, resource_path: list[str]) -> None ], ids=["all-jpg-images", "jpg-planes", "jpg-grayscale"], ) - def test_should_save_images_in_directories_for_different_sizes(self, resource_path: list[str]) -> None: + def test_should_save_images_in_directories_for_different_sizes( + self, resource_path: list[str], device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmp_parent_dir: @@ -639,7 +656,8 @@ def test_should_save_images_in_directories_for_different_sizes(self, resource_pa ], ids=["all-jpg-images", "jpg-planes", "jpg-grayscale"], ) - def test_should_save_images_in_files(self, resource_path: list[str]) -> None: + def test_should_save_images_in_files(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmp_parent_dir: @@ -663,6 +681,7 @@ def test_should_save_images_in_files(self, resource_path: list[str]) -> None: assert im_saved.channel == im_loaded.channel +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestToPngFiles: @pytest.mark.parametrize( @@ -670,7 +689,8 @@ class TestToPngFiles: [images_all(), [plane_png_path, plane_jpg_path], [grayscale_png_path, grayscale_png_path]], ids=["all-images", "planes", "grayscale"], ) - def test_should_raise_if_invalid_path(self, resource_path: list[str]) -> None: + def test_should_raise_if_invalid_path(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( ValueError, @@ -683,7 +703,8 @@ def test_should_raise_if_invalid_path(self, resource_path: list[str]) -> None: [images_all(), [plane_png_path, plane_jpg_path], [grayscale_png_path, grayscale_png_path]], ids=["all-images", "planes", "grayscale"], ) - def test_should_save_images_in_directory(self, resource_path: list[str]) -> None: + def test_should_save_images_in_directory(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmpdir: image_list.to_png_files(tmpdir) @@ -698,7 +719,10 @@ def test_should_save_images_in_directory(self, resource_path: list[str]) -> None [images_all(), [plane_png_path, plane_jpg_path], [grayscale_png_path, grayscale_png_path]], ids=["all-images", "planes", "grayscale"], ) - def test_should_save_images_in_directories_for_different_sizes(self, resource_path: list[str]) -> None: + def test_should_save_images_in_directories_for_different_sizes( + self, resource_path: list[str], device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmp_parent_dir: @@ -722,7 +746,8 @@ def test_should_save_images_in_directories_for_different_sizes(self, resource_pa [images_all(), [plane_png_path, plane_jpg_path], [grayscale_png_path, grayscale_png_path]], ids=["all-images", "planes", "grayscale"], ) - def test_should_save_images_in_files(self, resource_path: list[str]) -> None: + def test_should_save_images_in_files(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with tempfile.TemporaryDirectory() as tmp_parent_dir: @@ -741,6 +766,7 @@ def test_should_save_images_in_files(self, resource_path: list[str]) -> None: assert image_list == image_list_loaded +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestShuffleImages: @pytest.mark.parametrize( @@ -748,8 +774,10 @@ class TestShuffleImages: [images_all(), [plane_png_path, plane_jpg_path] * 2], ids=["all-images", "planes"], ) - def test_shuffle_images(self, resource_path: list[str], snapshot_png_image_list: SnapshotAssertion) -> None: - torch.set_default_device(_get_device()) + def test_shuffle_images( + self, resource_path: list[str], snapshot_png_image_list: SnapshotAssertion, device: Device + ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() random.seed(420) @@ -766,6 +794,7 @@ def test_shuffle_images(self, resource_path: list[str], snapshot_png_image_list: @pytest.mark.parametrize("resource_path3", images_all_channel(), ids=images_all_channel_ids()) @pytest.mark.parametrize("resource_path2", images_all_channel(), ids=images_all_channel_ids()) @pytest.mark.parametrize("resource_path1", images_all_channel(), ids=images_all_channel_ids()) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestTransformsEqualImageTransforms: @pytest.mark.parametrize( @@ -826,8 +855,9 @@ def test_all_transform_methods( resource_path1: str, resource_path2: str, resource_path3: str, + device: Device, ) -> None: - torch.set_default_device(torch.device("cpu")) + configure_test_with_device(device) image_list_original = ImageList.from_files( [ resolve_resource_path(resource_path1), @@ -860,6 +890,7 @@ def test_all_transform_methods( [images_all(), [plane_png_path, plane_jpg_path] * 2], ids=["all-images", "planes"], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestTransforms: class TestAddNoise: @pytest.mark.parametrize( @@ -876,9 +907,10 @@ def test_should_add_noise( resource_path: list[str], standard_deviation: float, snapshot_png_image_list: SnapshotAssertion, + device: Device, ) -> None: skip_if_os([os_mac]) - torch.set_default_device(torch.device("cpu")) + configure_test_with_device(device) torch.manual_seed(0) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() @@ -893,19 +925,21 @@ def test_should_add_noise( [images_all(), [plane_png_path, plane_jpg_path] * 2], ids=["SingleSizeImageList", "MultiSizeImageList"], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestErrorsAndWarningsWithoutEmptyImageList: class TestAddImageTensor: - def test_should_raise(self, resource_path: list[str]) -> None: - torch.set_default_device(torch.device("cpu")) + def test_should_raise(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises(DuplicateIndexError, match=r"The index '0' is already in use."): image_list._add_image_tensor(image_list.to_images([0])[0]._image_tensor, 0) class TestEquals: - def test_should_raise(self, resource_path: list[str]) -> None: + def test_should_raise(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) assert (image_list_original.__eq__(image_list_original.to_images([0]))) is NotImplemented @@ -921,7 +955,9 @@ def test_should_warn_if_coordinates_outsize_image( resource_path: list[str], new_x: int, new_y: int, + device: Device, ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) image_blank_tensor = torch.zeros((image_list.number_of_images, image_list.channel, 1, 1)) with pytest.warns( @@ -936,7 +972,9 @@ class TestAdjustColorBalance: def test_should_not_adjust_color_balance_channel_1( self, resource_path: list[str], + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)).change_channel(1) image_list_clone = image_list_original._clone() with pytest.warns( @@ -955,6 +993,7 @@ def test_should_not_adjust_color_balance_channel_1( [images_all(), [plane_png_path, plane_jpg_path] * 2, []], ids=["SingleSizeImageList", "MultiSizeImageList", "EmptyImageList"], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestErrorsAndWarningsWithEmptyImageList: class TestChangeChannel: @@ -964,7 +1003,8 @@ class TestChangeChannel: [-1, 0, 2, 5], ids=["channel-negative-1", "channel-0", "channel-2", "channel-5"], ) - def test_should_raise(self, resource_path: list[str], channel: int) -> None: + def test_should_raise(self, resource_path: list[str], channel: int, device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( ValueError, @@ -974,7 +1014,8 @@ def test_should_raise(self, resource_path: list[str], channel: int) -> None: class TestRemoveImageByIndex: - def test_should_raise_invalid_index(self, resource_path: list[str]) -> None: + def test_should_raise_invalid_index(self, resource_path: list[str], device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises(IndexOutOfBoundsError): image_list.remove_image_by_index(-1) @@ -988,7 +1029,10 @@ class TestRemoveImagesWithSize: [(-10, 10), (10, -10), (-10, -10)], ids=["invalid width", "invalid height", "invalid width and height"], ) - def test_should_raise_negative_size(self, resource_path: list[str], width: int, height: int) -> None: + def test_should_raise_negative_size( + self, resource_path: list[str], width: int, height: int, device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, @@ -1003,7 +1047,10 @@ class TestResize: [(-10, 10), (10, -10), (-10, -10)], ids=["invalid width", "invalid height", "invalid width and height"], ) - def test_should_raise_new_size(self, resource_path: list[str], new_width: int, new_height: int) -> None: + def test_should_raise_new_size( + self, resource_path: list[str], new_width: int, new_height: int, device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, @@ -1018,7 +1065,10 @@ class TestCrop: [(-10, 1), (1, -10), (-10, -1)], ids=["invalid width", "invalid height", "invalid width and height"], ) - def test_should_raise_invalid_size(self, resource_path: list[str], new_width: int, new_height: int) -> None: + def test_should_raise_invalid_size( + self, resource_path: list[str], new_width: int, new_height: int, device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, @@ -1031,7 +1081,10 @@ def test_should_raise_invalid_size(self, resource_path: list[str], new_width: in [(-10, 1), (1, -10), (-10, -1)], ids=["invalid x", "invalid y", "invalid x and y"], ) - def test_should_raise_invalid_coordinates(self, resource_path: list[str], new_x: int, new_y: int) -> None: + def test_should_raise_invalid_coordinates( + self, resource_path: list[str], new_x: int, new_y: int, device: Device + ) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(resource_path)) with pytest.raises( OutOfBoundsError, @@ -1047,10 +1100,9 @@ class TestAddNoise: ids=["sigma below zero"], ) def test_should_raise_standard_deviation( - self, - resource_path: list[str], - standard_deviation: float, + self, resource_path: list[str], standard_deviation: float, device: Device ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises( @@ -1071,7 +1123,9 @@ def test_should_raise( self, resource_path: list[str], factor: float, + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises(OutOfBoundsError, match=r"factor \(=-1\) is not inside \[0, \u221e\)."): @@ -1081,7 +1135,9 @@ def test_should_raise( def test_should_not_brighten( self, resource_path: list[str], + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.warns( @@ -1103,7 +1159,9 @@ def test_should_raise( self, resource_path: list[str], factor: float, + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises(OutOfBoundsError, match=r"factor \(=-1\) is not inside \[0, \u221e\)."): @@ -1113,7 +1171,9 @@ def test_should_raise( def test_should_not_adjust( self, resource_path: list[str], + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.warns( @@ -1135,7 +1195,9 @@ def test_should_raise( self, resource_path: list[str], factor: float, + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises(OutOfBoundsError, match=r"factor \(=-1\) is not inside \[0, \u221e\)."): @@ -1145,7 +1207,9 @@ def test_should_raise( def test_should_not_adjust_color_balance_factor_1( self, resource_path: list[str], + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.warns( @@ -1158,7 +1222,8 @@ def test_should_not_adjust_color_balance_factor_1( class TestBlur: - def test_should_raise_radius_out_of_bounds(self, resource_path: str) -> None: + def test_should_raise_radius_out_of_bounds(self, resource_path: str, device: Device) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises( @@ -1179,7 +1244,8 @@ def test_should_raise_radius_out_of_bounds(self, resource_path: str) -> None: ) assert image_list_original == image_list_clone - def test_should_not_blur(self, resource_path: str) -> None: + def test_should_not_blur(self, resource_path: str, device: Device) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.warns( @@ -1201,7 +1267,9 @@ def test_should_raise( self, resource_path: list[str], factor: float, + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.raises(OutOfBoundsError, match=r"factor \(=-1\) is not inside \[0, \u221e\)."): @@ -1211,7 +1279,9 @@ def test_should_raise( def test_should_not_adjust( self, resource_path: list[str], + device: Device, ) -> None: + configure_test_with_device(device) image_list_original = ImageList.from_files(resolve_resource_path(resource_path)) image_list_clone = image_list_original._clone() with pytest.warns( @@ -1223,6 +1293,7 @@ def test_should_not_adjust( assert image_list_original == image_list_clone +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestSingleSizeImageList: @pytest.mark.parametrize( @@ -1231,7 +1302,9 @@ class TestSingleSizeImageList: torch.ones(4, 1, 1), ], ) - def test_create_from_tensor_3_dim(self, tensor: Tensor) -> None: + def test_create_from_tensor_3_dim(self, tensor: Tensor, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(device) expected_tensor = tensor.unsqueeze(dim=1) image_list = _SingleSizeImageList._create_from_tensor(tensor, list(range(tensor.size(0)))) assert image_list._tensor_positions_to_indices == list(range(tensor.size(0))) @@ -1246,7 +1319,9 @@ def test_create_from_tensor_3_dim(self, tensor: Tensor) -> None: torch.ones(4, 3, 1, 1), ], ) - def test_create_from_tensor_4_dim(self, tensor: Tensor) -> None: + def test_create_from_tensor_4_dim(self, tensor: Tensor, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(device) image_list = _SingleSizeImageList._create_from_tensor(tensor, list(range(tensor.size(0)))) assert image_list._tensor_positions_to_indices == list(range(tensor.size(0))) assert len(image_list) == tensor.size(0) @@ -1255,7 +1330,9 @@ def test_create_from_tensor_4_dim(self, tensor: Tensor) -> None: assert image_list.channel == tensor.size(1) @pytest.mark.parametrize("tensor", [torch.ones(4, 3, 1, 1, 1), torch.ones(4, 3)], ids=["5-dim", "2-dim"]) - def test_should_raise_from_invalid_tensor(self, tensor: Tensor) -> None: + def test_should_raise_from_invalid_tensor(self, tensor: Tensor, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(device) with pytest.raises( ValueError, match=rf"Invalid Tensor. This Tensor requires 3 or 4 dimensions but has {tensor.dim()}", @@ -1268,7 +1345,9 @@ def test_should_raise_from_invalid_tensor(self, tensor: Tensor) -> None: torch.randn(16, 4, 4), ], ) - def test_get_batch_and_iterate_3_dim(self, tensor: Tensor) -> None: + def test_get_batch_and_iterate_3_dim(self, tensor: Tensor, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(device) expected_tensor = tensor.unsqueeze(dim=1) image_list = _SingleSizeImageList._create_from_tensor(tensor, list(range(tensor.size(0)))) batch_size = math.ceil(expected_tensor.size(0) / 1.999) @@ -1297,7 +1376,9 @@ def test_get_batch_and_iterate_3_dim(self, tensor: Tensor) -> None: torch.randn(16, 4, 4, 4), ], ) - def test_get_batch_and_iterate_4_dim(self, tensor: Tensor) -> None: + def test_get_batch_and_iterate_4_dim(self, tensor: Tensor, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(device) image_list = _SingleSizeImageList._create_from_tensor(tensor, list(range(tensor.size(0)))) batch_size = math.ceil(tensor.size(0) / 1.999) assert image_list._get_batch(0, batch_size).size(0) == batch_size @@ -1316,16 +1397,19 @@ def test_get_batch_and_iterate_4_dim(self, tensor: Tensor) -> None: next(iterate_image_list) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestEmptyImageList: - def test_warn_empty_image_list(self) -> None: + def test_warn_empty_image_list(self, device: Device) -> None: + configure_test_with_device(device) with pytest.warns( UserWarning, match=r"You are using an empty ImageList. This method changes nothing if used on an empty ImageList.", ): _EmptyImageList._warn_empty_image_list() - def test_create_image_list_in_empty_image_list(self) -> None: + def test_create_image_list_in_empty_image_list(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(NotImplementedError): _EmptyImageList._create_image_list([], []) @@ -1334,87 +1418,108 @@ def test_create_image_list_in_empty_image_list(self) -> None: [_SingleSizeImageList(), _MultiSizeImageList()], ids=["SingleSizeImageList", "MultiSizeImageList"], ) - def test_create_image_list(self, image_list: ImageList) -> None: + def test_create_image_list(self, image_list: ImageList, device: Device) -> None: + configure_test_with_device(device) assert isinstance(image_list._create_image_list([], []), _EmptyImageList) - def test_from_images(self) -> None: + def test_from_images(self, device: Device) -> None: + configure_test_with_device(device) assert ImageList.from_images([]) == _EmptyImageList() - def test_from_files(self) -> None: + def test_from_files(self, device: Device) -> None: + configure_test_with_device(device) assert ImageList.from_files([]) == _EmptyImageList() with tempfile.TemporaryDirectory() as tmpdir: assert ImageList.from_files(tmpdir) == _EmptyImageList() assert ImageList.from_files([tmpdir]) == _EmptyImageList() - def test_clone(self) -> None: + def test_clone(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList() == _EmptyImageList()._clone() assert _EmptyImageList() is _EmptyImageList()._clone() # Singleton - def test_repr_png(self) -> None: + def test_repr_png(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(TypeError, match=r"You cannot display an empty ImageList"): ImageList.from_images([])._repr_png_() - def test_eq(self) -> None: + def test_eq(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList() == _EmptyImageList() assert _EmptyImageList().__eq__(Table()) is NotImplemented - def test_hash(self) -> None: + def test_hash(self, device: Device) -> None: + configure_test_with_device(device) assert hash(_EmptyImageList()) == hash(_EmptyImageList()) - def test_sizeof(self) -> None: + def test_sizeof(self, device: Device) -> None: + configure_test_with_device(device) assert sys.getsizeof(_EmptyImageList()) >= 0 assert _EmptyImageList().__sizeof__() == 0 - def test_number_of_images(self) -> None: + def test_number_of_images(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().number_of_images == 0 - def test_widths(self) -> None: + def test_widths(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().widths == [] - def test_heights(self) -> None: + def test_heights(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().heights == [] - def test_channel(self) -> None: + def test_channel(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().channel is NotImplemented - def test_sizes(self) -> None: + def test_sizes(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().sizes == [] - def test_number_of_sizes(self) -> None: + def test_number_of_sizes(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().number_of_sizes == 0 - def test_get_image(self) -> None: + def test_get_image(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(IndexOutOfBoundsError, match=r"There is no element at index '0'."): _EmptyImageList().get_image(0) - def test_index(self) -> None: - assert _EmptyImageList().index(Image.from_file(resolve_resource_path(plane_png_path), _get_device())) == [] + def test_index(self, device: Device) -> None: + configure_test_with_device(device) + assert _EmptyImageList().index(Image.from_file(resolve_resource_path(plane_png_path))) == [] - def test_has_image(self) -> None: - assert not _EmptyImageList().has_image(Image.from_file(resolve_resource_path(plane_png_path), _get_device())) - assert Image.from_file(resolve_resource_path(plane_png_path), _get_device()) not in _EmptyImageList() + def test_has_image(self, device: Device) -> None: + configure_test_with_device(device) + assert not _EmptyImageList().has_image(Image.from_file(resolve_resource_path(plane_png_path))) + assert Image.from_file(resolve_resource_path(plane_png_path)) not in _EmptyImageList() - def test_to_jpeg_file(self) -> None: + def test_to_jpeg_file(self, device: Device) -> None: + configure_test_with_device(device) with pytest.warns(UserWarning, match="You are using an empty ImageList. No files will be saved."): _EmptyImageList().to_jpeg_files("path") - def test_to_png_file(self) -> None: + def test_to_png_file(self, device: Device) -> None: + configure_test_with_device(device) with pytest.warns(UserWarning, match="You are using an empty ImageList. No files will be saved."): _EmptyImageList().to_png_files("path") - def test_to_images(self) -> None: + def test_to_images(self, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList().to_images() == [] assert _EmptyImageList().to_images([0]) == [] @pytest.mark.parametrize("resource_path", images_all(), ids=images_all_ids()) - def test_add_image_tensor(self, resource_path: str) -> None: - torch.set_default_device(_get_device()) + def test_add_image_tensor(self, resource_path: str, device: Device) -> None: + configure_test_with_device(device) assert _EmptyImageList()._add_image_tensor( - Image.from_file(resolve_resource_path(resource_path), _get_device())._image_tensor, + Image.from_file(resolve_resource_path(resource_path))._image_tensor, 0, ) == ImageList.from_files(resolve_resource_path(resource_path)) - def test_remove_image_by_index(self) -> None: + def test_remove_image_by_index(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(IndexOutOfBoundsError): _EmptyImageList().remove_image_by_index(0) @@ -1483,8 +1588,8 @@ def test_remove_image_by_index(self) -> None: "find_edges", ], ) - def test_transform_is_still_empty_image_list(self, method: str, attributes: list) -> None: - torch.set_default_device(torch.device("cpu")) + def test_transform_is_still_empty_image_list(self, method: str, attributes: list, device: Device) -> None: + configure_test_with_device(device) image_list = _EmptyImageList() with pytest.warns( diff --git a/tests/safeds/data/image/typing/test_image_size.py b/tests/safeds/data/image/typing/test_image_size.py index d54174a2c..1c89b46f1 100644 --- a/tests/safeds/data/image/typing/test_image_size.py +++ b/tests/safeds/data/image/typing/test_image_size.py @@ -14,7 +14,7 @@ images_all_ids, plane_png_path, resolve_resource_path, - skip_if_device_not_available, + configure_test_with_device, ) @@ -23,8 +23,8 @@ class TestFromImage: @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @pytest.mark.parametrize("resource_path", images_all(), ids=images_all_ids()) def test_should_create(self, resource_path: str, device: Device) -> None: - skip_if_device_not_available(device) - image = Image.from_file(resolve_resource_path(resource_path), device) + configure_test_with_device(device) + image = Image.from_file(resolve_resource_path(resource_path)) expected_image_size = ImageSize(image.width, image.height, image.channel) assert ImageSize.from_image(image) == expected_image_size diff --git a/tests/safeds/data/labeled/containers/_tabular_dataset/test_into_dataloader.py b/tests/safeds/data/labeled/containers/_tabular_dataset/test_into_dataloader.py index a2a217401..5ff34f91c 100644 --- a/tests/safeds/data/labeled/containers/_tabular_dataset/test_into_dataloader.py +++ b/tests/safeds/data/labeled/containers/_tabular_dataset/test_into_dataloader.py @@ -1,7 +1,12 @@ import pytest +from torch.types import Device + +from safeds._config import _get_device from safeds.data.tabular.containers import Table from torch.utils.data import DataLoader +from tests.helpers import get_devices, get_devices_ids, configure_test_with_device + @pytest.mark.parametrize( ("data", "target_name", "extra_names"), @@ -21,11 +26,17 @@ "test", ], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_should_create_dataloader( data: dict[str, list[int]], target_name: str, extra_names: list[str] | None, + device: Device, ) -> None: + configure_test_with_device(device) tabular_dataset = Table.from_dict(data).to_tabular_dataset(target_name, extra_names) data_loader = tabular_dataset._into_dataloader_with_classes(1, 2) + batch = next(iter(data_loader)) + assert batch[0].device == _get_device() + assert batch[1].device == _get_device() assert isinstance(data_loader, DataLoader) diff --git a/tests/safeds/data/labeled/containers/_time_series_dataset/test_into_dataloader.py b/tests/safeds/data/labeled/containers/_time_series_dataset/test_into_dataloader.py index 8a9cbb393..66816a747 100644 --- a/tests/safeds/data/labeled/containers/_time_series_dataset/test_into_dataloader.py +++ b/tests/safeds/data/labeled/containers/_time_series_dataset/test_into_dataloader.py @@ -1,8 +1,16 @@ +from typing import Type + import pytest +from torch.types import Device + +from safeds._config import _get_device from safeds.data.tabular.containers import Table from safeds.data.labeled.containers import TimeSeriesDataset from torch.utils.data import DataLoader +from safeds.exceptions import OutOfBoundsError +from tests.helpers import get_devices, get_devices_ids, configure_test_with_device + @pytest.mark.parametrize( ("data", "target_name", "time_name", "extra_names"), @@ -23,14 +31,55 @@ "test", ], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_should_create_dataloader( data: dict[str, list[int]], target_name: str, time_name: str, extra_names: list[str] | None, + device: Device, ) -> None: + configure_test_with_device(device) tabular_dataset = Table.from_dict(data).to_time_series_dataset(target_name, time_name, extra_names) data_loader = tabular_dataset._into_dataloader_with_window(1, 1, 1) + batch = next(iter(data_loader)) + assert batch[0].device == _get_device() + assert batch[1].device == _get_device() + assert isinstance(data_loader, DataLoader) + + +@pytest.mark.parametrize( + ("data", "target_name", "time_name", "extra_names"), + [ + ( + { + "A": [1, 4, 3], + "B": [2, 5, 4], + "C": [3, 6, 5], + "T": [0, 1, 6], + }, + "T", + "B", + [], + ), + ], + ids=[ + "test", + ], +) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_should_create_dataloader_predict( + data: dict[str, list[int]], + target_name: str, + time_name: str, + extra_names: list[str] | None, + device: Device, +) -> None: + configure_test_with_device(device) + tabular_dataset = Table.from_dict(data).to_time_series_dataset(target_name, time_name, extra_names) + data_loader = tabular_dataset._into_dataloader_with_window_predict(1, 1, 1) + batch = next(iter(data_loader)) + assert batch[0].device == _get_device() assert isinstance(data_loader, DataLoader) @@ -62,8 +111,8 @@ def test_should_create_dataloader( ).to_time_series_dataset("T", "B"), 1, 0, - ValueError, - r"forecast_horizon must be greater than or equal to 1", + OutOfBoundsError, + r"forecast_horizon \(=0\) is not inside \[1, \u221e\).", ), ( Table( @@ -76,8 +125,8 @@ def test_should_create_dataloader( ).to_time_series_dataset("T", "B"), 0, 1, - ValueError, - r"window_size must be greater than or equal to 1", + OutOfBoundsError, + r"window_size \(=0\) is not inside \[1, \u221e\).", ), ], ids=[ @@ -86,12 +135,83 @@ def test_should_create_dataloader( "window_size", ], ) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_should_create_dataloader_invalid( data: TimeSeriesDataset, window_size: int, forecast_horizon: int, - error_type: ValueError, + error_type: Type[ValueError], error_msg: str, + device: Device, ) -> None: + configure_test_with_device(device) with pytest.raises(error_type, match=error_msg): data._into_dataloader_with_window(window_size=window_size, forecast_horizon=forecast_horizon, batch_size=1) + + +@pytest.mark.parametrize( + ("data", "window_size", "forecast_horizon", "error_type", "error_msg"), + [ + ( + Table( + { + "A": [1, 4], + "B": [2, 5], + "C": [3, 6], + "T": [0, 1], + } + ).to_time_series_dataset("T", "B"), + 1, + 2, + ValueError, + r"Can not create windows with window size less then forecast horizon \+ window_size", + ), + ( + Table( + { + "A": [1, 4], + "B": [2, 5], + "C": [3, 6], + "T": [0, 1], + } + ).to_time_series_dataset("T", "B"), + 1, + 0, + OutOfBoundsError, + r"forecast_horizon \(=0\) is not inside \[1, \u221e\).", + ), + ( + Table( + { + "A": [1, 4], + "B": [2, 5], + "C": [3, 6], + "T": [0, 1], + } + ).to_time_series_dataset("T", "B"), + 0, + 1, + OutOfBoundsError, + r"window_size \(=0\) is not inside \[1, \u221e\).", + ), + ], + ids=[ + "forecast_and_window", + "forecast", + "window_size", + ], +) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_should_create_dataloader_predict_invalid( + data: TimeSeriesDataset, + window_size: int, + forecast_horizon: int, + error_type: Type[ValueError], + error_msg: str, + device: Device, +) -> None: + configure_test_with_device(device) + with pytest.raises(error_type, match=error_msg): + data._into_dataloader_with_window_predict( + window_size=window_size, forecast_horizon=forecast_horizon, batch_size=1 + ) diff --git a/tests/safeds/data/labeled/containers/test_image_dataset.py b/tests/safeds/data/labeled/containers/test_image_dataset.py index 8369acc30..f11af31e1 100644 --- a/tests/safeds/data/labeled/containers/test_image_dataset.py +++ b/tests/safeds/data/labeled/containers/test_image_dataset.py @@ -5,6 +5,9 @@ import pytest import torch +from torch.types import Device + +from safeds._config import _get_device from safeds.data.image.containers import ImageList from safeds.data.image.containers._empty_image_list import _EmptyImageList from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList @@ -21,11 +24,20 @@ ) from torch import Tensor -from tests.helpers import images_all, plane_png_path, resolve_resource_path, white_square_png_path +from tests.helpers import ( + images_all, + plane_png_path, + resolve_resource_path, + white_square_png_path, + get_devices, + get_devices_ids, + configure_test_with_device, +) T = TypeVar("T", Column, Table, ImageList) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestImageDatasetInit: @pytest.mark.parametrize( @@ -89,178 +101,176 @@ class TestImageDatasetInit: ], ) def test_should_raise_with_invalid_data( - self, - input_data: ImageList, - output_data: T, - error: type[Exception], - error_msg: str, + self, input_data: ImageList, output_data: T, error: type[Exception], error_msg: str, device: Device ) -> None: + configure_test_with_device(device) with pytest.raises(error, match=error_msg): ImageDataset(input_data, output_data) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestLength: - def test_should_return_length(self) -> None: + def test_should_return_length(self, device: Device) -> None: + configure_test_with_device(device) image_dataset = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])) assert len(image_dataset) == 1 + assert image_dataset._input._tensor.device == _get_device() + assert image_dataset._output._tensor.device == _get_device() +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestEq: @pytest.mark.parametrize( - ("image_dataset1", "image_dataset2"), + "image_dataset_output", [ - ( - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ), - ( - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ), - ( - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), - ), + Column("images", [1]), + Table({"images": [1]}), + plane_png_path, ], ) - def test_should_be_equal(self, image_dataset1: ImageDataset, image_dataset2: ImageDataset) -> None: + def test_should_be_equal(self, image_dataset_output: str | Column | Table, device: Device) -> None: + configure_test_with_device(device) + image_dataset1 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset_output)) if isinstance(image_dataset_output, str) else image_dataset_output) # type: ignore[type-var] + image_dataset2 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset_output)) if isinstance(image_dataset_output, str) else image_dataset_output) # type: ignore[type-var] + assert image_dataset1 is not image_dataset2 assert image_dataset1 == image_dataset2 + assert image_dataset1._input._tensor.device == _get_device() + assert image_dataset1._output._tensor.device == _get_device() + assert image_dataset2._input._tensor.device == _get_device() + assert image_dataset2._output._tensor.device == _get_device() @pytest.mark.parametrize( - "image_dataset1", + "image_dataset1_output", [ - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), + Column("images", [1]), + Table({"images": [1]}), + plane_png_path, ], ) @pytest.mark.parametrize( - "image_dataset2", + ("image_dataset2_input", "image_dataset2_output"), [ - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("ims", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"ims": [1]})), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [0])), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - Table({"images": [0], "others": [1]}), - ), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(white_square_png_path)), - ), - ImageDataset(ImageList.from_files(resolve_resource_path(white_square_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(white_square_png_path)), Table({"images": [1]})), - ImageDataset( - ImageList.from_files(resolve_resource_path(white_square_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), + (plane_png_path, Column("ims", [1])), + (plane_png_path, Table({"ims": [1]})), + (plane_png_path, Column("images", [0])), + (plane_png_path, Table({"images": [0], "others": [1]})), + (plane_png_path, white_square_png_path), + (white_square_png_path, Column("images", [1])), + (white_square_png_path, Table({"images": [1]})), + (white_square_png_path, plane_png_path), ], ) - def test_should_not_be_equal(self, image_dataset1: ImageDataset, image_dataset2: ImageDataset) -> None: + def test_should_not_be_equal( + self, + image_dataset1_output: str | Column | Table, + image_dataset2_input: str, + image_dataset2_output: str | Column | Table, + device: Device, + ) -> None: + configure_test_with_device(device) + image_dataset1 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset1_output)) if isinstance(image_dataset1_output, str) else image_dataset1_output) # type: ignore[type-var] + image_dataset2 = ImageDataset(ImageList.from_files(resolve_resource_path(image_dataset2_input)), ImageList.from_files(resolve_resource_path(image_dataset2_output)) if isinstance(image_dataset2_output, str) else image_dataset2_output) # type: ignore[type-var] assert image_dataset1 != image_dataset2 + assert image_dataset1._input._tensor.device == _get_device() + assert image_dataset1._output._tensor.device == _get_device() + assert image_dataset2._input._tensor.device == _get_device() + assert image_dataset2._output._tensor.device == _get_device() - def test_should_be_not_implemented(self) -> None: + def test_should_be_not_implemented(self, device: Device) -> None: + configure_test_with_device(device) image_dataset = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])) other = Table() assert image_dataset.__eq__(other) is NotImplemented +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestHash: @pytest.mark.parametrize( - ("image_dataset1", "image_dataset2"), + "image_dataset_output", [ - ( - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ), - ( - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ), - ( - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), - ), + Column("images", [1]), + Table({"images": [1]}), + plane_png_path, ], ) - def test_hash_should_be_equal(self, image_dataset1: ImageDataset, image_dataset2: ImageDataset) -> None: + def test_hash_should_be_equal(self, image_dataset_output: str | Column | Table, device: Device) -> None: + configure_test_with_device(device) + image_dataset1 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset_output)) if isinstance(image_dataset_output, str) else image_dataset_output) # type: ignore[type-var] + image_dataset2 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset_output)) if isinstance(image_dataset_output, str) else image_dataset_output) # type: ignore[type-var] + assert image_dataset1 is not image_dataset2 assert hash(image_dataset1) == hash(image_dataset2) + assert image_dataset1._input._tensor.device == _get_device() + assert image_dataset1._output._tensor.device == _get_device() + assert image_dataset2._input._tensor.device == _get_device() + assert image_dataset2._output._tensor.device == _get_device() @pytest.mark.parametrize( - "image_dataset1", + "image_dataset1_output", [ - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), + Column("images", [1]), + Table({"images": [1]}), + plane_png_path, ], ) @pytest.mark.parametrize( - "image_dataset2", + ("image_dataset2_input", "image_dataset2_output"), [ - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("ims", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"ims": [1]})), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [0])), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - Table({"images": [0], "others": [1]}), - ), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(white_square_png_path)), - ), - ImageDataset(ImageList.from_files(resolve_resource_path(white_square_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(white_square_png_path)), Table({"images": [1]})), - ImageDataset( - ImageList.from_files(resolve_resource_path(white_square_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), + (plane_png_path, Column("ims", [1])), + (plane_png_path, Table({"ims": [1]})), + (plane_png_path, Column("images", [0])), + (plane_png_path, Table({"images": [0], "others": [1]})), + (plane_png_path, white_square_png_path), + (white_square_png_path, Column("images", [1])), + (white_square_png_path, Table({"images": [1]})), + (white_square_png_path, plane_png_path), ], ) - def test_hash_should_not_be_equal(self, image_dataset1: ImageDataset, image_dataset2: ImageDataset) -> None: + def test_hash_should_not_be_equal( + self, + image_dataset1_output: str | Column | Table, + image_dataset2_input: str, + image_dataset2_output: str | Column | Table, + device: Device, + ) -> None: + configure_test_with_device(device) + image_dataset1 = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset1_output)) if isinstance(image_dataset1_output, str) else image_dataset1_output) # type: ignore[type-var] + image_dataset2 = ImageDataset(ImageList.from_files(resolve_resource_path(image_dataset2_input)), ImageList.from_files(resolve_resource_path(image_dataset2_output)) if isinstance(image_dataset2_output, str) else image_dataset2_output) # type: ignore[type-var] assert hash(image_dataset1) != hash(image_dataset2) + assert image_dataset1._input._tensor.device == _get_device() + assert image_dataset1._output._tensor.device == _get_device() + assert image_dataset2._input._tensor.device == _get_device() + assert image_dataset2._output._tensor.device == _get_device() +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestSizeOf: @pytest.mark.parametrize( - "image_dataset", + "image_dataset_output", [ - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Column("images", [1])), - ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), Table({"images": [1]})), - ImageDataset( - ImageList.from_files(resolve_resource_path(plane_png_path)), - ImageList.from_files(resolve_resource_path(plane_png_path)), - ), + Column("images", [1]), + Table({"images": [1]}), + plane_png_path, ], ) - def test_should_size_be_greater_than_normal_object(self, image_dataset: ImageDataset) -> None: + def test_should_size_be_greater_than_normal_object( + self, image_dataset_output: str | Column | Table, device: Device + ) -> None: + configure_test_with_device(device) + image_dataset = ImageDataset(ImageList.from_files(resolve_resource_path(plane_png_path)), ImageList.from_files(resolve_resource_path(image_dataset_output)) if isinstance(image_dataset_output, str) else image_dataset_output) # type: ignore[type-var] assert sys.getsizeof(image_dataset) > sys.getsizeof(object()) + assert image_dataset._input._tensor.device == _get_device() + assert image_dataset._output._tensor.device == _get_device() +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestShuffle: - def test_should_be_different_order(self) -> None: + def test_should_be_different_order(self, device: Device) -> None: + configure_test_with_device(device) torch.manual_seed(1234) image_list = ImageList.from_files(resolve_resource_path(images_all())).resize(10, 10) image_dataset = ImageDataset(image_list, Column("images", images_all())) @@ -269,8 +279,11 @@ def test_should_be_different_order(self) -> None: batch_shuffled = image_dataset_shuffled._get_batch(0, len(image_dataset)) assert not torch.all(torch.eq(batch[0], batch_shuffled[0])) assert not torch.all(torch.eq(batch[1], batch_shuffled[1])) + assert image_dataset._input._tensor.device == _get_device() + assert image_dataset._output._tensor.device == _get_device() +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestBatch: @pytest.mark.parametrize( @@ -283,22 +296,34 @@ class TestBatch: (4, math.ceil(len(images_all()) / 4)), ], ) - def test_should_raise_index_out_of_bounds_error(self, batch_number: int, batch_size: int) -> None: + def test_should_raise_index_out_of_bounds_error(self, batch_number: int, batch_size: int, device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(images_all())).resize(10, 10) image_dataset = ImageDataset(image_list, Column("images", images_all())) with pytest.raises(IndexOutOfBoundsError): image_dataset._get_batch(batch_number, batch_size) - def test_should_raise_out_of_bounds_error(self) -> None: + def test_should_raise_out_of_bounds_error(self, device: Device) -> None: + configure_test_with_device(device) image_list = ImageList.from_files(resolve_resource_path(images_all())).resize(10, 10) image_dataset = ImageDataset(image_list, Column("images", images_all())) with pytest.raises(OutOfBoundsError): image_dataset._get_batch(0, -1) + def test_get_batch_device(self, device: Device) -> None: + configure_test_with_device(device) + image_list = ImageList.from_files(resolve_resource_path(images_all())).resize(10, 10) + image_dataset = ImageDataset(image_list, Column("images", images_all())) + batch = image_dataset._get_batch(0) + assert batch[0].device == _get_device() + assert batch[1].device == _get_device() + +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestTableAsTensor: - def test_should_raise_if_not_one_hot_encoded(self) -> None: + def test_should_raise_if_not_one_hot_encoded(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises( ValueError, match=r"The given table is not correctly one hot encoded as it contains rows that have a sum not equal to 1.", @@ -313,14 +338,18 @@ def test_should_raise_if_not_one_hot_encoded(self) -> None: (torch.randn(10, 10), r"Tensor and column_names have different amounts of classes \(10!=2\)."), ], ) - def test_should_raise_from_tensor(self, tensor: Tensor, error_msg: str) -> None: + def test_should_raise_from_tensor(self, tensor: Tensor, error_msg: str, device: Device) -> None: + configure_test_with_device(device) + tensor = tensor.to(_get_device()) with pytest.raises(ValueError, match=error_msg): _TableAsTensor._from_tensor(tensor, ["a", "b"]) - def test_eq_should_be_not_implemented(self) -> None: + def test_eq_should_be_not_implemented(self, device: Device) -> None: + configure_test_with_device(device) assert _TableAsTensor(Table()).__eq__(Table()) is NotImplemented +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestColumnAsTensor: @pytest.mark.parametrize( @@ -353,14 +382,19 @@ def test_should_raise_from_tensor( one_hot_encoder: OneHotEncoder, error: type[Exception], error_msg: str, + device: Device, ) -> None: + configure_test_with_device(device) + tensor = tensor.to(_get_device()) with pytest.raises(error, match=error_msg): _ColumnAsTensor._from_tensor(tensor, "a", one_hot_encoder) - def test_eq_should_be_not_implemented(self) -> None: + def test_eq_should_be_not_implemented(self, device: Device) -> None: + configure_test_with_device(device) assert _ColumnAsTensor(Column("column", [1])).__eq__(Table()) is NotImplemented - def test_should_not_warn(self) -> None: + def test_should_not_warn(self, device: Device) -> None: + configure_test_with_device(device) with warnings.catch_warnings(): warnings.filterwarnings("error") _ColumnAsTensor(Column("column", [1, 2, 3])) diff --git a/tests/safeds/ml/nn/test_cnn_workflow.py b/tests/safeds/ml/nn/test_cnn_workflow.py index 164440b7e..95caf6781 100644 --- a/tests/safeds/ml/nn/test_cnn_workflow.py +++ b/tests/safeds/ml/nn/test_cnn_workflow.py @@ -3,6 +3,8 @@ import pytest import torch + +from safeds._config import _get_device from safeds.data.image.containers import ImageList from safeds.data.labeled.containers import ImageDataset from safeds.data.tabular.containers import Column, Table @@ -23,7 +25,7 @@ from syrupy import SnapshotAssertion from torch.types import Device -from tests.helpers import device_cpu, device_cuda, images_all, resolve_resource_path, skip_if_device_not_available +from tests.helpers import device_cpu, device_cuda, images_all, resolve_resource_path, configure_test_with_device if TYPE_CHECKING: from safeds.ml.nn import Layer @@ -63,8 +65,7 @@ def test_should_train_and_predict_model( prediction_label: list[str], device: Device, ) -> None: - skip_if_device_not_available(device) - torch.set_default_device(device) + configure_test_with_device(device) torch.manual_seed(seed) image_list, filenames = ImageList.from_files(resolve_resource_path(images_all()), return_filenames=True) @@ -95,6 +96,7 @@ def test_should_train_and_predict_model( ).item() prediction: ImageDataset = nn.predict(image_dataset.get_input()) assert one_hot_encoder.inverse_transform(prediction.get_output()) == Table({"class": prediction_label}) + assert prediction._output._tensor.device == _get_device() class TestImageToColumnClassifier: @@ -131,8 +133,7 @@ def test_should_train_and_predict_model( prediction_label: list[str], device: Device, ) -> None: - skip_if_device_not_available(device) - torch.set_default_device(device) + configure_test_with_device(device) torch.manual_seed(seed) image_list, filenames = ImageList.from_files(resolve_resource_path(images_all()), return_filenames=True) @@ -162,6 +163,7 @@ def test_should_train_and_predict_model( ).item() prediction: ImageDataset = nn.predict(image_dataset.get_input()) assert prediction.get_output() == Column("class", prediction_label) + assert prediction._output._tensor.device == _get_device() class TestImageToImageRegressor: @@ -182,8 +184,7 @@ def test_should_train_and_predict_model( snapshot_png_image_list: SnapshotAssertion, device: Device, ) -> None: - skip_if_device_not_available(device) - torch.set_default_device(device) + configure_test_with_device(device) torch.manual_seed(seed) image_list = ImageList.from_files(resolve_resource_path(images_all())) @@ -212,3 +213,4 @@ def test_should_train_and_predict_model( ).item() prediction = nn.predict(image_dataset.get_input()) assert isinstance(prediction.get_output(), ImageList) + assert prediction._output._tensor.device == _get_device() diff --git a/tests/safeds/ml/nn/test_forward_workflow.py b/tests/safeds/ml/nn/test_forward_workflow.py index 87a282383..1256c94f5 100644 --- a/tests/safeds/ml/nn/test_forward_workflow.py +++ b/tests/safeds/ml/nn/test_forward_workflow.py @@ -1,3 +1,7 @@ +import pytest +from torch.types import Device + +from safeds._config import _get_device from safeds.data.tabular.containers import Table from safeds.data.tabular.transformation import StandardScaler from safeds.ml.nn import ( @@ -7,10 +11,13 @@ OutputConversionTable, ) -from tests.helpers import resolve_resource_path +from tests.helpers import resolve_resource_path, get_devices, get_devices_ids, configure_test_with_device + +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_forward_model(device: Device) -> None: + configure_test_with_device(device) -def test_lstm_model() -> None: # Create a DataFrame _inflation_path = "_datas/US_Inflation_rates.csv" table_1 = Table.from_csv_file( @@ -32,4 +39,4 @@ def test_lstm_model() -> None: fitted_model = model.fit(train_table.to_tabular_dataset("target"), epoch_size=1, learning_rate=0.01) fitted_model.predict(test_table.keep_only_columns(["value"])) - assert True + assert model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() diff --git a/tests/safeds/ml/nn/test_lstm_workflow.py b/tests/safeds/ml/nn/test_lstm_workflow.py index 33e3f1b49..256602360 100644 --- a/tests/safeds/ml/nn/test_lstm_workflow.py +++ b/tests/safeds/ml/nn/test_lstm_workflow.py @@ -1,3 +1,7 @@ +import pytest +from torch.types import Device + +from safeds._config import _get_device from safeds.data.tabular.containers import Table from safeds.data.tabular.transformation import RangeScaler from safeds.ml.nn import ( @@ -8,10 +12,13 @@ OutputConversionTimeSeries, ) -from tests.helpers import resolve_resource_path +from tests.helpers import resolve_resource_path, get_devices, get_devices_ids, configure_test_with_device + +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) +def test_lstm_model(device: Device) -> None: + configure_test_with_device(device) -def test_lstm_model() -> None: # Create a DataFrame _inflation_path = "_datas/US_Inflation_rates.csv" table = Table.from_csv_file(path=resolve_resource_path(_inflation_path)) @@ -27,3 +34,4 @@ def test_lstm_model() -> None: trained_model = model.fit(train_table.to_time_series_dataset("value", "date"), epoch_size=1) trained_model.predict(test_table.to_time_series_dataset("value", "date")) + assert model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() diff --git a/tests/safeds/ml/nn/test_model.py b/tests/safeds/ml/nn/test_model.py index d4a72d492..32fe4b733 100644 --- a/tests/safeds/ml/nn/test_model.py +++ b/tests/safeds/ml/nn/test_model.py @@ -1,4 +1,6 @@ import pytest +from torch.types import Device + from safeds.data.image.typing import ImageSize from safeds.data.labeled.containers import TabularDataset from safeds.data.tabular.containers import Table @@ -18,8 +20,8 @@ InputConversion, InputConversionImage, InputConversionTable, - LSTMLayer, Layer, + LSTMLayer, MaxPooling2DLayer, NeuralNetworkClassifier, NeuralNetworkRegressor, @@ -29,8 +31,10 @@ OutputConversionTable, ) from safeds.ml.nn._output_conversion_image import OutputConversionImageToColumn +from tests.helpers import get_devices, get_devices_ids, configure_test_with_device +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestClassificationModel: @pytest.mark.parametrize( "epoch_size", @@ -39,7 +43,8 @@ class TestClassificationModel: ], ids=["epoch_size_out_of_bounds"], ) - def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int) -> None: + def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int, device: Device) -> None: + configure_test_with_device(device) with pytest.raises( OutOfBoundsError, match=rf"epoch_size \(={epoch_size}\) is not inside \[1, \u221e\)\.", @@ -60,7 +65,8 @@ def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int) -> None ], ids=["batch_size_out_of_bounds"], ) - def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int) -> None: + def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int, device: Device) -> None: + configure_test_with_device(device) with pytest.raises( OutOfBoundsError, match=rf"batch_size \(={batch_size}\) is not inside \[1, \u221e\)\.", @@ -74,7 +80,8 @@ def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int) -> None batch_size=batch_size, ) - def test_should_raise_if_fit_function_returns_wrong_datatype(self) -> None: + def test_should_raise_if_fit_function_returns_wrong_datatype(self, device: Device) -> None: + configure_test_with_device(device) fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=1)], @@ -92,7 +99,8 @@ def test_should_raise_if_fit_function_returns_wrong_datatype(self) -> None: ], ids=["one", "two"], ) - def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_size: int) -> None: + def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_size: int, device: Device) -> None: + configure_test_with_device(device) fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=1)], @@ -115,7 +123,9 @@ def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_siz def test_should_raise_if_predict_function_returns_wrong_datatype_for_multiclass_classification( self, batch_size: int, + device: Device, ) -> None: + configure_test_with_device(device) fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=3)], @@ -135,7 +145,8 @@ def test_should_raise_if_predict_function_returns_wrong_datatype_for_multiclass_ predictions = fitted_model.predict(Table.from_dict({"b": [1, 4, 124]})) assert isinstance(predictions, TabularDataset) - def test_should_raise_if_model_has_not_been_fitted(self) -> None: + def test_should_raise_if_model_has_not_been_fitted(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(ModelNotFittedError, match="The model has not been fitted yet."): NeuralNetworkClassifier( InputConversionTable(), @@ -145,7 +156,8 @@ def test_should_raise_if_model_has_not_been_fitted(self) -> None: Table.from_dict({"a": [1]}), ) - def test_should_raise_if_is_fitted_is_set_correctly_for_binary_classification(self) -> None: + def test_should_raise_if_is_fitted_is_set_correctly_for_binary_classification(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -167,7 +179,8 @@ def test_should_raise_if_is_fitted_is_set_correctly_for_binary_classification(se assert model.is_fitted assert model_2.is_fitted - def test_should_raise_if_is_fitted_is_set_correctly_for_multiclass_classification(self) -> None: + def test_should_raise_if_is_fitted_is_set_correctly_for_multiclass_classification(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], @@ -189,7 +202,8 @@ def test_should_raise_if_is_fitted_is_set_correctly_for_multiclass_classificatio assert model.is_fitted assert model_2.is_fitted - def test_should_raise_if_test_features_mismatch(self) -> None: + def test_should_raise_if_test_features_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], @@ -206,7 +220,8 @@ def test_should_raise_if_test_features_mismatch(self) -> None: Table.from_dict({"a": [1], "c": [2]}), ) - def test_should_raise_if_train_features_mismatch(self) -> None: + def test_should_raise_if_train_features_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=1)], @@ -221,7 +236,8 @@ def test_should_raise_if_train_features_mismatch(self) -> None: ) learned_model.fit(Table.from_dict({"k": [0.1, 0, 0.2], "l": [0, 0.15, 0.5]}).to_tabular_dataset("k")) - def test_should_raise_if_table_size_and_input_size_mismatch(self) -> None: + def test_should_raise_if_table_size_and_input_size_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], @@ -234,7 +250,8 @@ def test_should_raise_if_table_size_and_input_size_mismatch(self) -> None: Table.from_dict({"a": [1, 0, 2], "b": [0, 15, 5], "c": [3, 33, 333]}).to_tabular_dataset("a"), ) - def test_should_raise_if_fit_doesnt_batch_callback(self) -> None: + def test_should_raise_if_fit_doesnt_batch_callback(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -256,7 +273,8 @@ def callback_was_called(self) -> bool: assert obj.callback_was_called() is True - def test_should_raise_if_fit_doesnt_epoch_callback(self) -> None: + def test_should_raise_if_fit_doesnt_epoch_callback(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -475,11 +493,14 @@ def test_should_raise_if_model_has_invalid_structure( layers: list[Layer], output_conversion: OutputConversion, error_msg: str, + device: Device, ) -> None: + configure_test_with_device(device) with pytest.raises(InvalidModelStructureError, match=error_msg): NeuralNetworkClassifier(input_conversion, layers, output_conversion) +@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestRegressionModel: @pytest.mark.parametrize( "epoch_size", @@ -488,7 +509,8 @@ class TestRegressionModel: ], ids=["epoch_size_out_of_bounds"], ) - def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int) -> None: + def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int, device: Device) -> None: + configure_test_with_device(device) with pytest.raises( OutOfBoundsError, match=rf"epoch_size \(={epoch_size}\) is not inside \[1, \u221e\)\.", @@ -509,7 +531,8 @@ def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int) -> None ], ids=["batch_size_out_of_bounds"], ) - def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int) -> None: + def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int, device: Device) -> None: + configure_test_with_device(device) with pytest.raises( OutOfBoundsError, match=rf"batch_size \(={batch_size}\) is not inside \[1, \u221e\)\.", @@ -531,7 +554,8 @@ def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int) -> None ], ids=["one", "two"], ) - def test_should_raise_if_fit_function_returns_wrong_datatype(self, batch_size: int) -> None: + def test_should_raise_if_fit_function_returns_wrong_datatype(self, batch_size: int, device: Device) -> None: + configure_test_with_device(device) fitted_model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -550,7 +574,8 @@ def test_should_raise_if_fit_function_returns_wrong_datatype(self, batch_size: i ], ids=["one", "two"], ) - def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_size: int) -> None: + def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_size: int, device: Device) -> None: + configure_test_with_device(device) fitted_model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -562,7 +587,8 @@ def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_siz predictions = fitted_model.predict(Table.from_dict({"b": [5, 6, 7]})) assert isinstance(predictions, TabularDataset) - def test_should_raise_if_model_has_not_been_fitted(self) -> None: + def test_should_raise_if_model_has_not_been_fitted(self, device: Device) -> None: + configure_test_with_device(device) with pytest.raises(ModelNotFittedError, match="The model has not been fitted yet."): NeuralNetworkRegressor( InputConversionTable(), @@ -572,7 +598,8 @@ def test_should_raise_if_model_has_not_been_fitted(self) -> None: Table.from_dict({"a": [1]}), ) - def test_should_raise_if_is_fitted_is_set_correctly(self) -> None: + def test_should_raise_if_is_fitted_is_set_correctly(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -584,7 +611,8 @@ def test_should_raise_if_is_fitted_is_set_correctly(self) -> None: ) assert model.is_fitted - def test_should_raise_if_test_features_mismatch(self) -> None: + def test_should_raise_if_test_features_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -601,7 +629,8 @@ def test_should_raise_if_test_features_mismatch(self) -> None: Table.from_dict({"a": [1], "c": [2]}), ) - def test_should_raise_if_train_features_mismatch(self) -> None: + def test_should_raise_if_train_features_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -618,7 +647,8 @@ def test_should_raise_if_train_features_mismatch(self) -> None: Table.from_dict({"k": [1, 0, 2], "l": [0, 15, 5]}).to_tabular_dataset("l"), ) - def test_should_raise_if_table_size_and_input_size_mismatch(self) -> None: + def test_should_raise_if_table_size_and_input_size_mismatch(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], @@ -631,7 +661,8 @@ def test_should_raise_if_table_size_and_input_size_mismatch(self) -> None: Table.from_dict({"a": [1, 0, 2], "b": [0, 15, 5], "c": [3, 33, 333]}).to_tabular_dataset("a"), ) - def test_should_raise_if_fit_doesnt_batch_callback(self) -> None: + def test_should_raise_if_fit_doesnt_batch_callback(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -653,7 +684,8 @@ def callback_was_called(self) -> bool: assert obj.callback_was_called() is True - def test_should_raise_if_fit_doesnt_epoch_callback(self) -> None: + def test_should_raise_if_fit_doesnt_epoch_callback(self, device: Device) -> None: + configure_test_with_device(device) model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], @@ -794,6 +826,8 @@ def test_should_raise_if_model_has_invalid_structure( layers: list[Layer], output_conversion: OutputConversion, error_msg: str, + device: Device, ) -> None: + configure_test_with_device(device) with pytest.raises(InvalidModelStructureError, match=error_msg): NeuralNetworkRegressor(input_conversion, layers, output_conversion)