Source code for topotoolbox.grid_object

"""This module contains the GridObject class.
"""
import copy
from typing import Tuple

import numpy as np
import matplotlib.pyplot as plt

from scipy.ndimage import (
    convolve,
    median_filter,
    generic_filter,
    grey_erosion,
    grey_dilation,
    distance_transform_edt)
from scipy.signal import wiener
from rasterio import CRS
from rasterio.warp import reproject
from rasterio.enums import Resampling

# pylint: disable=no-name-in-module
from . import _grid  # type: ignore

__all__ = ['GridObject']


[docs] class GridObject(): """A class containing all information of a Digital Elevation Model (DEM). """
[docs] def __init__(self) -> None: """Initialize a GridObject instance. """ # path to file self.path = '' # name of DEM self.name = '' # raster metadata self.z = np.empty((), order='F', dtype=np.float32) self.cellsize = 0.0 # in meters if crs.is_projected == True # georeference self.bounds = None self.transform = None self.crs = None
@property def shape(self): """Tuple of grid dimensions """ return self.z.shape @property def rows(self): """The size of the first dimension of the grid """ return self.z.shape[0] @property def columns(self): """The size of the second dimension of the grid """ return self.z.shape[1]
[docs] def reproject(self, crs: 'CRS', resolution: 'float | None' = None, resampling: 'Resampling' = Resampling.bilinear): """Reproject GridObject to a new coordinate system. Parameters ---------- crs : rasterio.CRS Target coordinate system resolution : float, optional Target resolution. If one is not provided, a resolution that approximately matches that of the source coordinate system will be used. resampling : rasterio.enums.Resampling, optional Resampling method. The default is bilinear resampling. Returns ------- GridObject The reprojected data. """ dst = GridObject() dst.z, dst.transform = reproject( self.z, src_transform=self.transform, src_crs=self.crs, dst_transform=None, # Let rasterio derive the transform for us dst_crs=crs, dst_nodata=np.nan, dst_resolution=resolution, resampling=resampling, ) # reproject gives us a 3D array, we want the first band # We also want it in column-major order dst.z = np.asfortranarray(dst.z[0, :, :]) dst.crs = crs # Get cellsize from transform in case we did not specify one if dst.transform is not None: dst.cellsize = abs(dst.transform[0]) return dst
[docs] def fillsinks(self, bc: 'np.ndarray | GridObject | None' = None, hybrid: bool = True) -> 'GridObject': """Fill sinks in the digital elevation model (DEM). Parameters ---------- bc : ndarray or GridObject, optional Boundary conditions for sink filling. `bc` should be an array of np.uint8 that matches the shape of the DEM. Values of 1 indicate pixels that should be fixed to their values in the original DEM and values of 0 indicate pixels that should be filled. hybrid: bool, optional Should hybrid reconstruction algorithm be used? Defaults to True. Hybrid reconstruction is faster but requires additional memory be allocated for a queue. Returns ------- GridObject The filled DEM. """ dem = self.z.astype(np.float32, order='F') output = np.zeros_like(dem) restore_nans = False if bc is None: bc = np.ones_like(dem, dtype=np.uint8) bc[1:-1, 1:-1] = 0 # Set interior pixels to 0 nans = np.isnan(dem) dem[nans] = -np.inf bc[nans] = 1 # Set NaNs to 1 restore_nans = True if bc.shape != self.shape: err = ("The shape of the provided boundary conditions does not " f"match the shape of the DEM. {self.shape}") raise ValueError(err)from None if isinstance(bc, GridObject): bc = bc.z if hybrid: queue = np.zeros_like(dem, dtype=np.int64) _grid.fillsinks_hybrid(output, queue, dem, bc, self.shape) else: _grid.fillsinks(output, dem, bc, self.shape) if restore_nans: dem[nans] = np.nan output[nans] = np.nan result = copy.copy(self) result.z = output return result
[docs] def identifyflats( self, raw: bool = False, output: list[str] | None = None) -> tuple: """Identifies flats and sills in a digital elevation model (DEM). Parameters ---------- raw : bool, optional If True, returns the raw output grid as np.ndarray. Defaults to False. output : list of str, optional List of strings indicating desired output types. Possible values are 'sills', 'flats'. Order of inputs in list are irrelevant, first entry in output will always be sills. Defaults to ['sills', 'flats']. Returns ------- tuple A tuple containing copies of the DEM with identified flats and/or sills. Notes ----- Flats are identified as 1s, sills as 2s, and presills as 5s (since they are also flats) in the output grid. Only relevant when using raw=True. """ # Since having lists as default arguments can lead to problems, output # is initialized with None by default and only converted to a list # containing default output here: if output is None: output = ['sills', 'flats'] dem = self.z.astype(np.float32, order='F') output_grid = np.zeros_like(dem, dtype=np.int32) _grid.identifyflats(output_grid, dem, self.shape) if raw: return tuple(output_grid) result = [] if 'flats' in output: flats = copy.copy(self) flats.z = np.zeros_like(flats.z, order='F') flats.z = np.where((output_grid & 1) == 1, 1, flats.z) result.append(flats) if 'sills' in output: sills = copy.copy(self) sills.z = np.zeros_like(sills.z, order='F') sills.z = np.where((output_grid & 2) == 2, 1, sills.z) result.append(sills) return tuple(result)
[docs] def excesstopography( self, threshold: "float | int | np.ndarray | GridObject" = 0.2, method: str = 'fsm2d',) -> 'GridObject': """ Compute the two-dimensional excess topography using the specified method. Parameters ---------- threshold : float, int, GridObject, or np.ndarray, optional Threshold value or array to determine slope limits, by default 0.2. If a float or int, the same threshold is applied to the entire DEM. If a GridObject or np.ndarray, it must match the shape of the DEM. method : str, optional Method to compute the excess topography, by default 'fsm2d'. Options are: - 'fsm2d': Uses the fast sweeping method. - 'fmm2d': Uses the fast marching method. Returns ------- GridObject A new GridObject with the computed excess topography. Raises ------ ValueError If `method` is not one of ['fsm2d', 'fmm2d']. If `threshold` is an np.ndarray and doesn't match the shape of the DEM. TypeError If `threshold` is not a float, int, GridObject, or np.ndarray. """ if method not in ['fsm2d', 'fmm2d']: err = (f"Invalid method '{method}'. Supported methods are" + " 'fsm2d' and 'fmm2d'.") raise ValueError(err) from None dem = self.z if isinstance(threshold, (float, int)): threshold_slopes = np.full( dem.shape, threshold, order='F', dtype=np.float32) elif isinstance(threshold, GridObject): threshold_slopes = threshold.z elif isinstance(threshold, np.ndarray): threshold_slopes = threshold else: err = "Threshold must be a float, int, GridObject, or np.ndarray." raise TypeError(err) from None if not dem.shape == threshold_slopes.shape: err = "Threshold array must have the same shape as the DEM." raise ValueError(err) from None if not threshold_slopes.flags['F_CONTIGUOUS']: threshold_slopes = np.asfortranarray(threshold) if not np.issubdtype(threshold_slopes.dtype, np.float32): threshold_slopes = threshold_slopes.astype(np.float32) excess = np.zeros_like(dem) cellsize = self.cellsize if method == 'fsm2d': _grid.excesstopography_fsm2d( excess, dem, threshold_slopes, cellsize, self.shape) elif method == 'fmm2d': heap = np.zeros_like(dem, dtype=np.int64) back = np.zeros_like(dem, dtype=np.int64) _grid.excesstopography_fmm2d(excess, heap, back, dem, threshold_slopes, cellsize, self.shape) result = copy.copy(self) result.z = excess return result
[docs] def filter(self, method: str = 'mean', kernelsize: int = 3) -> 'GridObject': """The function filter is a wrapper around various image filtering algorithms. Only filters with rectangular kernels of uneven dimensions are supported. Parameters ---------- method : str, optional Which method will be used to filter the DEM: ['mean', 'average', 'median', 'sobel', 'scharr', 'wiener', 'std'], by default 'mean' kernelsize : int, optional Size of the kernel that will be applied. Note that ['sobel', 'scharr'] require that the kernelsize is 3, by default 3 Returns ------- GridObject The filtered DEM as a GridObject. Raises ------ ValueError If the kernelsize does not match the requirements of this function or the selected method is not implemented in the function. """ valid_methods = ['mean', 'average', 'median', 'sobel', 'scharr', 'wiener', 'std'] if method in ['mean', 'average']: kernel = np.ones((kernelsize, kernelsize)) / kernelsize**2 filtered = convolve(self.z, kernel, mode='nearest') elif method in ['median']: filtered = median_filter(self.z, size=kernelsize, mode='reflect') elif method in ['sobel', 'scharr']: if kernelsize != 3: arr = f"The method '{method}' only works with a 3x3 kernel'." raise ValueError(arr) from None if method == 'sobel': ky = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) else: # 'scharr' ky = np.array([[3, 10, 3], [0, 0, 0], [-3, - 10, - 3]]) kx = ky.T filtered = np.hypot( convolve(self.z, ky, mode='nearest'), convolve(self.z, kx, mode='nearest')) elif method in ['wiener']: filtered = wiener(self.z, mysize=kernelsize) elif method in ['std']: # This solution is based on this thread: # https://stackoverflow.com/questions/19518827/what-is-the-python # -equivalent-for-matlabs-stdfilt-function filtered = generic_filter(self.z, np.std, size=kernelsize) factor = np.sqrt(kernelsize**2 / (kernelsize**2 - 1)) np.multiply(filtered, factor, out=filtered) else: err = (f"Argument 'method={method}' has to be" f"one of {valid_methods}.") raise ValueError(err) from None # Keep NaNs like they are in self.z filtered[np.isnan(self.z)] = np.nan result = copy.copy(self) result.z = filtered return result
[docs] def gradient8(self, unit: str = 'tangent', multiprocessing: bool = True): """ Compute the gradient of a digital elevation model (DEM) using an 8-direction algorithm. Parameters ---------- unit : str, optional The unit of the gradient to be calculated. Options are: - 'tangent' : Calculate the gradient as a tangent (default). - 'radian' : Calculate the gradient in radians. - 'degree' : Calculate the gradient in degrees. - 'sine' : Calculate the gradient as the sine of the angle. - 'percent' : Calculate the gradient as a percentage. multiprocessing : bool, optional If True, use multiprocessing for computation. Default is True. Returns ------- GridObject A new GridObject with the calculated gradient. """ if multiprocessing: use_mp = 1 else: use_mp = 0 dem = self.z.astype(np.float32, order='F') output = np.zeros_like(dem) _grid.gradient8(output, dem, self.cellsize, use_mp, self.shape) result = copy.copy(self) if unit == 'radian': output = np.arctan(output) elif unit == 'degree': output = np.arctan(output) * (180.0 / np.pi) elif unit == 'sine': output = np.sin(np.arctan(output)) elif unit == 'percent': output = output * 100.0 result.z = output return result
[docs] def curvature(self, ctype='profc', meanfilt=False) -> 'GridObject': """curvature returns the second numerical derivative (curvature) of a digital elevation model. By default, curvature returns the profile curvature (profc). Parameters ---------- ctype : str, optional What type of curvature will be computed, by default 'profc' - 'profc' : profile curvature [m^(-1)], - 'planc' : planform curvature [m^(-1))], - 'tangc' : tangential curvature [m^(-1)], - 'meanc' : mean curvature [m^(-1)], - 'total' : total curvature [m^(-2)] meanfilt : bool, optional True if a mean filter will be applied before comuting the curvature, by default False Returns ------- GridObject A GridObject storing the computed values. Raises ------ ValueError If wrong ctype has been used. Examples -------- >>> dem = topotoolbox.load_dem('tibet') >>> curv = dem.curvature() >>> curv.show() """ if meanfilt: kernel = np.ones((3, 3)) / 9 dem = convolve(self.z, kernel, mode='nearest') else: dem = self.z kernel_fx = np.array( [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]) / (6 * self.cellsize) kernel_fy = np.array( [[1, 1, 1], [0, 0, 0], [-1, -1, -1]]) / (6 * self.cellsize) fx = convolve(dem, kernel_fx, mode='nearest') fy = convolve(dem, kernel_fy, mode='nearest') kernel_fxx = np.array( [[1, -2, 1], [1, -2, 1], [1, -2, 1]]) / (3 * self.cellsize**2) kernel_fyy = kernel_fxx.T kernel_fxy = np.array( [[-1, 0, 1], [0, 0, 0], [1, 0, -1]]) / (4 * self.cellsize**2) fxx = convolve(dem, kernel_fxx, mode='nearest') fyy = convolve(dem, kernel_fyy, mode='nearest') fxy = convolve(dem, kernel_fxy, mode='nearest') epsilon = 1e-10 if ctype == 'profc': denominator = (fx**2 + fy**2) * (1 + fx ** 2 + fy**2)**(3/2) + epsilon curvature = - (fx**2 * fxx + 2 * fx * fy * fxy + fy ** 2 * fyy) / denominator elif ctype == 'tangc': denominator = (fx**2 + fy**2) * (1 + fx ** 2 + fy**2)**(1/2) + epsilon curvature = - (fy**2 * fxx - 2 * fx * fy * fxy + fx ** 2 * fyy) / denominator elif ctype == 'planc': denominator = (fx**2 + fy**2)**(3/2) + epsilon curvature = - (fy**2 * fxx - 2 * fx * fy * fxy + fx ** 2 * fyy) / denominator elif ctype == 'meanc': denominator = 2 * (fx**2 + fy**2 + 1)**(3/2) + epsilon curvature = -((1 + fy ** 2) * fxx - 2 * fxy * fx * fy + (1 + fx ** 2) * fyy) / denominator elif ctype == 'total': curvature = fxx**2 + 2 * fxy**2 + fyy**2 else: raise ValueError( "Invalid curvature type. Must be one of: 'profc', 'planc'," "'tangc', 'meanc', 'total'.") # Keep NaNs like they are in dem curvature[np.isnan(dem)] = np.nan result = copy.copy(self) result.z = curvature return result
[docs] def dilate( self, size: tuple | None = None, footprint: np.ndarray | None = None, structure: np.ndarray | None = None) -> 'GridObject': """A simple wrapper around th scipy.ndimage.grey_dilation function, that also handles NaNs in the input GridObject. Either size, footprint or structure has to be passed to this function. If nothing is provided, the function will raise an error. size : tuple of ints, optional A tuple of ints containing the shape of the structuring element. Only needed if neither footprint nor structure is provided. Will result in a full and flat structuring element. Defaults to None footprint : np.ndarray of ints, optional A array defining the footprint of the erosion operation. Non-zero elements define the neighborhood over which the erosion is applied. Defaults to None structure : np.ndarray of ints, optional A array defining the structuring element used for the erosion. This defines the connectivity of the elements. Defaults to None Returns ------- GridObject A GridObject storing the computed values. Raises ------ ValueError If size, structure and footprint are all None. """ if size is None and structure is None and footprint is None: err = ("Dilate requires a structuring element to be specified." " Use the size argument for a full and flat structuring" " element (equivalent to a a minimum filter) or the" " structure and footprint arguments to specify" " a non-flat structuring element.") raise ValueError(err) from None # Replace NaN values with inf dem = self.z.copy() dem[np.isnan(dem)] = -np.inf dilated = grey_dilation( input=dem, size=size, structure=structure, footprint=footprint) # Keep NaNs like they are in dem dilated[np.isnan(self.z)] = np.nan result = copy.copy(self) result.z = dilated return result
[docs] def erode( self, size: tuple | None = None, footprint: np.ndarray | None = None, structure: np.ndarray | None = None) -> 'GridObject': """Apply a morphological erosion operation to the GridObject. Either size, footprint or structure has to be passed to this function. If nothing is provided, the function will raise an error. Parameters ---------- size : tuple of ints A tuple of ints containing the shape of the structuring element. Only needed if neither footprint nor structure is provided. Will result in a full and flat structuring element. Defaults to None footprint : np.ndarray of ints, optional A array defining the footprint of the erosion operation. Non-zero elements define the neighborhood over which the erosion is applied. Defaults to None structure : np.ndarray of ints, optional A array defining the structuring element used for the erosion. This defines the connectivity of the elements. Defaults to None Returns ------- GridObject A GridObject storing the computed values. Raises ------ ValueError If size, structure and footprint are all None.""" if size is None and structure is None and footprint is None: err = ("Erode requires a structuring element to be specified." " Use the size argument for a full and flat structuring" " element (equivalent to a a minimum filter) or the" " structure and footprint arguments to specify" " a non-flat structuring element.") raise ValueError(err) from None # Replace NaN values with inf dem = self.z.copy() dem[np.isnan(dem)] = np.inf eroded = grey_erosion( dem, size=size, structure=structure, footprint=footprint) # Keep NaNs like they are in dem eroded[np.isnan(self.z)] = np.nan result = copy.copy(self) result.z = eroded return result
[docs] def evansslope( self, partial_derivatives: bool = False, mode: str = 'nearest', modified: bool = False) -> 'GridObject' | Tuple['GridObject', 'GridObject']: """Evans method fits a second-order polynomial to 3x3 subgrids. The parameters of the polynomial are the partial derivatives which are used to calculate the surface slope = sqrt(Gx**2 + Gy**2). Evans method approximates the surface by regression surfaces. Gradients are thus less susceptible to noise in the DEM. Parameters ---------- mode : str, optional The mode parameter determines how the input DEM is extended beyond its boundaries: ['reflect', 'constant', 'nearest', 'mirror', 'wrap', 'grid-mirror', 'grid-constant', 'grid-wrap']. See scipy.ndimage.convolve for more information, by default 'nearest' modified : bool, optional If True, the surface is weakly smoothed before gradients are calculated (see Shary et al., 2002), by default False partial_derivatives : bool, optional If True, both partial derivatives [fx, fy] will be returned as GridObjects instead of just the evansslope, by default False Returns ------- GridObject A GridObject containing the computed evansslope data. """ dem = self.z.copy() # NaN replacement not optional since convolve can't handle NaNs indices = distance_transform_edt( np.isnan(dem), return_distances=False, return_indices=True) dem = dem[tuple(indices)] if modified: kernel = np.array([[0, 1, 0], [1, 41, 1], [0, 1, 0]])/45 dem = convolve(dem, kernel, mode=mode) kx = np.array( [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])/(6*self.cellsize) fx = convolve(dem, kx, mode=mode) # kernel for dz/dy ky = np.array( [[1, 1, 1], [0, 0, 0], [-1, -1, -1]])/(6*self.cellsize) fy = convolve(dem, ky, mode=mode) if partial_derivatives: result_kx = copy.copy(self) result_ky = copy.copy(self) result_kx.z = kx result_ky.z = ky return result_kx, result_ky slope = np.sqrt(fx**2 + fy**2) slope[np.isnan(self.z)] = np.nan result = copy.copy(self) result.z = slope return result
def _gwdt_computecosts(self) -> np.ndarray: """ Compute the cost array used in the gradient-weighted distance transform (GWDT) algorithm. Returns ------- np.ndarray A 2D array of costs corresponding to each grid cell in the DEM. """ dem = self.z flats = self.identifyflats(raw=True) filled_dem = self.fillsinks().z dims = self.shape costs = np.zeros_like(dem, dtype=np.float32, order='F') conncomps = np.zeros_like(dem, dtype=np.int64, order='F') _grid.gwdt_computecosts(costs, conncomps, flats, dem, filled_dem, dims) del conncomps, flats, filled_dem return costs def _gwdt(self) -> np.ndarray: """ Perform the grey-weighted distance transform (GWDT) on the DEM. Returns ------- np.ndarray A 2D array representing the GWDT distances for each grid cell. """ costs = self._gwdt_computecosts() flats = self.identifyflats(raw=True) dims = self.shape dist = np.zeros_like(flats, dtype=np.float32, order='F') prev = np.zeros_like(flats, dtype=np.int64, order='F') heap = np.zeros_like(flats, dtype=np.int64, order='F') back = np.zeros_like(flats, dtype=np.int64, order='F') _grid.gwdt(dist, prev, costs, flats, heap, back, dims) del costs, prev, heap, back return dist def _flow_routing_d8_carve(self) -> tuple[np.ndarray, np.ndarray]: """ Compute the flow routing using the D8 algorithm with carving for flat areas. Returns ------- np.ndarray array indicating the source cells for flow routing. (source) np.ndarray array indicating the flow direction for each grid cell. (direction) """ filled_dem = self.fillsinks().z dist = self._gwdt() flats = self.identifyflats(raw=True) dims = self.shape source = np.zeros_like(flats, dtype=np.int64, order='F') direction = np.zeros_like(flats, dtype=np.uint8, order='F') _grid.flow_routing_d8_carve( source, direction, filled_dem, dist, flats, dims) del filled_dem, dist, flats return source, direction def _flow_routing_targets(self) -> np.ndarray: """ Identify the target cells for flow routing. Returns ------- np.ndarray A 2D array where each cell points to its downstream target cell. """ source, direction = self._flow_routing_d8_carve() dims = self.shape target = np.zeros_like(source, dtype=np.int64, order='F') _grid.flow_routing_targets(target, source, direction, dims) del source, direction return target
[docs] def info(self) -> None: """Prints all variables of a GridObject. """ print(f"name: {self.name}") print(f"path: {self.path}") print(f"rows: {self.rows}") print(f"cols: {self.columns}") print(f"cellsize: {self.cellsize}") print(f"bounds: {self.bounds}") print(f"transform: {self.transform}") print(f"crs: {self.crs}")
[docs] def show(self, cmap='terrain') -> None: """ Display the GridObject instance as an image using Matplotlib. Parameters ---------- cmap : str, optional Matplotlib colormap that will be used in the plot. """ plt.imshow(self, cmap=cmap) plt.title(self.name) plt.colorbar() plt.tight_layout() plt.show()
[docs] def shufflelabel(self): """Randomize the labels of a GridObject This function is helpful when plotting drainage basins. It will work with any kind of data, but is most useful when given ordinal data such as an integer-valued GridObject. Returns ------- GridObject A grid identical to the input, but with randomly reassigned labels. """ result = copy.copy(self) labels = self.z u,indices = np.unique(labels,return_inverse=True) result.z = np.random.permutation(u)[indices] return result
# 'Magic' functions: # ------------------------------------------------------------------------ def __eq__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] == other.z[x][y] return dem def __ne__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] != other.z[x][y] return dem def __gt__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] > other.z[x][y] return dem def __lt__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] < other.z[x][y] return dem def __ge__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] >= other.z[x][y] return dem def __le__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): dem.z[x][y] = self.z[x][y] <= other.z[x][y] return dem def __add__(self, other): dem = copy.copy(self) if isinstance(other, self.__class__): dem.z = self.z + other.z return dem dem.z = self.z + other return dem def __sub__(self, other): dem = copy.copy(self) if isinstance(other, self.__class__): dem.z = self.z - other.z return dem dem.z = self.z - other return dem def __mul__(self, other): dem = copy.copy(self) if isinstance(other, self.__class__): dem.z = self.z * other.z return dem dem.z = self.z * other return dem def __div__(self, other): dem = copy.copy(self) if isinstance(other, self.__class__): dem.z = self.z / other.z return dem dem.z = self.z / other return dem def __and__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): if (self.z[x][y] not in [0, 1] or other.z[x][y] not in [0, 1]): raise ValueError( "Invalid cell value. 'and' can only compare " + "True (1) and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) & int(other.z[x][y])) return dem def __or__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): if (self.z[x][y] not in [0, 1] or other.z[x][y] not in [0, 1]): raise ValueError( "Invalid cell value. 'or' can only compare True (1)" + " and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) | int(other.z[x][y])) return dem def __xor__(self, other): dem = copy.deepcopy(self) if not isinstance(other, self.__class__): raise TypeError("Can only compare two GridObjects.") if self.columns != other.columns or self.rows != other.rows: raise ValueError("Both GridObjects have to be the same size.") for x in range(0, self.columns): for y in range(0, self.rows): if (self.z[x][y] not in [0, 1] or other.z[x][y] not in [0, 1]): raise ValueError( "Invalid cell value. 'xor' can only compare True (1)" + " and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) ^ int(other.z[x][y])) return dem def __len__(self): return len(self.z) def __iter__(self): return iter(self.z) def __getitem__(self, index): return self.z[index] def __setitem__(self, index, value): try: value = np.float32(value) except (ValueError, TypeError): raise TypeError( f"{value} can't be converted to float32.") from None self.z[index] = value def __array__(self): return self.z def __str__(self): return str(self.z)