diff --git a/python/damask/_grid.py b/python/damask/_grid.py index a6340ce9b..a65f4eb30 100644 --- a/python/damask/_grid.py +++ b/python/damask/_grid.py @@ -3,6 +3,7 @@ import copy import warnings import multiprocessing as mp from functools import partial +import typing from typing import Union, Optional, TextIO, List, Sequence from pathlib import Path @@ -16,6 +17,7 @@ from . import util from . import grid_filters from . import Rotation from . import Table +from ._typehints import FloatSequence, IntSequence class Grid: """ @@ -29,9 +31,9 @@ class Grid: def __init__(self, material: np.ndarray, - size, - origin = [0.0,0.0,0.0], - comments = []): + size: FloatSequence, + origin: FloatSequence = np.zeros(3), + comments: Union[str, Sequence[str]] = []): """ New geometry definition for grid solvers. @@ -40,18 +42,18 @@ class Grid: material : numpy.ndarray of shape (:,:,:) Material indices. The shape of the material array defines the number of cells. - size : list or numpy.ndarray of shape (3) + size : sequence of float, len (3) Physical size of grid in meter. - origin : list or numpy.ndarray of shape (3), optional - Coordinates of grid origin in meter. - comments : list of str, optional + origin : sequence of float, len (3), optional + Coordinates of grid origin in meter. Defaults to [0.0,0.0,0.0]. + comments : (list of) str, optional Comments, e.g. history of operations. """ self.material = material - self.size = size - self.origin = origin - self.comments = comments + self.size = size # type: ignore + self.origin = origin # type: ignore + self.comments = comments # type: ignore def __repr__(self) -> str: @@ -75,7 +77,7 @@ class Grid: copy = __copy__ - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """ Test equality of other. @@ -86,8 +88,8 @@ class Grid: """ if not isinstance(other, Grid): - raise TypeError - return (np.allclose(other.size,self.size) + return NotImplemented + return bool(np.allclose(other.size,self.size) and np.allclose(other.origin,self.origin) and np.all(other.cells == self.cells) and np.all(other.material == self.material)) @@ -118,19 +120,19 @@ class Grid: return self._size @size.setter - def size(self, size: Union[Sequence[float], np.ndarray]): + def size(self, size: FloatSequence): if len(size) != 3 or any(np.array(size) < 0): raise ValueError(f'invalid size {size}') else: self._size = np.array(size) @property - def origin(self) -> Union[Sequence[float], np.ndarray]: + def origin(self) -> np.ndarray: """Coordinates of grid origin in meter.""" return self._origin @origin.setter - def origin(self, origin: np.ndarray): + def origin(self, origin: FloatSequence): if len(origin) != 3: raise ValueError(f'invalid origin {origin}') else: @@ -165,7 +167,7 @@ class Grid: Parameters ---------- - fname : str or or pathlib.Path + fname : str or pathlib.Path Grid file to read. Valid extension is .vti, which will be appended if not given. @@ -186,8 +188,9 @@ class Grid: comments=comments) + @typing. no_type_check @staticmethod - def load_ASCII(fname): + def load_ASCII(fname)-> "Grid": """ Load from geom file. @@ -225,10 +228,10 @@ class Grid: comments = [] content = f.readlines() for i,line in enumerate(content[:header_length]): - items: List[str] = line.split('#')[0].lower().strip().split() + items = line.split('#')[0].lower().strip().split() key = items[0] if items else '' if key == 'grid': - cells = np.array([int(dict(zip(items[1::2],items[2::2]))[i]) for i in ['a','b','c']]) + cells = np.array([ int(dict(zip(items[1::2],items[2::2]))[i]) for i in ['a','b','c']]) elif key == 'size': size = np.array([float(dict(zip(items[1::2],items[2::2]))[i]) for i in ['x','y','z']]) elif key == 'origin': @@ -236,7 +239,7 @@ class Grid: else: comments.append(line.strip()) - material = np.empty(int(cells.prod())) # initialize as flat array + material = np.empty(int(cells.prod())) # initialize as flat array i = 0 for line in content[header_length:]: items = line.split('#')[0].split() @@ -267,7 +270,7 @@ class Grid: Parameters ---------- - fname : str, pathlib.Path, or file handle + fname : str or pathlib.Path Geometry file to read. Returns @@ -286,7 +289,7 @@ class Grid: @staticmethod - def load_DREAM3D(fname: str, + def load_DREAM3D(fname: Union[str, Path], feature_IDs: str = None, cell_data: str = None, phases: str = 'Phases', Euler_angles: str = 'EulerAngles', base_group: str = None) -> "Grid": @@ -300,24 +303,24 @@ class Grid: Parameters ---------- - fname : str + fname : str or or pathlib.Path Filename of the DREAM.3D (HDF5) file. - feature_IDs : str + feature_IDs : str, optional Name of the dataset containing the mapping between cells and grain-wise data. Defaults to 'None', in which case cell-wise data is used. - cell_data : str + cell_data : str, optional Name of the group (folder) containing cell-wise data. Defaults to None in wich case it is automatically detected. - phases : str + phases : str, optional Name of the dataset containing the phase ID. It is not used for grain-wise data, i.e. when feature_IDs is not None. Defaults to 'Phases'. - Euler_angles : str + Euler_angles : str, optional Name of the dataset containing the crystallographic orientation as Euler angles in radians It is not used for grain-wise data, i.e. when feature_IDs is not None. Defaults to 'EulerAngles'. - base_group : str + base_group : str, optional Path to the group (folder) that contains geometry (_SIMPL_GEOMETRY), and grain- or cell-wise data. Defaults to None, in which case it is set as the path that contains _SIMPL_GEOMETRY/SPACING. @@ -349,7 +352,9 @@ class Grid: @staticmethod - def from_table(table: Table, coordinates: str, labels: Union[str, Sequence[str]]) -> "Grid": + def from_table(table: Table, + coordinates: str, + labels: Union[str, Sequence[str]]) -> "Grid": """ Create grid from ASCII table. @@ -360,7 +365,7 @@ class Grid: coordinates : str Label of the vector column containing the spatial coordinates. Need to be ordered (1./x fast, 3./z slow). - labels : str or list of str + labels : (list of) str Label(s) of the columns containing the material definition. Each unique combination of values results in one material ID. @@ -386,26 +391,26 @@ class Grid: return np.argmin(np.sum((np.broadcast_to(point,(len(seeds),3))-seeds)**2,axis=1) - weights) @staticmethod - def from_Laguerre_tessellation(cells, - size, - seeds, - weights, - material = None, - periodic = True): + def from_Laguerre_tessellation(cells: IntSequence, + size: FloatSequence, + seeds: np.ndarray, + weights: FloatSequence, + material: IntSequence = None, + periodic: bool = True): """ Create grid from Laguerre tessellation. Parameters ---------- - cells : int numpy.ndarray of shape (3) + cells : sequence of int, len (3) Number of cells in x,y,z direction. - size : list or numpy.ndarray of shape (3) + size : sequence of float, len (3) Physical size of the grid in meter. seeds : numpy.ndarray of shape (:,3) Position of the seed points in meter. All points need to lay within the box. - weights : numpy.ndarray of shape (seeds.shape[0]) + weights : sequence of float, len (seeds.shape[0]) Weights of the seeds. Setting all weights to 1.0 gives a standard Voronoi tessellation. - material : numpy.ndarray of shape (seeds.shape[0]), optional + material : sequence of int, len (seeds.shape[0]), optional Material ID of the seeds. Defaults to None, in which case materials are consecutively numbered. periodic : Boolean, optional @@ -427,6 +432,7 @@ class Grid: seeds_p = seeds coords = grid_filters.coordinates0_point(cells,size).reshape(-1,3) + pool = mp.Pool(int(os.environ.get('OMP_NUM_THREADS',4))) result = pool.map_async(partial(Grid._find_closest_seed,seeds_p,weights_p), coords) pool.close() @@ -435,30 +441,30 @@ class Grid: if periodic: material_ %= len(weights) - return Grid(material = material_ if material is None else material[material_], + return Grid(material = material_ if material is None else np.array(material)[material_], size = size, comments = util.execution_stamp('Grid','from_Laguerre_tessellation'), ) @staticmethod - def from_Voronoi_tessellation(cells: np.ndarray, - size: Union[Sequence[float], np.ndarray], + def from_Voronoi_tessellation(cells: IntSequence, + size: FloatSequence, seeds: np.ndarray, - material: np.ndarray = None, + material: IntSequence = None, periodic: bool = True) -> "Grid": """ Create grid from Voronoi tessellation. Parameters ---------- - cells : int numpy.ndarray of shape (3) + cells : sequence of int, len (3) Number of cells in x,y,z direction. - size : list or numpy.ndarray of shape (3) + size : sequence of float, len (3) Physical size of the grid in meter. seeds : numpy.ndarray of shape (:,3) Position of the seed points in meter. All points need to lay within the box. - material : numpy.ndarray of shape (seeds.shape[0]), optional + material : sequence of int, len (seeds.shape[0]), optional Material ID of the seeds. Defaults to None, in which case materials are consecutively numbered. periodic : Boolean, optional @@ -478,7 +484,7 @@ class Grid: except TypeError: material_ = tree.query(coords, n_jobs = int(os.environ.get('OMP_NUM_THREADS',4)))[1] # scipy <1.6 - return Grid(material = (material_ if material is None else material[material_]).reshape(cells), + return Grid(material = (material_ if material is None else np.array(material)[material_]).reshape(cells), size = size, comments = util.execution_stamp('Grid','from_Voronoi_tessellation'), ) @@ -527,20 +533,20 @@ class Grid: @staticmethod - def from_minimal_surface(cells: np.ndarray, - size: Union[Sequence[float], np.ndarray], + def from_minimal_surface(cells: IntSequence, + size: FloatSequence, surface: str, threshold: float = 0.0, periods: int = 1, - materials: tuple = (0,1)) -> "Grid": + materials: IntSequence = (0,1)) -> "Grid": """ Create grid from definition of triply periodic minimal surface. Parameters ---------- - cells : int numpy.ndarray of shape (3) + cells : sequence of int, len (3) Number of cells in x,y,z direction. - size : list or numpy.ndarray of shape (3) + size : sequence of float, len (3) Physical size of the grid in meter. surface : str Type of the minimal surface. See notes for details. @@ -548,7 +554,7 @@ class Grid: Threshold of the minimal surface. Defaults to 0.0. periods : integer, optional. Number of periods per unit cell. Defaults to 1. - materials : (int, int), optional + materials : sequence of int, len (2) Material IDs. Defaults to (0,1). Returns @@ -589,22 +595,21 @@ class Grid: >>> import numpy as np >>> import damask - >>> damask.Grid.from_minimal_surface(np.array([64]*3,int),np.ones(3), - ... 'Gyroid') - cells a b c: 64 x 64 x 64 - size x y z: 1.0 x 1.0 x 1.0 - origin x y z: 0.0 0.0 0.0 + >>> damask.Grid.from_minimal_surface([64]*3,np.ones(3)*1.e-4,'Gyroid') + cells : 64 x 64 x 64 + size : 0.0001 x 0.0001 x 0.0001 / m³ + origin: 0.0 0.0 0.0 / m # materials: 2 Minimal surface of 'Neovius' type. non-default material IDs. >>> import numpy as np >>> import damask - >>> damask.Grid.from_minimal_surface(np.array([80]*3,int),np.ones(3), + >>> damask.Grid.from_minimal_surface([80]*3,np.ones(3)*5.e-4, ... 'Neovius',materials=(1,5)) - cells a b c: 80 x 80 x 80 - size x y z: 1.0 x 1.0 x 1.0 - origin x y z: 0.0 0.0 0.0 + cells : 80 x 80 x 80 + size : 0.0005 x 0.0005 x 0.0005 / m³ + origin: 0.0 0.0 0.0 / m # materials: 2 (min: 1, max: 5) """ @@ -634,7 +639,7 @@ class Grid: v.add(self.material.flatten(order='F'),'material') v.add_comments(self.comments) - v.save(fname if str(fname).endswith('.vti') else str(fname)+'.vti',parallel=False,compress=compress) + v.save(fname,parallel=False,compress=compress) def save_ASCII(self, fname: Union[str, TextIO]): @@ -667,15 +672,15 @@ class Grid: header='\n'.join(header), fmt=format_string, comments='') - def show(self): + def show(self) -> None: """Show on screen.""" VTK.from_rectilinear_grid(self.cells,self.size,self.origin).show() def add_primitive(self, - dimension: np.ndarray, - center: np.ndarray, - exponent: Union[np.ndarray, float], + dimension: Union[FloatSequence, IntSequence], + center: Union[FloatSequence, IntSequence], + exponent: Union[FloatSequence, float], fill: int = None, R: Rotation = Rotation(), inverse: bool = False, @@ -685,14 +690,15 @@ class Grid: Parameters ---------- - dimension : int or float numpy.ndarray of shape (3) - Dimension (diameter/side length) of the primitive. If given as - integers, cell centers are addressed. - If given as floats, coordinates are addressed. - center : int or float numpy.ndarray of shape (3) - Center of the primitive. If given as integers, cell centers are addressed. - If given as floats, coordinates in space are addressed. - exponent : numpy.ndarray of shape (3) or float + dimension : sequence of int or float, len (3) + Dimension (diameter/side length) of the primitive. + If given as integers, cell centers are addressed. + If given as floats, physical coordinates are addressed. + center : sequence of int or float, len (3) + Center of the primitive. + If given as integers, cell centers are addressed. + If given as floats, physical coordinates are addressed. + exponent : float or sequence of float, len (3) Exponents for the three axes. 0 gives octahedron (ǀxǀ^(2^0) + ǀyǀ^(2^0) + ǀzǀ^(2^0) < 1) 1 gives sphere (ǀxǀ^(2^1) + ǀyǀ^(2^1) + ǀzǀ^(2^1) < 1) @@ -719,9 +725,9 @@ class Grid: >>> import damask >>> g = damask.Grid(np.zeros([64]*3,int), np.ones(3)*1e-4) >>> g.add_primitive(np.ones(3)*5e-5,np.ones(3)*5e-5,1) - cells a b c: 64 x 64 x 64 - size x y z: 0.0001 x 0.0001 x 0.0001 - origin x y z: 0.0 0.0 0.0 + cells : 64 x 64 x 64 + size : 0.0001 x 0.0001 x 0.0001 / m³ + origin: 0.0 0.0 0.0 / m # materials: 2 Add a cube at the origin. @@ -730,9 +736,9 @@ class Grid: >>> import damask >>> g = damask.Grid(np.zeros([64]*3,int), np.ones(3)*1e-4) >>> g.add_primitive(np.ones(3,int)*32,np.zeros(3),np.inf) - cells a b c: 64 x 64 x 64 - size x y z: 0.0001 x 0.0001 x 0.0001 - origin x y z: 0.0 0.0 0.0 + cells : 64 x 64 x 64 + size : 0.0001 x 0.0001 x 0.0001 / m³ + origin: 0.0 0.0 0.0 / m # materials: 2 """ @@ -769,7 +775,7 @@ class Grid: Parameters ---------- - directions : iterable containing str + directions : (sequence of) str Direction(s) along which the grid is mirrored. Valid entries are 'x', 'y', 'z'. reflect : bool, optional @@ -788,9 +794,9 @@ class Grid: >>> import damask >>> g = damask.Grid(np.zeros([32]*3,int), np.ones(3)*1e-4) >>> g.mirror('xy',True) - cells a b c: 64 x 64 x 32 - size x y z: 0.0002 x 0.0002 x 0.0001 - origin x y z: 0.0 0.0 0.0 + cells : 64 x 64 x 32 + size : 0.0002 x 0.0002 x 0.0001 / m³ + origin: 0.0 0.0 0.0 / m # materials: 1 """ @@ -821,7 +827,7 @@ class Grid: Parameters ---------- - directions : iterable containing str + directions : (sequence of) str Direction(s) along which the grid is flipped. Valid entries are 'x', 'y', 'z'. @@ -844,13 +850,13 @@ class Grid: ) - def scale(self, cells: np.ndarray, periodic: bool = True) -> "Grid": + def scale(self, cells: IntSequence, periodic: bool = True) -> "Grid": """ Scale grid to new cells. Parameters ---------- - cells : numpy.ndarray of shape (3) + cells : sequence of int, len (3) Number of cells in x,y,z direction. periodic : Boolean, optional Assume grid to be periodic. Defaults to True. @@ -868,9 +874,9 @@ class Grid: >>> import damask >>> g = damask.Grid(np.zeros([32]*3,int),np.ones(3)*1e-4) >>> g.scale(g.cells*2) - cells a b c: 64 x 64 x 64 - size x y z: 0.0001 x 0.0001 x 0.0001 - origin x y z: 0.0 0.0 0.0 + cells : 64 x 64 x 64 + size : 0.0001 x 0.0001 x 0.0001 / m³ + origin: 0.0 0.0 0.0 / m # materials: 1 """ @@ -888,7 +894,10 @@ class Grid: ) - def clean(self, stencil: int = 3, selection: Sequence[float] = None, periodic: bool = True) -> "Grid": + def clean(self, + stencil: int = 3, + selection: IntSequence = None, + periodic: bool = True) -> "Grid": """ Smooth grid by selecting most frequent material index within given stencil at each location. @@ -896,7 +905,7 @@ class Grid: ---------- stencil : int, optional Size of smoothing stencil. - selection : list, optional + selection : sequence of int, optional Field values that can be altered. Defaults to all. periodic : Boolean, optional Assume grid to be periodic. Defaults to True. @@ -907,7 +916,7 @@ class Grid: Updated grid-based geometry. """ - def mostFrequent(arr, selection = None): + def mostFrequent(arr: np.ndarray, selection = None): me = arr[arr.size//2] if selection is None or me in selection: unique, inverse = np.unique(arr, return_inverse=True) @@ -947,7 +956,7 @@ class Grid: ) - def rotate(self, R: Rotation, fill: Union[int, float] = None) -> "Grid": + def rotate(self, R: Rotation, fill: int = None) -> "Grid": """ Rotate grid (pad if required). @@ -955,7 +964,7 @@ class Grid: ---------- R : damask.Rotation Rotation to apply to the grid. - fill : int or float, optional + fill : int, optional Material index to fill the corners. Defaults to material.max() + 1. Returns @@ -985,17 +994,20 @@ class Grid: ) - def canvas(self, cells = None, offset = None, fill = None): + def canvas(self, + cells: IntSequence = None, + offset: IntSequence = None, + fill: int = None) -> "Grid": """ Crop or enlarge/pad grid. Parameters ---------- - cells : numpy.ndarray of shape (3) + cells : sequence of int, len (3), optional Number of cells x,y,z direction. - offset : numpy.ndarray of shape (3) + offset : sequence of int, len (3), optional Offset (measured in cells) from old to new grid [0,0,0]. - fill : int or float, optional + fill : int, optional Material index to fill the background. Defaults to material.max() + 1. Returns @@ -1010,42 +1022,43 @@ class Grid: >>> import numpy as np >>> import damask >>> g = damask.Grid(np.zeros([32]*3,int),np.ones(3)*1e-4) - >>> g.canvas(np.array([32,32,16],int)) - cells a b c: 33 x 32 x 16 - size x y z: 0.0001 x 0.0001 x 5e-05 - origin x y z: 0.0 0.0 0.0 + >>> g.canvas([32,32,16]) + cells : 33 x 32 x 16 + size : 0.0001 x 0.0001 x 5e-05 / m³ + origin: 0.0 0.0 0.0 / m # materials: 1 """ - if offset is None: offset = 0 + offset_ = np.array(offset,int) if offset is not None else np.zeros(3,int) + cells_ = np.array(cells,int) if cells is not None else self.cells if fill is None: fill = np.nanmax(self.material) + 1 dtype = float if int(fill) != fill or self.material.dtype in np.sctypes['float'] else int - canvas = np.full(self.cells if cells is None else cells,fill,dtype) + canvas = np.full(cells_,fill,dtype) - LL = np.clip( offset, 0,np.minimum(self.cells, cells+offset)) - UR = np.clip( offset+cells, 0,np.minimum(self.cells, cells+offset)) - ll = np.clip(-offset, 0,np.minimum( cells,self.cells-offset)) - ur = np.clip(-offset+self.cells,0,np.minimum( cells,self.cells-offset)) + LL = np.clip( offset_, 0,np.minimum(self.cells, cells_+offset_)) + UR = np.clip( offset_+cells_, 0,np.minimum(self.cells, cells_+offset_)) + ll = np.clip(-offset_, 0,np.minimum( cells_,self.cells-offset_)) + ur = np.clip(-offset_+self.cells,0,np.minimum( cells_,self.cells-offset_)) canvas[ll[0]:ur[0],ll[1]:ur[1],ll[2]:ur[2]] = self.material[LL[0]:UR[0],LL[1]:UR[1],LL[2]:UR[2]] return Grid(material = canvas, size = self.size/self.cells*np.asarray(canvas.shape), - origin = self.origin+offset*self.size/self.cells, + origin = self.origin+offset_*self.size/self.cells, comments = self.comments+[util.execution_stamp('Grid','canvas')], ) - def substitute(self, from_material: np.ndarray, to_material: np.ndarray) -> "Grid": + def substitute(self, from_material: IntSequence, to_material: IntSequence) -> "Grid": """ Substitute material indices. Parameters ---------- - from_material : iterable of ints + from_material : sequence of int Material indices to be substituted. - to_material : iterable of ints + to_material : sequence of int New material indices. Returns @@ -1092,7 +1105,7 @@ class Grid: def vicinity_offset(self, vicinity: int = 1, offset: int = None, - trigger: Sequence[int] = [], + trigger: IntSequence = [], periodic: bool = True) -> "Grid": """ Offset material index of points in the vicinity of xxx. @@ -1109,7 +1122,7 @@ class Grid: offset : int, optional Offset (positive or negative) to tag material indices, defaults to material.max()+1. - trigger : list of ints, optional + trigger : sequence of int, optional List of material indices that trigger a change. Defaults to [], meaning that any different neighbor triggers a change. periodic : Boolean, optional @@ -1121,7 +1134,7 @@ class Grid: Updated grid-based geometry. """ - def tainted_neighborhood(stencil, trigger): + def tainted_neighborhood(stencil: np.ndarray, trigger): me = stencil[stencil.shape[0]//2] return np.any(stencil != me if len(trigger) == 0 else np.in1d(stencil,np.array(list(set(trigger) - {me})))) @@ -1140,7 +1153,7 @@ class Grid: ) - def get_grain_boundaries(self, periodic = True, directions = 'xyz'): + def get_grain_boundaries(self, periodic: bool = True, directions: Sequence[str] = 'xyz'): """ Create VTK unstructured grid containing grain boundaries. @@ -1148,7 +1161,7 @@ class Grid: ---------- periodic : Boolean, optional Assume grid to be periodic. Defaults to True. - directions : iterable containing str, optional + directions : (sequence of) string, optional Direction(s) along which the boundaries are determined. Valid entries are 'x', 'y', 'z'. Defaults to 'xyz'. diff --git a/python/damask/_vtk.py b/python/damask/_vtk.py index cbf70c37a..95cea4542 100644 --- a/python/damask/_vtk.py +++ b/python/damask/_vtk.py @@ -419,7 +419,7 @@ class VTK: return writer.GetOutputString() - def show(self): + def show(self) -> None: """ Render. diff --git a/python/tests/test_Grid.py b/python/tests/test_Grid.py index 3538e3dd8..2b9ca22f4 100644 --- a/python/tests/test_Grid.py +++ b/python/tests/test_Grid.py @@ -237,12 +237,27 @@ class TestGrid: modified) - def test_canvas(self,default): + def test_canvas_extend(self,default): cells = default.cells - grid_add = np.random.randint(0,30,(3)) - modified = default.canvas(cells + grid_add) + cells_add = np.random.randint(0,30,(3)) + modified = default.canvas(cells + cells_add) assert np.all(modified.material[:cells[0],:cells[1],:cells[2]] == default.material) + @pytest.mark.parametrize('sign',[+1,-1]) + @pytest.mark.parametrize('extra_offset',[0,-1]) + def test_canvas_move_out(self,sign,extra_offset): + g = Grid(np.zeros(np.random.randint(3,30,(3)),int),np.ones(3)) + o = sign*np.ones(3)*g.cells.min() +extra_offset*sign + if extra_offset == 0: + assert np.all(g.canvas(offset=o).material == 1) + else: + assert np.all(np.unique(g.canvas(offset=o).material) == (0,1)) + + def test_canvas_cells(self,default): + g = Grid(np.zeros(np.random.randint(3,30,(3)),int),np.ones(3)) + cells = np.random.randint(1,30,(3)) + offset = np.random.randint(-30,30,(3)) + assert np.all(g.canvas(cells,offset).cells == cells) @pytest.mark.parametrize('center1,center2',[(np.random.random(3)*.5,np.random.random()*8), (np.random.randint(4,8,(3)),np.random.randint(9,12,(3)))])