Source code for pumapy.utilities.workspace

import pumapy
from pumapy.utilities.logger import Logger, print_warning
import skimage.transform as trans
import numpy as np
from copy import deepcopy
from scipy.ndimage import rotate


[docs]class Workspace: def __init__(self, **kwargs): """ Workspace class holding the domain as a numpy matrix :param kwargs: keyword arguments - No arguments --> zero matrix of shape (1,1,1) - 'shape' argument --> then set all to zeros - 'value' --> value set all to full of value - 'nparray' --> set Workspace matrix as input array - 'scalars' --> bool indicating if a Workspace contains scalars stored in matrix variable - 'vectors' --> bool indicating if a Workspace contains vectors stored in orientation variable :type kwargs: dict """ self.log = Logger() self.voxel_length = 1e-6 # 3D numpy array holding the geometry (scalar) self.matrix = np.zeros((1, 1, 1), dtype=np.uint16) # 3D numpy array holding the orientation field (vectors) self.orientation = np.zeros((1, 1, 1, 3), dtype=float) # setting matrix if len(kwargs) > 0: if 'shape' in kwargs: if isinstance(kwargs['shape'], tuple) and len(kwargs['shape']) == 3: if 'value' in kwargs: self.matrix = np.full(kwargs['shape'], kwargs['value'], dtype=np.uint16) else: self.matrix = np.zeros(kwargs['shape'], dtype=np.uint16) else: raise Exception("Wrong shape, tuple with 3 dimensions required.") elif 'nparray' in kwargs: if isinstance(kwargs['nparray'], np.ndarray): if kwargs['nparray'].ndim == 3: self.matrix = kwargs['nparray'].copy().astype(np.uint16) else: raise Exception("Wrong nparray ndim, 3 dimensions required.") else: raise Exception("Wrong nparray type.") else: raise Exception("Unrecognized keyword.") # setting orientation if 'vectors' in kwargs and kwargs['vectors']: if not isinstance(kwargs['vectors'], bool): raise Exception("Orientation input is not a bool.") else: if 'shape' in kwargs: if isinstance(kwargs['shape'], tuple) and len(kwargs['shape']) == 3: if 'vectorvalue' in kwargs: self.orientation = np.tile(kwargs['vectorvalue'], list(kwargs['shape']) + [1]).astype(float) else: self.orientation = np.zeros(list(kwargs['shape']) + [3], dtype=float) else: raise Exception("Wrong shape, tuple with 3 dimensions required.") else: raise Exception("Unrecognized keyword.")
[docs] @classmethod def from_shape(cls, shape, orientation=False): """ Generate workspace from shape, all matrix value set to zero. :param shape: shape of workspace to be created :type shape: (int, int, int) :param orientation: specify if workspace contains orientation :type orientation: bool :return: new workspace :rtype: pumapy.Workspace :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape((10, 11, 12)) >>> # puma.render_volume(ws, style='edges') # to visualize it """ return cls(shape=shape, vectors=orientation)
[docs] @classmethod def from_shape_value(cls, shape, value, orientation=False): """ Generate workspace from shape, all matrix values set to the value passed. :param shape: shape of workspace to be created :type shape: (int, int, int) :param value: value to be assigned to the matrix variable :type value: int :param orientation: specify if workspace contains orientation :type orientation: bool :return: new workspace :rtype: pumapy.Workspace :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape_value((20, 31, 212), 1) >>> # puma.render_volume(ws, style='edges') # to visualize it """ return cls(shape=shape, value=value, vectors=orientation)
[docs] @classmethod def from_shape_vector(cls, shape, vector): """ Generate workspace from shape, all orientation vectors set to the vector passed. :param shape: shape of workspace to be created :type shape: (int, int, int) :param vector: vector to be assigned to the orientation variable :type vector: (float, float, float) :return: new workspace with orientation :rtype: pumapy.Workspace :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape_vector((5, 6, 2), (0.4, 2, 5)) >>> # puma.render_orientation(ws) # to visualize it """ return cls(shape=shape, vectorvalue=vector, vectors=True)
[docs] @classmethod def from_shape_value_vector(cls, shape, value, vector): """ Generate workspace from shape, all matrix and orientation set to the values passed. :param shape: shape of workspace to be created :type shape: (int, int, int) :param value: value to be assigned to the matrix variable :type value: int :param vector: vector to be assigned to the orientation variable :type vector: (float, float, float) :return: new workspace with orientation :rtype: pumapy.Workspace :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape_value_vector((5, 6, 2), 1, (0.4, 2, 5)) >>> # puma.render_orientation(ws) # to visualize it """ return cls(shape=shape, value=value, vectorvalue=vector, vectors=True)
[docs] @classmethod def from_array(cls, nparray): """ Generate workspace matrix from numpy array. :param nparray: array of shape (X,Y,Z) to be assigned to the matrix variable. NB: array is turned into type uint16 :type nparray: np.ndarray :return: new workspace :rtype: pumapy.Workspace :Example: >>> import pumapy as puma >>> array = np.random.randint(5, size=(10, 10, 10)) >>> ws = puma.Workspace.from_array(array) >>> # puma.render_volume(ws, style='edges') # to visualize it """ return cls(nparray=nparray, vectors=False)
[docs] def set_voxel_length(self, voxel_length): """ Set voxel size, which by default is set to 1e-6 :param voxel_length: size of a voxel side :type voxel_length: float :return: None :Example: >>> import pumapy as puma >>> array = np.random.randint(5, size=(10, 10, 10)) >>> ws = puma.Workspace.from_array(array) >>> ws.set_voxel_length(1) # equivalent to ws.voxel_length = 1 >>> # puma.render_volume(ws, style='edges') # to visualize it """ if not isinstance(voxel_length, int) and not isinstance(voxel_length, float): raise Exception("Voxel_length needs to be an int or float, got " + str(type(voxel_length))) else: self.voxel_length = voxel_length
[docs] def set_matrix(self, nparray): """ Set matrix with numpy array :param nparray: array of shape (X,Y,Z) to be assigned to the matrix variable :type nparray: np.ndarray :return: None :Example: >>> import pumapy as puma >>> ws = puma.Workspace() >>> nparray = np.random.randint(5, size=(10, 10, 10)) >>> ws.set_matrix(nparray) # equivalent to ws.matrix = nparray.copy().astype(np.uint16) >>> # puma.render_volume(ws, style='edges') # to visualize it """ if isinstance(nparray, np.ndarray): if nparray.ndim == 3: self.matrix = nparray.copy().astype(np.uint16) else: raise Exception("Wrong nparray ndim, 3 dimensions required. Leaving matrix unchanged") else: print_warning("Wrong nparray type. Leaving matrix unchanged")
[docs] def set_orientation(self, nparray): """ Set orientation with numpy array :param nparray: array of shape (X,Y,Z, 3) to be assigned to the orientation variable :type nparray: np.ndarray :return: None :Example: >>> import pumapy as puma >>> ws = puma.Workspace() >>> ws.set_orientation(np.random.rand(10, 10, 10, 3)) >>> # puma.render_orientation(ws, solid_color=None) # to visualize it """ if isinstance(nparray, np.ndarray): if nparray.ndim == 4 and nparray.shape[3] == 3: self.orientation = nparray.copy().astype(float) else: raise Exception("Wrong nparray ndim, 4 dimensions required as (x,y,z,3). Leaving orientation unchanged") else: raise Exception("Wrong nparray type. Leaving orientation unchanged")
[docs] def copy(self): """ Create a copy of the workspace :return: copy of workspace :rtype: pumapy.Workspace """ return deepcopy(self)
def __getitem__(self, key): return self.matrix[key] def __setitem__(self, key, value): self.matrix[key] = value
[docs] def get_size(self): """ Return size :return: number of voxels :rtype: int """ return self.matrix.size
[docs] def len_x(self): """ Return x dimension length :return: number of voxels in x dimension :rtype: int """ return self.matrix.shape[0]
[docs] def len_y(self): """ Return y dimension length :return: number of voxels in y dimension :rtype: int """ return self.matrix.shape[1]
[docs] def len_z(self): """ Return z dimension length :return: number of voxels in z dimension :rtype: int """ return self.matrix.shape[2]
[docs] def get_shape(self): """ Return shape of domain :return: shape of domain (matrix) :rtype: (int, int, int) """ return self.matrix.shape
[docs] def min(self): """ Return minimum of domain :return: minimum :rtype: int """ return np.min(self.matrix)
[docs] def max(self): """ Return maximum of domain :return: maximum :rtype: int """ return np.max(self.matrix)
[docs] def average(self): """ Return average of domain :return: grayscale average :rtype: float """ return self.matrix.mean()
[docs] def unique_values(self): """ Return unique values in domain matrix variable :return: unique values :rtype: np.ndarray """ return np.unique(self.matrix)
[docs] def unique_values_counts(self): """ Return unique values and their counts in domain matrix variable :return: unique values and counts :rtype: (np.ndarray, int) """ return np.unique(self.matrix, return_counts=True)
[docs] def orientation_magnitude(self): """ Return orientation vector's magnitude :return: orientation field magnitude :rtype: np.ndarray """ return np.linalg.norm(self.orientation, axis=3)
[docs] def resize_new_matrix(self, shape, value=None): """ Resize matrix numpy array :param shape: shape of workspace to be resized :type shape: (int, int, int) :param value: value to be assigned to the new resized matrix variable :type value: int :Example: >>> import pumapy as puma >>> ws = puma.Workspace() >>> ws.resize_new_matrix((10, 10, 10), value=3) >>> # puma.render_volume(ws, style='edges') # to visualize it """ if isinstance(shape, tuple) and len(shape) == 3: if value is None: self.matrix = np.zeros(shape, dtype=np.uint16) else: self.matrix = np.full(shape, value, dtype=np.uint16) else: raise Exception("Wrong shape, tuple with 3 dimensions required.")
[docs] def resize_new_orientation(self, shape, orientation_value=None): """ Resize orientation numpy array :param shape: shape of workspace to be resized :type shape: (int, int, int) :param orientation_value: vector to be assigned to the new resized orientation variable :type orientation_value: (float, float, float) :Example: >>> import pumapy as puma >>> ws = puma.Workspace() >>> ws.resize_new_orientation((10, 10, 10), orientation_value=(1., 0., 0.)) >>> # puma.render_orientation(ws) # to visualize it """ if isinstance(shape, tuple) and len(shape) == 3: if orientation_value is None: self.orientation = np.zeros(list(shape) + [3], dtype=float) else: if isinstance(orientation_value, tuple) and len(orientation_value) == 3: self.orientation = np.tile(orientation_value, list(shape) + [1]).astype(float) else: raise Exception("vectorvalue has to be (float, float, float).") else: raise Exception("Wrong shape, tuple with 3 dimensions required.")
[docs] def create_orientation(self): """ Create orientation field of the same size as the matrix :return: None :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape((10, 10, 10)) >>> ws.create_orientation() """ self.orientation = np.zeros(list(self.matrix.shape) + [3], dtype=float)
[docs] def resize(self, shape, segmented, anti_aliasing=True, interpolation_order=1): """ Resize both matrix and orientation (if present) by rescaling the content to specified size :param shape: shape of workspace to be resized :type shape: (int, int, int) :param segmented: specifying whether the domain is already segmented (True) or grayscales (False) :type segmented: bool :param anti_aliasing: if aliasing is to be prevented applying a Gaussian filter to smooth before scaling. If domain is segmented, automatically set to False in order to preserve domain :type anti_aliasing: bool :param interpolation_order: order of the interpolation spline used. For segmented, it is enforced to be 0,which is 'nearest neighbor' to preserve the segmentation :type interpolation_order: int :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws2 = ws_fiberform.copy() >>> ws2.resize((100, 200, 200), segmented=False) >>> # puma.compare_slices(ws_fiberform, ws2) # to visualize it """ if isinstance(shape, tuple) and len(shape) == 3: if self.orientation.shape[:3] == self.matrix.shape: self.orientation = trans.resize(self.orientation, list(shape) + [3], order=0, preserve_range=True, anti_aliasing=False) if segmented: self.matrix = trans.resize(self.matrix, shape, order=0, anti_aliasing=False, preserve_range=True) else: self.matrix = trans.resize(self.matrix, shape, order=interpolation_order, anti_aliasing=anti_aliasing, preserve_range=True) self.matrix = self.matrix.astype('uint16') else: raise Exception("Wrong shape, tuple with 3 dimensions required.")
[docs] def rescale(self, scale, segmented, anti_aliasing=True, interpolation_order=1): """ Rescale both matrix and orientation (if present) by rescaling the content by a specified factor :param scale: specifying the scaling factor :type scale: float :param segmented: specifying whether the domain is already segmented (True) or grayscales (False) :type segmented: bool :param anti_aliasing: if aliasing is to be prevented applying a Gaussian filter to smooth before scaling. If domain is segmented, automatically set to False in order to preserve domain :type anti_aliasing: bool :param interpolation_order: order of the interpolation spline used. For segmented, it is enforced to be 0, which is 'nearest neighbor' to preserve the segmentation :type interpolation_order: int :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws2 = ws_fiberform.copy() >>> ws2.rescale(0.5, segmented=False) Rescaled workspace size: (100, 100, 100) >>> # puma.compare_slices(ws_fiberform, ws2) # to visualize it, pay attention to the shape """ unit_dim_check = None if min(self.get_shape()) == 1: if self.len_x() == 1: unit_dim_check = 0 elif self.len_y() == 1: unit_dim_check = 1 elif self.len_z() == 1: unit_dim_check = 2 self.matrix = np.squeeze(self.matrix) if self.orientation.shape[:3] == self.matrix.shape: self.orientation = trans.rescale(self.orientation, scale, order=0, channel_axis=-1, preserve_range=True, anti_aliasing=False) if segmented: self.matrix = trans.rescale(self.matrix, scale, order=0, anti_aliasing=False, preserve_range=True) else: self.matrix = trans.rescale(self.matrix, scale, order=interpolation_order, anti_aliasing=anti_aliasing, preserve_range=True) if unit_dim_check is not None: self.matrix = np.expand_dims(self.matrix, axis=unit_dim_check) self.matrix = self.matrix.astype('uint16') self.voxel_length /= scale print("Rescaled workspace size: {}".format(self.get_shape()))
[docs] def set(self, matrix_value=None, orientation_value=None): """ Set all elements in matrix equal to value (and orientation to vectorvalue is passed) :param matrix_value: value to fill to the matrix variable. NB this value will be turned into np.uint16 :type matrix_value: int :param orientation_value: vector to fill to the orientation variable :type orientation_value: ((float, float, float)) :return: None :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape_value((10, 10, 10), 5) >>> ws.set(matrix_value=4) """ check = True if matrix_value is not None: if np.issubdtype(type(matrix_value), np.integer) and 0 <= matrix_value <= 65535: self.matrix.fill(np.uint16(matrix_value)) check = False else: raise Exception("matrix_value has to be np.uint16 (i.e. 0 <= matrix_value <= 65535).") if orientation_value is not None: if isinstance(orientation_value, tuple) and len(orientation_value) == 3: if self.orientation.shape[:3] != self.matrix.shape: self.resize_new_orientation(self.matrix.shape, orientation_value=orientation_value) else: self.orientation[:] = orientation_value check = False else: raise Exception("orientation_value has to be (float, float, float).") if check: print_warning("No changes have been made to the Workspace.")
[docs] def apply_mask(self, mask, apply_to_orientation=False): """ Apply mask of same size as the matrix by leaving the mask's 1s unchanged and setting mask's 0s to 0 :param mask: array of type bool with same size as matrix :type mask: np.ndarray :param apply_to_orientation: specifying whether the mask is to be applied to the orientation (if present) :type apply_to_orientation: bool :return: None :Example: >>> import pumapy as puma >>> ws = puma.Workspace.from_shape_value_vector((10, 10, 10), 1, (0.4, 2, 5)) >>> mask = np.random.randint(255, size=(10, 10, 10)) > 100 >>> ws.apply_mask(mask, apply_to_orientation=True) >>> # puma.render_volume(ws, style='edges') # to visualize it """ if isinstance(mask, np.ndarray) and mask.dtype == 'bool': if mask.shape == self.matrix.shape: self.matrix[~mask] = 0 if self.orientation.shape[:3] == self.matrix.shape and apply_to_orientation: self.orientation[~mask] = 0 else: raise Exception("The mask has to be of the same size as the Workspace matrix.") else: raise Exception("The mask has to be a Numpy array of type bool.")
[docs] def set_material_id(self, cutoff, value): """ Threshold the workspace according to cutoff (i.e. tuple with inclusive range to set) :param cutoff: convert a range of grayscale values specified by the cutoff into an single ID number :type cutoff: (int, int) :param value: ID number to assign to range :type value: int :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws2 = ws_fiberform.copy() >>> ws2.set_material_id((0, 100), 0) # NB the order of these operations is important! >>> ws2.set_material_id((100, 255), 1) # this is why binarize and binarize_range should be preferred >>> # puma.compare_slices(ws_fiberform, ws2) # to visualize it """ if value < 0: raise Exception("Value ID can only be positive. Leaving matrix unchanged.") if value > 1000000: raise Exception("Value ID cannot be > 1000000. Leaving matrix unchanged.") self.matrix = np.where(np.logical_and(self.matrix >= cutoff[0], self.matrix <= cutoff[1]), np.uint16(value), self.matrix) self.log.log_section("Set Material ID") self.log.log_line(str(cutoff) + " -> " + str(value)) self.log.write_log()
[docs] def binarize(self, threshold): """ Binarize the workspace according to threshold, inclusive for higher range set to 1, lower to 0 :param threshold: grayscale value dividing the domain into 0s and 1s (threshold turns into 1) :type threshold: int :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws2 = ws_fiberform.copy() >>> ws2.binarize(100) >>> # puma.compare_slices(ws_fiberform, ws2) # to visualize it """ self.matrix = np.where(self.matrix < threshold, np.uint16(0), np.uint16(1)) self.log.log_section("Binarize domain") self.log.log_line(">" + str(threshold) + " -> 1; 0 otherwise") self.log.write_log()
[docs] def binarize_range(self, ones_cutoff): """ Binarize the workspace according to range within cutoff, inclusive for cutoff ints set to 1, rest to 0 :param ones_cutoff: convert a range of grayscale values specified by the cutoff into 1s, rest into 0s :type ones_cutoff: (int, int) :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws2 = ws_fiberform.copy() >>> ws2.binarize_range((100, 255)) >>> # puma.compare_slices(ws_fiberform, ws2) # to visualize it """ self.matrix = np.where(np.logical_and(self.matrix >= ones_cutoff[0], self.matrix <= ones_cutoff[1]), np.uint16(1), np.uint16(0)) self.log.log_section("Binarize domain") self.log.log_line(str(ones_cutoff[0]) + " < matrix < " + str(ones_cutoff[1]) + " -> 1; 0 otherwise") self.log.write_log()
[docs] def rotate(self, degrees, around_axis, reshape=True, boundary_mode='reflect', apply_to_orientation=True): """ Rotate domain by specified degrees :param degrees: degrees to rotate domain :type degrees: float :param around_axis: specify around what axis to perform the rotation. It can be 'x', 'y' or 'z' :type around_axis: string :param reshape: specify whether to reshape the domain (and therefore contain every voxel - reshape=True) or keep its original size (reshape=False) :type reshape: bool :param boundary_mode: specifying what to do with the boundaries. Options: ‘reflect’, ‘constant’, ‘nearest’, ‘mirror’, ‘wrap’ :type boundary_mode: string :param apply_to_orientation: specify whether to apply rotation to the orientation, if present :type apply_to_orientation: bool :return: None :Example: >>> import pumapy as puma >>> ws_fiberform = puma.import_3Dtiff(puma.path_to_example_file("200_fiberform.tif"), 1.3e-6) Importing ... >>> ws_copy = ws_fiberform.copy() >>> ws_copy.rotate(45, 'z', reshape=False, boundary_mode='reflect') >>> # puma.compare_slices(ws_fiberform, ws_copy) # to visualize it >>> ws_copy = ws_fiberform.copy() >>> ws_copy.rotate(45, 'z', reshape=True, boundary_mode='constant') >>> # puma.compare_slices(ws_fiberform, ws_copy) # to visualize it >>> ws_copy = ws_fiberform.copy() >>> ws_copy.rotate(45, 'z', reshape=True, boundary_mode='reflect') >>> # puma.compare_slices(ws_fiberform, ws_copy) # to visualize it """ if around_axis == 'x': axes = (1, 2) elif around_axis == 'y': axes = (0, 2) elif around_axis == 'z': axes = (0, 1) else: raise Exception("Axis not recognized: around_axis can only be 'x', 'y' or 'z'") self.matrix = rotate(self.matrix, angle=degrees, axes=axes, mode=boundary_mode, reshape=reshape) if self.orientation.shape[:3] == self.matrix.shape and apply_to_orientation: from scipy.spatial.transform import Rotation rotation_degrees = degrees rotation_radians = np.radians(rotation_degrees) if around_axis == 'x': rotation_axis = np.array([1, 0, 0]) elif around_axis == 'y': rotation_axis = np.array([0, 1, 0]) else: rotation_axis = np.array([0, 0, 1]) rotation_vector = rotation_radians * rotation_axis rotation = Rotation.from_rotvec(rotation_vector) for i in range(self.len_x()): for j in range(self.len_y()): for k in range(self.len_z()): self.orientation[i, j, k] = rotation.apply(self.orientation[i, j, k]) self.orientation = rotate(self.orientation, angle=degrees, axes=axes, mode=boundary_mode, reshape=reshape, order=0)
[docs] def porosity(self, cutoff=(0, 0), display=False): """ Compute porosity of domain :param cutoff: void cutoff :type cutoff: (int, int) :param display: print result :type display: bool :return: volume fraction :rtype: float :Example: >>> import pumapy as puma >>> ws = puma.import_3Dtiff(puma.path_to_example_file("100_fiberform.tif"), 1.3e-6) Importing ... >>> ws.porosity(cutoff=(0, 89), display=True) Volume Fraction for cutoff (0, 89): ... """ return pumapy.compute_volume_fraction(self, cutoff, display=display)
[docs] def show_matrix(self): """ Print content of matrix domain's variable """ if isinstance(self, Workspace): x, y, z = self.matrix.shape elif isinstance(self, np.ndarray): x, y, z = self.shape else: raise Exception("Print can only be called on a Workspace or Numpy array.") print() print("3D Workspace:") # Printing coordinate system used print(" o---> y") print(" |") print("x v") print('[', end='') for k in range(z): print("(:,:,{})".format(k)) print('[', end='') for i in range(x): print('[', end='') for j in range(y): print(self[i, j, k], end='') if j != y - 1: print(' ', end='') print(']', end='') if i != x - 1: print() if k != z - 1: print() print() print(']')
[docs] def show_orientation(self, dec=1): """ Print content of orientation domain's variable """ if isinstance(self, Workspace): dims = self.matrix.shape elif isinstance(self, np.ndarray): if self.shape[3] == 3: dims = self.shape else: raise Exception("Numpy array has to be of size (x,y,z,3,...).") else: raise Exception("Print can only be called on a Workspace or Numpy array.") print() print("3D Orientation:") # Printing coordinate system used print(" o---> y") print(" |") print("x v") print('[', end='') if isinstance(self, Workspace): for k in range(dims[2]): print("(:,:,{})".format(k)) print('[', end='') for i in range(dims[0]): print('[', end='') for j in range(dims[1]): print('({:.{}f}, {:.{}f}, {:.{}f})'.format(self.orientation[i, j, k, 0], dec, self.orientation[i, j, k, 1], dec, self.orientation[i, j, k, 2], dec), end='') if j != dims[1] - 1: print(' ', end='') print(']', end='') if i != dims[0] - 1: print() if k != dims[2] - 1: print() print() else: for k in range(dims[2]): print("(:,:,{})".format(k)) print('[', end='') for i in range(dims[0]): print('[', end='') for j in range(dims[1]): print('({}, {}, {})'.format(self[i, j, k, 0], self[i, j, k, 1], self[i, j, k, 2]), end='') if j != dims[1] - 1: print(' ', end='') print(']', end='') if i != dims[0] - 1: print() if k != dims[2] - 1: print() print() print(']')