misc.grid.cornerpoint
Common functionality for visualization of corner-point grids.
import pyresito.grid.cornerpoint as cp
1"""Common functionality for visualization of corner-point grids. 2 3import pyresito.grid.cornerpoint as cp 4""" 5import itertools as it 6import logging 7import numpy as np 8import numpy.ma as ma 9 10 11# add a valid log in case we are not run through the main program which 12# sets up one for us 13log = logging.getLogger(__name__) # pylint: disable=invalid-name 14log.addHandler(logging.NullHandler()) 15 16 17def scatter(cell_field): 18 """\ 19 Duplicate all items in every dimension. 20 21 Use this method to duplicate values associated with each cell to 22 each of the corners in the cell. 23 24 :param cell_field: Property per cell in the grid 25 :type cell_field: :class:`numpy.ndarray`, shape = (nk, nj, ni) 26 :return: Property per corner in the cube 27 :rtype: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2) 28 29 >>> scatter(np.array([[[1, 2], 30 [3, 4]], 31 [[5, 6], 32 [7, 8]]])) 33 34 array([[[[[[1, 1], [2, 2]], 35 [[1, 1], [2, 2]]], 36 [[[3, 3], [4, 4]], 37 [[3, 3], [4, 4]]]], 38 39 [[[[1, 1], [2, 2]], 40 [[1, 1], [2, 2]]], 41 [[[3, 3], [4, 4]], 42 [[3, 3], [4, 4]]]]], 43 44 [[[[[5, 5], [6, 6]], 45 [[5, 5], [6, 6]]], 46 [[[7, 7], [8, 8]], 47 [[7, 7], [8, 8]]]], 48 49 [[[[5, 5], [6, 6]], 50 [[5, 5], [6, 6]]], 51 [[[7, 7], [8, 8]], 52 [[7, 7], [8, 8]]]]]]) 53 """ 54 # get the dimensions of the cube in easily understood aliases 55 nk, nj, ni = cell_field.shape # pylint: disable=invalid-name 56 57 # create an extra dimension so that we can duplicate individual values 58 flat = np.reshape(cell_field, (nk, nj, ni, 1)) 59 60 # duplicate along each of the axis 61 dup_i = np.tile(flat, (1, 1, 1, 2)) 62 dup_j = np.tile(dup_i, (1, 1, 2, 1)) 63 dup_k = np.tile(dup_j, (1, 2, 1, 1)) 64 65 # reformat to a cube with the appropriate dimensions 66 corn_field = np.reshape(dup_k, (nk, 2, nj, 2, ni, 2)) 67 return corn_field 68 69 70def inner_dup(pillar_field): 71 """Duplicate all inner items in both dimensions. 72 73 Use this method to duplicate values associated with each pillar to 74 the corners on each side(s) of the pillar; four corners for all 75 interior pillars, two corners for all pillars on the rim and only 76 one (element) corner for those pillars that are on the grid corners. 77 78 :param pillar_field: Property per pillar in the grid 79 :type pillar_field: :class:`numpy.ndarray`, shape=(m+1, n+1) 80 :returns: Property per corner in a grid plane 81 :rtype: :class:`numpy.ndarray`, shape=(2*m, 2*n)) 82 83 >>> inner_dup(np.array ([[1, 2, 3], 84 [4, 5, 6], 85 [7, 8, 9]])) 86 array([[1, 2, 2, 3], 87 [4, 5, 5, 6], 88 [4, 5, 5, 6], 89 [7, 8, 8, 9]]) 90 """ 91 # extract the inner horizontal part of the plane 92 horz_part = pillar_field[:, 1:-1] 93 94 # tile it to a separate plane 95 horz_dupd = np.tile(horz_part, (2, 1, 1)) 96 97 # shift the tile to the end so it can be rolled 98 # into the second dimension 99 horz_shft = np.transpose(horz_dupd, (1, 2, 0)) 100 101 # roll this into the second dimension 102 horz_roll = np.reshape(horz_shft, 103 (horz_shft.shape[0], 104 horz_shft.shape[1] * 2)) 105 106 # add back the first and last column 107 horz = np.column_stack([pillar_field[:, 0], 108 horz_roll, 109 pillar_field[:, -1]]) 110 111 # extract the inner vertical part of the plane 112 vert_part = horz[1:-1, :] 113 114 # tile it to a separate plane 115 vert_dupd = np.tile(vert_part, (1, 1, 2)) 116 117 # roll this into the first dimension 118 vert_roll = np.reshape(vert_dupd, 119 (vert_dupd.shape[1] * 2, 120 vert_dupd.shape[2] // 2)) 121 122 # add back the first and last row 123 result = np.vstack([horz[0, :], 124 vert_roll, 125 horz[-1, :]]) 126 return result 127 128 129def elem_vtcs_ndcs(nk, nj, ni): # pylint: disable=invalid-name 130 """List zcorn indices used by every element in an nk*nj*ni grid 131 132 :param nk: Number of layers in Z direction 133 :type nk: int 134 :param nj: Number of elements in the Y direction 135 :type nj: int 136 :param ni: Number of elements in the X direction 137 :type ni: int 138 :returns: Zero-based indices for the hexahedral element corners 139 :rtype: :class:`numpy.ndarray`, shape=(nk*nj*ni, 8), dtype=int 140 """ 141 # hex_perm is the order a hexahedron should be specified to the 142 # gridding engine (it is the same for VTK and Ensight Gold formats) 143 # relative to the order they are in the Eclipse format. the latter 144 # four is listed first because we turn the z order around with a 145 # transform. 146 # local indices: 0, 1, 2, 3, 4, 5, 6, 7 147 hex_perm = np.array([4, 5, 7, 6, 0, 1, 3, 2], dtype=int) 148 149 # kji is the global (k, j, i) index for every element; we tile 150 # this into 8 corners per element, with 3 indices per corner; 151 # this this matrix contains repeats of 8 identical permutations 152 # of the values 0..(ni-1), 0..(nj-1) and 0..(nk-1) 153 kji = np.array(list(it.product(range(nk), 154 range(nj), 155 range(ni)))) 156 kji = np.reshape(np.tile(kji, [1, hex_perm.shape[0]]), [-1, 3]) 157 158 # bfr is the local (bottom, front, right) index for every corner; 159 # this is a block of 8 local corners which are picked out of the 160 # zcorn matrix to then correspond with the gridding engines order. 161 # we tile this block for every element 162 bfr = np.array(list(it.product([0, 1], repeat=3)))[hex_perm, :] 163 bfr = np.tile(bfr, [nk*nj*ni, 1]) 164 165 # calculate running number for every corner, combining the element 166 # indices kji with the local corner indices bfr. the zcorn hypercube 167 # has dimensions nk * 2 * nj * 2 * ni * 2; the indexing is simply a 168 # flattened view of this hypercube. 169 ndx = (((2*kji[:, 0] + bfr[:, 0]) * 2*nj + 170 (2*kji[:, 1] + bfr[:, 1])) * 2*ni + 171 2*kji[:, 2] + bfr[:, 2]) 172 173 # we then collect the indices so that we get 8 of them (all those that 174 # belongs to the same element) on a row 175 ndx = np.reshape(ndx, [-1, hex_perm.shape[0]]) 176 return ndx 177 178 179# pylint: disable=invalid-name, multiple-statements, too-many-locals 180def corner_coordinates(coord, zcorn): 181 """Generate (x, y, z) coordinates for each corner-point. 182 183 :param coord: Pillar geometrical information 184 :type coord: :class:`numpy.ndarray`, shape=(nj+1, ni+1, 2, 3) 185 :param zcorn: Depth values along each pillar 186 :type zcorn: :class:`numpy.ndarray`, shape=(nk, 2, nj, 2, ni, 2) 187 :returns: Coordinate values for each corner 188 :rtype: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 189 """ 190 # indices to treat the pillar matrix as a record 191 X = 0 192 Y = 1 193 Z = 2 194 START = 0 195 END = 1 196 197 # calculate depth derivatives for the pillar slope 198 dx = coord[:, :, END, X] - coord[:, :, START, X] 199 dy = coord[:, :, END, Y] - coord[:, :, START, Y] 200 dz = coord[:, :, END, Z] - coord[:, :, START, Z] 201 202 # make the pillar information compatible with a plane of corners, 203 # having a singleton dimension at the front (instead tiling 2*nk) 204 x0 = inner_dup(coord[:, :, START, X])[None, :, :] 205 y0 = inner_dup(coord[:, :, START, Y])[None, :, :] 206 z0 = inner_dup(coord[:, :, START, Z])[None, :, :] 207 dxdz = inner_dup(dx / dz)[None, :, :] 208 dydz = inner_dup(dy / dz)[None, :, :] 209 210 # infer the grid size from the dimensions of coord 211 nj = coord.shape[0] - 1 212 ni = coord.shape[1] - 1 213 214 # make the formal dimensions of the depths compatible with the plane 215 # of vectors created from the pillars; notice that the first dimension 216 # is not singular 217 z = np.reshape(zcorn, (-1, nj*2, ni*2)) 218 219 # calculate the point in the plane where the horizon intersect with the 220 # pillar; we get the (x, y)-coordinates for each corner. notice that the 221 # difference z - z0 is for each point, whereas dz is for the pillar! 222 x = (z - z0) * dxdz + x0 223 y = (z - z0) * dydz + y0 224 225 # reformat the coordinates into the final coordinates 226 xyz = np.vstack([np.reshape(x, (1, -1)), 227 np.reshape(y, (1, -1)), 228 np.reshape(z, (1, -1))]) 229 return xyz 230 231 232# Enumeration of the dimensions in a point tuple 233Dim = type('Dim', (), { 234 'X': 0, 235 'Y': 1, 236 'Z': 2, 237}) 238 239 240# Enumeration of the faces of a hexahedron. The contents are the local 241# indices in each row of 8 corners that are set up for the elements by 242# the elem_vtcs_ndcs function. (The indices here are the numbers that 243# are in the comment "local indices" above the hex_perm selector) 244Face = type('Face', (), { 245 'DOWN': np.array([0, 1, 2, 3]), 246 'UP': np.array([4, 5, 6, 7]), 247 'FRONT': np.array([4, 7, 3, 0]), 248 'BACK': np.array([5, 6, 2, 1]), 249 'LEFT': np.array([7, 6, 2, 3]), 250 'RIGHT': np.array([4, 5, 1, 0]), 251 'ALL': np.array([0, 1, 2, 3, 4, 5, 6, 7]), 252}) 253 254 255def cp_cells(grid, face): 256 """ 257 Make a cell array from a cornerpoint grid. The cells will be in the 258 same order as the Cartesian enumeration of the grid. 259 260 :param grid: Pillar coordinates and corner depths 261 :type grid: dict, with 'coord' and 'zcorn' properties 262 :param face: Which face that should be extracted, e.g. Face.UP 263 :type face: Face enum 264 :returns: Set of geometrical objects that can be sent to rendering 265 :rtype: dict with 'points', shape (nverts, 3), and 'cells', shape 266 (nelems, ncorns), where ncorns is either 8 (hexahedron 267 volume or 4 (quadrilateral face), depending on the face 268 parameter. 269 """ 270 src = {} 271 272 log.debug("Generating points for corner depths") 273 # notice that VTK wants shape (nverts, 3), where as the code sets up 274 # for Ensight format which is (3, nverts). 275 src['points'] = corner_coordinates(grid['COORD'], 276 grid['ZCORN']).T.copy() 277 278 # order of the dimensions are different from the structure to the call 279 ni, nj, nk = grid['DIMENS'] 280 281 # extract only the columns of the face we want to show. the shape was 282 # originally (nelem, 8). copy is necessary to get a contiguous array 283 # for VTK 284 log.debug("Generating cells") 285 src['cells'] = elem_vtcs_ndcs(nk, nj, ni)[:, face].copy() 286 return src 287 288 289def cell_filter(grid, func): 290 """Create a filter for a specific grid. 291 292 :param grid: Grid structure that should be filtered 293 :type grid: dict 294 :param func: Lambda function that takes i, j, k indices (one-based) 295 and returns boolean for whether the cells should be 296 included or not. The function should be vectorized. 297 :type func: Callable[(int, int, int), bool] 298 299 >>> layr = cell_filter (grid, lamba i, j, k: np.greater_equal (k, 56)) 300 """ 301 # extract the dimensions; notice that the i-dimensions is first 302 # in this list, since it is directly from the grid 303 ni, nj, nk = grid['DIMENS'] # pylint: disable=invalid-name 304 305 # setup a grid of the i, j and k address for each of the cells; 306 # notice that now the order of the dimensions are swapped so that 307 # they fit with the memory layout of a loaded Numpy array 308 kji = np.mgrid[1:(nk+1), 1:(nj+1), 1:(ni+1)] 309 310 # call the filter function on all these addresses, and simply 311 # return the boolean array of those 312 filter_flags = func(kji[2], kji[1], kji[0]) 313 masked = filter_flags.astype(np.bool) 314 315 # get the mask of active cells, and combine this with the masked 316 # cells from the filter, giving us a flag for all visible nodes 317 active = grid['ACTNUM'] 318 visible = np.logical_and(active, masked) 319 320 return visible 321 322 323def face_coords(grid): 324 """Get (x, y, z) coordinates for each corner-point. 325 326 :param grid: Cornerpoint-grid 327 :type grid: dict 328 :returns: Coordinate values for each corner. Use the Face enum to index 329 the first dimension, k, j, i coordinates to index the next 330 three, and Dim enum to index the last dimension. 331 Note that the first point in a face is not necessarily the 332 point that is closest to the origin of the grid. 333 :rtype: :class:`numpy.ndarray`, shape = (8, nk, nj, ni, 3) 334 335 >>> import numpy as np 336 >>> import pyresito.io.ecl as ecl 337 >>> import pyresito.grid.cornerpoint as cp 338 >>> case = ecl.EclipseCase ("FOO") 339 >>> coord_fijkd = cp.face_coords (case.grid ()) 340 >>> # get the midpoint of the upper face in each cell 341 >>> up_mid = np.average (coord_fijkd [cp.Face.UP, :, :, :, :], axis = 0) 342 """ 343 # get coordinates in native corner-point format 344 xyz = corner_coordinates(grid['COORD'], grid['ZCORN']) 345 346 # get extent of grid 347 ni, nj, nk = grid['DIMENS'] 348 349 # break up the latter dimension into a many-dimensional hypercube; this 350 # is an inexpensive operation since it won't have to reshuffle the memory 351 xyz = np.reshape(xyz, (3, nk, 2, nj, 2, ni, 2)) 352 353 # move all the dimensions local within an element to the front 354 xyz = np.transpose(xyz, (2, 4, 6, 1, 3, 5, 0)) 355 356 # coalesce the local dimensions so that they end up in one dimension 357 # which can be indexed by the Face enum to extract a certain field 358 xyz = np.reshape(xyz, (8, nk, nj, ni, 3)) 359 360 return xyz 361 362 363def horizon(grid, layer=0, top=True): 364 """Extract the points that are in a certain horizon and average them so 365 that the result is per cell and not per pillar. 366 367 :param grid: Grid structure 368 :type grid: dict 369 :param layer: The K index of the horizon. Default is the layer at the top. 370 :type layer: int 371 :param top: Whether the top face should be exported. If this is False, 372 then the bottom face is exported instead. 373 :type top: bool 374 :returns: Depth at the specified horizon for each cell center 375 :rtype: :class:`numpy.ma.array`, shape = (nk, nj, ni), 376 dtype = numpy.float64 377 """ 378 # get the dimensions of the grid. notice that this array is always given 379 # in Fortran order (since Eclipse works that way) and not in native order 380 num_i, num_j, _ = grid['DIMENS'] 381 382 # zcorn is nk * (up,down) * nj * (front,back) * ni * (left,right). we first 383 # project the particular layer and face that we want to work with, ending 384 # up with four corners for each cell. 385 vertical = 0 if top else 1 386 corners = grid['ZCORN'][layer, vertical, :, :, :, :] 387 388 # move the second and fourth dimension (front/back and left/right) to the 389 # back and collapse them into one dimension, meaning that we get 390 # (nj, ni, 4). then take the average across this last dimension, ending up 391 # with an (nj, ni) grid, which is what we return 392 heights = np.average(np.reshape(np.transpose( 393 corners, axes=(0, 2, 1, 3)), (num_j, num_i, 2*2)), axis=2) 394 395 # if there are any inactive elements, then mask out the elevation at that 396 # spot (it is not really a part of the grid) 397 actnum = grid['ACTNUM'][layer, :, :] 398 return ma.array(data=heights, mask=np.logical_not(actnum)) 399 400 401def _reduce_corners(plane, red_op): 402 """ 403 :param plane: Z-coordinates for a specific horizon in the grid 404 :type plane: :class:`numpy.ndarray`, shape = (nj, 2, ni, 2) 405 :param red_op: How coordinates are merged (reduction operator): either 406 numpy.minimum or np.maximum, depending on top or bottom 407 :type red_op: :class:`numpy.ufunc` 408 :returns: Extremal value of each point around the corners. 409 :rtype: :class:`numpy.ndarray`, shape = (nj+1, ni+1) 410 411 >> plane = np.reshape (np.arange (36) + 1, (3, 2, 3, 2)) 412 >> _reduce_corners (plane, np.minimum) 413 """ 414 # constants 415 NORTH = 1 416 SOUTH = 0 417 EAST = 1 418 WEST = 0 419 420 # get dimensions of the domain 421 nj, _, ni, _ = plane.shape 422 423 # allocate output memory for all pillars 424 out = np.empty((nj + 1, ni + 1), dtype=np.float64) 425 426 # views into the original plane, getting a specific corner in every cell. 427 # these views then have dimensions (nj, ni) 428 nw = plane[:, NORTH, :, WEST] 429 ne = plane[:, NORTH, :, EAST] 430 sw = plane[:, SOUTH, :, WEST] 431 se = plane[:, SOUTH, :, EAST] 432 433 # four corners of the output grid only have a single value. the views have 434 # indices 0..n-1, the output array have indices 0..n 435 out[0, 0] = sw[0, 0] 436 out[nj, 0] = nw[nj-1, 0] 437 out[0, ni] = se[0, ni-1] 438 out[nj, ni] = ne[nj-1, ni-1] 439 440 # four edges contains values from two windows; first two horizontal edges, 441 # (varying along i), then two vertical edges (varying along j). recall that 442 # *ranges* in Python have inclusive start, exclusive end. 443 out[0, 1:ni] = red_op(sw[0, 1:ni], se[0, 0:ni-1]) 444 out[nj, 1:ni] = red_op(nw[nj-1, 1:ni], ne[nj-1, 0:ni-1]) 445 out[1:nj, 0] = red_op(nw[0:nj-1, 0], sw[1:nj, 0]) 446 out[1:nj, ni] = red_op(ne[0:nj-1, ni-1], se[1:nj, ni-1]) 447 448 # interior have two nested reduction operations 449 out[1:nj, 1:ni] = red_op( 450 red_op(nw[0:nj-1, 1:ni], sw[1:nj, 1:ni]), 451 red_op(ne[0:nj-1, 0:ni-1], se[1:nj, 0:ni-1])) 452 453 return out 454 455 456def horizon_pillars(grid, layer=0, top=True): 457 """Extract heights where a horizon crosses the pillars. 458 459 :param grid: Grid structure, containing both COORD and ZCORN properties. 460 :type grid: dict 461 :param layer: The K index of the horizon. Default is the layer at the top. 462 :type layer: int 463 :param top: Whether the top face should be exported. If this false is 464 False, then the bottom face is exported instead. 465 :type top: bool 466 :returns: Heights of the horizon at each pillar, in the same format as 467 the COORD matrix. 468 :rtype: :class:`numpy.ndarray`, shape = (nj+1, ni+1, 2, 3) 469 """ 470 # NumPy's average operations finds the average within arrays, but we want 471 # an operation that can take the average across two arrays 472 def avg_op(a, b): 473 return 0.5 * (a + b) 474 475 # zcorn is nk * (up,down) * nj * (front,back) * ni * (left,right). we first 476 # project the particular layer and face that we want to work with, ending 477 # up with four corners for each cell. take the average of the heights that 478 # are hinged up on each pillar to get a "smooth" surface. (this will not be 479 # the same surface that the cornerpoint grid was created from). 480 vertical = 0 if top else 1 481 points = _reduce_corners(grid['ZCORN'][layer, vertical, :, :, :, :], 482 avg_op) 483 return points 484 485 486def snugfit(grid): 487 """Create coordinate pillars that matches exactly the top and bottom 488 horizon of the grid cells. 489 490 This version assumes that the pillars in the grid are all strictly 491 vertical, i.e. the x- and y-coordinates will not be changed. 492 493 :param grid: Grid structure, containing both COORD and ZCORN properties. 494 :type grid: dict 495 :returns: Pillar coordinates, in the same format as the COORD matrix. 496 Note that a new matrix is returned, the original grid is not 497 altered/updated. 498 :rtype: :class:`numpy.ndarray`, shape = (nj+1, ni+1, 2, 3) 499 """ 500 # placement in the plane of each pillar is unchanged 501 x = grid['COORD'][:, :, 0, 0] 502 y = grid['COORD'][:, :, 0, 1] 503 504 # get implicit dimension of grid 505 njp1, nip1 = x.shape 506 507 # get new top and bottom from the z-coordinates; smaller z is shallower 508 top = _reduce_corners(grid['ZCORN'][0, 0, :, :, :, :], np.minimum) 509 btm = _reduce_corners(grid['ZCORN'][-1, 1, :, :, :, :], np.maximum) 510 511 # combine coordinates to a pillar grid with new z-coordinates 512 coord = np.reshape(np.dstack(( 513 x.ravel(), y.ravel(), top.ravel(), 514 x.ravel(), y.ravel(), btm.ravel())), (njp1, nip1, 2, 3)) 515 516 return coord 517 518 519def bounding_box(corn, filtr): 520 """\ 521 Bounding box of the grid. This function will always assume that the grid 522 is aligned with the geographical axes. 523 524 :param corn: Coordinate values for each corner. This matrix can be 525 constructed with the `corner_coordinates` function. 526 :type corn: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 527 :param filtr: Active corners; use scatter of ACTNUM if no filtering 528 :type filtr: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2), 529 dtype = numpy.bool 530 :return: Bottom front right corner and top back left corner. Since 531 the matrix is returned with C ordering, it is specified the 532 opposite way of what is natural for mathematical matrices. 533 :rtype: :class:`numpy.ndarray`, shape = (2, 3), dtype = np.float64 534 535 >>> corn = corner_coordinates(grid['COORD'], grid['ZCORN']) 536 >>> bbox = bounding_box(corn, scatter(grid['ACTNUM'])) 537 >>> p, q = bbox[0, :], bbox[1, :] 538 >>> diag = np.sqrt(np.dot(q - p, q - p)) 539 """ 540 # scatter the filter mask from each cell to its corners 541 mask = np.logical_not(filtr).ravel() 542 543 # build arrays of each separate coordinate based on its inclusion 544 x_vals = ma.array(data=corn[0, :], mask=mask) 545 y_vals = ma.array(data=corn[1, :], mask=mask) 546 z_vals = ma.array(data=corn[2, :], mask=mask) 547 548 # construct bounding box that includes all coordinates in visible grid 549 bbox = np.array([[np.min(x_vals), np.min(y_vals), np.min(z_vals)], 550 [np.max(x_vals), np.max(y_vals), np.max(z_vals)]], 551 dtype=corn.dtype) 552 return bbox 553 554 555def mass_center(corn, filtr): 556 """\ 557 Mass center of the grid. This function will always assume that the density 558 is equal throughout the field. 559 560 :param corn: Coordinate values for each corner. This matrix can be 561 constructed with the `corner_coordinates` function. 562 :type corn: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 563 :param filtr: Active corners; use scatter of ACTNUM if no filtering 564 :type filtr: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2), 565 dtype = numpy.bool 566 :return: Center of mass. This should be the focal point of the grid. 567 :rtype: :class:`numpy.ndarray`, shape = (3,), dtype = np.float64 568 569 >>> corn = cp.corner_coordinates(grid['COORD'], grid['ZCORN']) 570 >>> focal_point = cp.mass_center(corn, cp.scatter(grid['ACTNUM'])) 571 """ 572 # scatter the filter mask from each cell to its corners 573 mask = np.logical_not(filtr).ravel() 574 575 # build arrays of each separate coordinate based on its inclusion 576 x_vals = ma.array(data=corn[0, :], mask=mask) 577 y_vals = ma.array(data=corn[1, :], mask=mask) 578 z_vals = ma.array(data=corn[2, :], mask=mask) 579 580 # use this as a coarse approximation to find the element center 581 center = np.array([np.average(x_vals), 582 np.average(y_vals), 583 np.average(z_vals)], dtype=corn.dtype) 584 return center
18def scatter(cell_field): 19 """\ 20 Duplicate all items in every dimension. 21 22 Use this method to duplicate values associated with each cell to 23 each of the corners in the cell. 24 25 :param cell_field: Property per cell in the grid 26 :type cell_field: :class:`numpy.ndarray`, shape = (nk, nj, ni) 27 :return: Property per corner in the cube 28 :rtype: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2) 29 30 >>> scatter(np.array([[[1, 2], 31 [3, 4]], 32 [[5, 6], 33 [7, 8]]])) 34 35 array([[[[[[1, 1], [2, 2]], 36 [[1, 1], [2, 2]]], 37 [[[3, 3], [4, 4]], 38 [[3, 3], [4, 4]]]], 39 40 [[[[1, 1], [2, 2]], 41 [[1, 1], [2, 2]]], 42 [[[3, 3], [4, 4]], 43 [[3, 3], [4, 4]]]]], 44 45 [[[[[5, 5], [6, 6]], 46 [[5, 5], [6, 6]]], 47 [[[7, 7], [8, 8]], 48 [[7, 7], [8, 8]]]], 49 50 [[[[5, 5], [6, 6]], 51 [[5, 5], [6, 6]]], 52 [[[7, 7], [8, 8]], 53 [[7, 7], [8, 8]]]]]]) 54 """ 55 # get the dimensions of the cube in easily understood aliases 56 nk, nj, ni = cell_field.shape # pylint: disable=invalid-name 57 58 # create an extra dimension so that we can duplicate individual values 59 flat = np.reshape(cell_field, (nk, nj, ni, 1)) 60 61 # duplicate along each of the axis 62 dup_i = np.tile(flat, (1, 1, 1, 2)) 63 dup_j = np.tile(dup_i, (1, 1, 2, 1)) 64 dup_k = np.tile(dup_j, (1, 2, 1, 1)) 65 66 # reformat to a cube with the appropriate dimensions 67 corn_field = np.reshape(dup_k, (nk, 2, nj, 2, ni, 2)) 68 return corn_field
Duplicate all items in every dimension.
Use this method to duplicate values associated with each cell to each of the corners in the cell.
Parameters
- cell_field: Property per cell in the grid
Returns
Property per corner in the cube
>>> scatter(np.array([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]]))
array([[[[[[1, 1], [2, 2]], [[1, 1], [2, 2]]], [[[3, 3], [4, 4]], [[3, 3], [4, 4]]]],
[[[[1, 1], [2, 2]],
[[1, 1], [2, 2]]],
[[[3, 3], [4, 4]],
[[3, 3], [4, 4]]]]],
[[[[[5, 5], [6, 6]],
[[5, 5], [6, 6]]],
[[[7, 7], [8, 8]],
[[7, 7], [8, 8]]]],
[[[[5, 5], [6, 6]],
[[5, 5], [6, 6]]],
[[[7, 7], [8, 8]],
[[7, 7], [8, 8]]]]]])
71def inner_dup(pillar_field): 72 """Duplicate all inner items in both dimensions. 73 74 Use this method to duplicate values associated with each pillar to 75 the corners on each side(s) of the pillar; four corners for all 76 interior pillars, two corners for all pillars on the rim and only 77 one (element) corner for those pillars that are on the grid corners. 78 79 :param pillar_field: Property per pillar in the grid 80 :type pillar_field: :class:`numpy.ndarray`, shape=(m+1, n+1) 81 :returns: Property per corner in a grid plane 82 :rtype: :class:`numpy.ndarray`, shape=(2*m, 2*n)) 83 84 >>> inner_dup(np.array ([[1, 2, 3], 85 [4, 5, 6], 86 [7, 8, 9]])) 87 array([[1, 2, 2, 3], 88 [4, 5, 5, 6], 89 [4, 5, 5, 6], 90 [7, 8, 8, 9]]) 91 """ 92 # extract the inner horizontal part of the plane 93 horz_part = pillar_field[:, 1:-1] 94 95 # tile it to a separate plane 96 horz_dupd = np.tile(horz_part, (2, 1, 1)) 97 98 # shift the tile to the end so it can be rolled 99 # into the second dimension 100 horz_shft = np.transpose(horz_dupd, (1, 2, 0)) 101 102 # roll this into the second dimension 103 horz_roll = np.reshape(horz_shft, 104 (horz_shft.shape[0], 105 horz_shft.shape[1] * 2)) 106 107 # add back the first and last column 108 horz = np.column_stack([pillar_field[:, 0], 109 horz_roll, 110 pillar_field[:, -1]]) 111 112 # extract the inner vertical part of the plane 113 vert_part = horz[1:-1, :] 114 115 # tile it to a separate plane 116 vert_dupd = np.tile(vert_part, (1, 1, 2)) 117 118 # roll this into the first dimension 119 vert_roll = np.reshape(vert_dupd, 120 (vert_dupd.shape[1] * 2, 121 vert_dupd.shape[2] // 2)) 122 123 # add back the first and last row 124 result = np.vstack([horz[0, :], 125 vert_roll, 126 horz[-1, :]]) 127 return result
Duplicate all inner items in both dimensions.
Use this method to duplicate values associated with each pillar to the corners on each side(s) of the pillar; four corners for all interior pillars, two corners for all pillars on the rim and only one (element) corner for those pillars that are on the grid corners.
Parameters
- pillar_field: Property per pillar in the grid :returns: Property per corner in a grid plane
>>> inner_dup(np.array ([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]))
array([[1, 2, 2, 3],
[4, 5, 5, 6],
[4, 5, 5, 6],
[7, 8, 8, 9]])
130def elem_vtcs_ndcs(nk, nj, ni): # pylint: disable=invalid-name 131 """List zcorn indices used by every element in an nk*nj*ni grid 132 133 :param nk: Number of layers in Z direction 134 :type nk: int 135 :param nj: Number of elements in the Y direction 136 :type nj: int 137 :param ni: Number of elements in the X direction 138 :type ni: int 139 :returns: Zero-based indices for the hexahedral element corners 140 :rtype: :class:`numpy.ndarray`, shape=(nk*nj*ni, 8), dtype=int 141 """ 142 # hex_perm is the order a hexahedron should be specified to the 143 # gridding engine (it is the same for VTK and Ensight Gold formats) 144 # relative to the order they are in the Eclipse format. the latter 145 # four is listed first because we turn the z order around with a 146 # transform. 147 # local indices: 0, 1, 2, 3, 4, 5, 6, 7 148 hex_perm = np.array([4, 5, 7, 6, 0, 1, 3, 2], dtype=int) 149 150 # kji is the global (k, j, i) index for every element; we tile 151 # this into 8 corners per element, with 3 indices per corner; 152 # this this matrix contains repeats of 8 identical permutations 153 # of the values 0..(ni-1), 0..(nj-1) and 0..(nk-1) 154 kji = np.array(list(it.product(range(nk), 155 range(nj), 156 range(ni)))) 157 kji = np.reshape(np.tile(kji, [1, hex_perm.shape[0]]), [-1, 3]) 158 159 # bfr is the local (bottom, front, right) index for every corner; 160 # this is a block of 8 local corners which are picked out of the 161 # zcorn matrix to then correspond with the gridding engines order. 162 # we tile this block for every element 163 bfr = np.array(list(it.product([0, 1], repeat=3)))[hex_perm, :] 164 bfr = np.tile(bfr, [nk*nj*ni, 1]) 165 166 # calculate running number for every corner, combining the element 167 # indices kji with the local corner indices bfr. the zcorn hypercube 168 # has dimensions nk * 2 * nj * 2 * ni * 2; the indexing is simply a 169 # flattened view of this hypercube. 170 ndx = (((2*kji[:, 0] + bfr[:, 0]) * 2*nj + 171 (2*kji[:, 1] + bfr[:, 1])) * 2*ni + 172 2*kji[:, 2] + bfr[:, 2]) 173 174 # we then collect the indices so that we get 8 of them (all those that 175 # belongs to the same element) on a row 176 ndx = np.reshape(ndx, [-1, hex_perm.shape[0]]) 177 return ndx
List zcorn indices used by every element in an nknjni grid
Parameters
- nk: Number of layers in Z direction
- nj: Number of elements in the Y direction
- ni: Number of elements in the X direction :returns: Zero-based indices for the hexahedral element corners
181def corner_coordinates(coord, zcorn): 182 """Generate (x, y, z) coordinates for each corner-point. 183 184 :param coord: Pillar geometrical information 185 :type coord: :class:`numpy.ndarray`, shape=(nj+1, ni+1, 2, 3) 186 :param zcorn: Depth values along each pillar 187 :type zcorn: :class:`numpy.ndarray`, shape=(nk, 2, nj, 2, ni, 2) 188 :returns: Coordinate values for each corner 189 :rtype: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 190 """ 191 # indices to treat the pillar matrix as a record 192 X = 0 193 Y = 1 194 Z = 2 195 START = 0 196 END = 1 197 198 # calculate depth derivatives for the pillar slope 199 dx = coord[:, :, END, X] - coord[:, :, START, X] 200 dy = coord[:, :, END, Y] - coord[:, :, START, Y] 201 dz = coord[:, :, END, Z] - coord[:, :, START, Z] 202 203 # make the pillar information compatible with a plane of corners, 204 # having a singleton dimension at the front (instead tiling 2*nk) 205 x0 = inner_dup(coord[:, :, START, X])[None, :, :] 206 y0 = inner_dup(coord[:, :, START, Y])[None, :, :] 207 z0 = inner_dup(coord[:, :, START, Z])[None, :, :] 208 dxdz = inner_dup(dx / dz)[None, :, :] 209 dydz = inner_dup(dy / dz)[None, :, :] 210 211 # infer the grid size from the dimensions of coord 212 nj = coord.shape[0] - 1 213 ni = coord.shape[1] - 1 214 215 # make the formal dimensions of the depths compatible with the plane 216 # of vectors created from the pillars; notice that the first dimension 217 # is not singular 218 z = np.reshape(zcorn, (-1, nj*2, ni*2)) 219 220 # calculate the point in the plane where the horizon intersect with the 221 # pillar; we get the (x, y)-coordinates for each corner. notice that the 222 # difference z - z0 is for each point, whereas dz is for the pillar! 223 x = (z - z0) * dxdz + x0 224 y = (z - z0) * dydz + y0 225 226 # reformat the coordinates into the final coordinates 227 xyz = np.vstack([np.reshape(x, (1, -1)), 228 np.reshape(y, (1, -1)), 229 np.reshape(z, (1, -1))]) 230 return xyz
Generate (x, y, z) coordinates for each corner-point.
Parameters
- coord: Pillar geometrical information
- zcorn: Depth values along each pillar :returns: Coordinate values for each corner
256def cp_cells(grid, face): 257 """ 258 Make a cell array from a cornerpoint grid. The cells will be in the 259 same order as the Cartesian enumeration of the grid. 260 261 :param grid: Pillar coordinates and corner depths 262 :type grid: dict, with 'coord' and 'zcorn' properties 263 :param face: Which face that should be extracted, e.g. Face.UP 264 :type face: Face enum 265 :returns: Set of geometrical objects that can be sent to rendering 266 :rtype: dict with 'points', shape (nverts, 3), and 'cells', shape 267 (nelems, ncorns), where ncorns is either 8 (hexahedron 268 volume or 4 (quadrilateral face), depending on the face 269 parameter. 270 """ 271 src = {} 272 273 log.debug("Generating points for corner depths") 274 # notice that VTK wants shape (nverts, 3), where as the code sets up 275 # for Ensight format which is (3, nverts). 276 src['points'] = corner_coordinates(grid['COORD'], 277 grid['ZCORN']).T.copy() 278 279 # order of the dimensions are different from the structure to the call 280 ni, nj, nk = grid['DIMENS'] 281 282 # extract only the columns of the face we want to show. the shape was 283 # originally (nelem, 8). copy is necessary to get a contiguous array 284 # for VTK 285 log.debug("Generating cells") 286 src['cells'] = elem_vtcs_ndcs(nk, nj, ni)[:, face].copy() 287 return src
Make a cell array from a cornerpoint grid. The cells will be in the same order as the Cartesian enumeration of the grid.
Parameters
- grid: Pillar coordinates and corner depths
- face: Which face that should be extracted, e.g. Face.UP :returns: Set of geometrical objects that can be sent to rendering
290def cell_filter(grid, func): 291 """Create a filter for a specific grid. 292 293 :param grid: Grid structure that should be filtered 294 :type grid: dict 295 :param func: Lambda function that takes i, j, k indices (one-based) 296 and returns boolean for whether the cells should be 297 included or not. The function should be vectorized. 298 :type func: Callable[(int, int, int), bool] 299 300 >>> layr = cell_filter (grid, lamba i, j, k: np.greater_equal (k, 56)) 301 """ 302 # extract the dimensions; notice that the i-dimensions is first 303 # in this list, since it is directly from the grid 304 ni, nj, nk = grid['DIMENS'] # pylint: disable=invalid-name 305 306 # setup a grid of the i, j and k address for each of the cells; 307 # notice that now the order of the dimensions are swapped so that 308 # they fit with the memory layout of a loaded Numpy array 309 kji = np.mgrid[1:(nk+1), 1:(nj+1), 1:(ni+1)] 310 311 # call the filter function on all these addresses, and simply 312 # return the boolean array of those 313 filter_flags = func(kji[2], kji[1], kji[0]) 314 masked = filter_flags.astype(np.bool) 315 316 # get the mask of active cells, and combine this with the masked 317 # cells from the filter, giving us a flag for all visible nodes 318 active = grid['ACTNUM'] 319 visible = np.logical_and(active, masked) 320 321 return visible
Create a filter for a specific grid.
Parameters
- grid: Grid structure that should be filtered
- func: Lambda function that takes i, j, k indices (one-based) and returns boolean for whether the cells should be included or not. The function should be vectorized.
>>> layr = cell_filter (grid, lamba i, j, k: np.greater_equal (k, 56))
324def face_coords(grid): 325 """Get (x, y, z) coordinates for each corner-point. 326 327 :param grid: Cornerpoint-grid 328 :type grid: dict 329 :returns: Coordinate values for each corner. Use the Face enum to index 330 the first dimension, k, j, i coordinates to index the next 331 three, and Dim enum to index the last dimension. 332 Note that the first point in a face is not necessarily the 333 point that is closest to the origin of the grid. 334 :rtype: :class:`numpy.ndarray`, shape = (8, nk, nj, ni, 3) 335 336 >>> import numpy as np 337 >>> import pyresito.io.ecl as ecl 338 >>> import pyresito.grid.cornerpoint as cp 339 >>> case = ecl.EclipseCase ("FOO") 340 >>> coord_fijkd = cp.face_coords (case.grid ()) 341 >>> # get the midpoint of the upper face in each cell 342 >>> up_mid = np.average (coord_fijkd [cp.Face.UP, :, :, :, :], axis = 0) 343 """ 344 # get coordinates in native corner-point format 345 xyz = corner_coordinates(grid['COORD'], grid['ZCORN']) 346 347 # get extent of grid 348 ni, nj, nk = grid['DIMENS'] 349 350 # break up the latter dimension into a many-dimensional hypercube; this 351 # is an inexpensive operation since it won't have to reshuffle the memory 352 xyz = np.reshape(xyz, (3, nk, 2, nj, 2, ni, 2)) 353 354 # move all the dimensions local within an element to the front 355 xyz = np.transpose(xyz, (2, 4, 6, 1, 3, 5, 0)) 356 357 # coalesce the local dimensions so that they end up in one dimension 358 # which can be indexed by the Face enum to extract a certain field 359 xyz = np.reshape(xyz, (8, nk, nj, ni, 3)) 360 361 return xyz
Get (x, y, z) coordinates for each corner-point.
Parameters
- grid: Cornerpoint-grid :returns: Coordinate values for each corner. Use the Face enum to index the first dimension, k, j, i coordinates to index the next three, and Dim enum to index the last dimension. Note that the first point in a face is not necessarily the point that is closest to the origin of the grid.
>>> import numpy as np
>>> import pyresito.io.ecl as ecl
>>> import pyresito.grid.cornerpoint as cp
>>> case = ecl.EclipseCase ("FOO")
>>> coord_fijkd = cp.face_coords (case.grid ())
>>> # get the midpoint of the upper face in each cell
>>> up_mid = np.average (coord_fijkd [cp.Face.UP, :, :, :, :], axis = 0)
364def horizon(grid, layer=0, top=True): 365 """Extract the points that are in a certain horizon and average them so 366 that the result is per cell and not per pillar. 367 368 :param grid: Grid structure 369 :type grid: dict 370 :param layer: The K index of the horizon. Default is the layer at the top. 371 :type layer: int 372 :param top: Whether the top face should be exported. If this is False, 373 then the bottom face is exported instead. 374 :type top: bool 375 :returns: Depth at the specified horizon for each cell center 376 :rtype: :class:`numpy.ma.array`, shape = (nk, nj, ni), 377 dtype = numpy.float64 378 """ 379 # get the dimensions of the grid. notice that this array is always given 380 # in Fortran order (since Eclipse works that way) and not in native order 381 num_i, num_j, _ = grid['DIMENS'] 382 383 # zcorn is nk * (up,down) * nj * (front,back) * ni * (left,right). we first 384 # project the particular layer and face that we want to work with, ending 385 # up with four corners for each cell. 386 vertical = 0 if top else 1 387 corners = grid['ZCORN'][layer, vertical, :, :, :, :] 388 389 # move the second and fourth dimension (front/back and left/right) to the 390 # back and collapse them into one dimension, meaning that we get 391 # (nj, ni, 4). then take the average across this last dimension, ending up 392 # with an (nj, ni) grid, which is what we return 393 heights = np.average(np.reshape(np.transpose( 394 corners, axes=(0, 2, 1, 3)), (num_j, num_i, 2*2)), axis=2) 395 396 # if there are any inactive elements, then mask out the elevation at that 397 # spot (it is not really a part of the grid) 398 actnum = grid['ACTNUM'][layer, :, :] 399 return ma.array(data=heights, mask=np.logical_not(actnum))
Extract the points that are in a certain horizon and average them so that the result is per cell and not per pillar.
Parameters
- grid: Grid structure
- layer: The K index of the horizon. Default is the layer at the top.
- top: Whether the top face should be exported. If this is False, then the bottom face is exported instead. :returns: Depth at the specified horizon for each cell center
457def horizon_pillars(grid, layer=0, top=True): 458 """Extract heights where a horizon crosses the pillars. 459 460 :param grid: Grid structure, containing both COORD and ZCORN properties. 461 :type grid: dict 462 :param layer: The K index of the horizon. Default is the layer at the top. 463 :type layer: int 464 :param top: Whether the top face should be exported. If this false is 465 False, then the bottom face is exported instead. 466 :type top: bool 467 :returns: Heights of the horizon at each pillar, in the same format as 468 the COORD matrix. 469 :rtype: :class:`numpy.ndarray`, shape = (nj+1, ni+1, 2, 3) 470 """ 471 # NumPy's average operations finds the average within arrays, but we want 472 # an operation that can take the average across two arrays 473 def avg_op(a, b): 474 return 0.5 * (a + b) 475 476 # zcorn is nk * (up,down) * nj * (front,back) * ni * (left,right). we first 477 # project the particular layer and face that we want to work with, ending 478 # up with four corners for each cell. take the average of the heights that 479 # are hinged up on each pillar to get a "smooth" surface. (this will not be 480 # the same surface that the cornerpoint grid was created from). 481 vertical = 0 if top else 1 482 points = _reduce_corners(grid['ZCORN'][layer, vertical, :, :, :, :], 483 avg_op) 484 return points
Extract heights where a horizon crosses the pillars.
Parameters
- grid: Grid structure, containing both COORD and ZCORN properties.
- layer: The K index of the horizon. Default is the layer at the top.
- top: Whether the top face should be exported. If this false is False, then the bottom face is exported instead. :returns: Heights of the horizon at each pillar, in the same format as the COORD matrix.
487def snugfit(grid): 488 """Create coordinate pillars that matches exactly the top and bottom 489 horizon of the grid cells. 490 491 This version assumes that the pillars in the grid are all strictly 492 vertical, i.e. the x- and y-coordinates will not be changed. 493 494 :param grid: Grid structure, containing both COORD and ZCORN properties. 495 :type grid: dict 496 :returns: Pillar coordinates, in the same format as the COORD matrix. 497 Note that a new matrix is returned, the original grid is not 498 altered/updated. 499 :rtype: :class:`numpy.ndarray`, shape = (nj+1, ni+1, 2, 3) 500 """ 501 # placement in the plane of each pillar is unchanged 502 x = grid['COORD'][:, :, 0, 0] 503 y = grid['COORD'][:, :, 0, 1] 504 505 # get implicit dimension of grid 506 njp1, nip1 = x.shape 507 508 # get new top and bottom from the z-coordinates; smaller z is shallower 509 top = _reduce_corners(grid['ZCORN'][0, 0, :, :, :, :], np.minimum) 510 btm = _reduce_corners(grid['ZCORN'][-1, 1, :, :, :, :], np.maximum) 511 512 # combine coordinates to a pillar grid with new z-coordinates 513 coord = np.reshape(np.dstack(( 514 x.ravel(), y.ravel(), top.ravel(), 515 x.ravel(), y.ravel(), btm.ravel())), (njp1, nip1, 2, 3)) 516 517 return coord
Create coordinate pillars that matches exactly the top and bottom horizon of the grid cells.
This version assumes that the pillars in the grid are all strictly vertical, i.e. the x- and y-coordinates will not be changed.
Parameters
- grid: Grid structure, containing both COORD and ZCORN properties. :returns: Pillar coordinates, in the same format as the COORD matrix. Note that a new matrix is returned, the original grid is not altered/updated.
520def bounding_box(corn, filtr): 521 """\ 522 Bounding box of the grid. This function will always assume that the grid 523 is aligned with the geographical axes. 524 525 :param corn: Coordinate values for each corner. This matrix can be 526 constructed with the `corner_coordinates` function. 527 :type corn: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 528 :param filtr: Active corners; use scatter of ACTNUM if no filtering 529 :type filtr: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2), 530 dtype = numpy.bool 531 :return: Bottom front right corner and top back left corner. Since 532 the matrix is returned with C ordering, it is specified the 533 opposite way of what is natural for mathematical matrices. 534 :rtype: :class:`numpy.ndarray`, shape = (2, 3), dtype = np.float64 535 536 >>> corn = corner_coordinates(grid['COORD'], grid['ZCORN']) 537 >>> bbox = bounding_box(corn, scatter(grid['ACTNUM'])) 538 >>> p, q = bbox[0, :], bbox[1, :] 539 >>> diag = np.sqrt(np.dot(q - p, q - p)) 540 """ 541 # scatter the filter mask from each cell to its corners 542 mask = np.logical_not(filtr).ravel() 543 544 # build arrays of each separate coordinate based on its inclusion 545 x_vals = ma.array(data=corn[0, :], mask=mask) 546 y_vals = ma.array(data=corn[1, :], mask=mask) 547 z_vals = ma.array(data=corn[2, :], mask=mask) 548 549 # construct bounding box that includes all coordinates in visible grid 550 bbox = np.array([[np.min(x_vals), np.min(y_vals), np.min(z_vals)], 551 [np.max(x_vals), np.max(y_vals), np.max(z_vals)]], 552 dtype=corn.dtype) 553 return bbox
Bounding box of the grid. This function will always assume that the grid is aligned with the geographical axes.
Parameters
- corn: Coordinate values for each corner. This matrix can be
constructed with the
corner_coordinates
function. - filtr: Active corners; use scatter of ACTNUM if no filtering
Returns
Bottom front right corner and top back left corner. Since the matrix is returned with C ordering, it is specified the opposite way of what is natural for mathematical matrices.
>>> corn = corner_coordinates(grid['COORD'], grid['ZCORN'])
>>> bbox = bounding_box(corn, scatter(grid['ACTNUM']))
>>> p, q = bbox[0, :], bbox[1, :]
>>> diag = np.sqrt(np.dot(q - p, q - p))
556def mass_center(corn, filtr): 557 """\ 558 Mass center of the grid. This function will always assume that the density 559 is equal throughout the field. 560 561 :param corn: Coordinate values for each corner. This matrix can be 562 constructed with the `corner_coordinates` function. 563 :type corn: :class:`numpy.ndarray`, shape=(3, nk*2*nj*2*ni*2) 564 :param filtr: Active corners; use scatter of ACTNUM if no filtering 565 :type filtr: :class:`numpy.ndarray`, shape = (nk, 2, nj, 2, ni, 2), 566 dtype = numpy.bool 567 :return: Center of mass. This should be the focal point of the grid. 568 :rtype: :class:`numpy.ndarray`, shape = (3,), dtype = np.float64 569 570 >>> corn = cp.corner_coordinates(grid['COORD'], grid['ZCORN']) 571 >>> focal_point = cp.mass_center(corn, cp.scatter(grid['ACTNUM'])) 572 """ 573 # scatter the filter mask from each cell to its corners 574 mask = np.logical_not(filtr).ravel() 575 576 # build arrays of each separate coordinate based on its inclusion 577 x_vals = ma.array(data=corn[0, :], mask=mask) 578 y_vals = ma.array(data=corn[1, :], mask=mask) 579 z_vals = ma.array(data=corn[2, :], mask=mask) 580 581 # use this as a coarse approximation to find the element center 582 center = np.array([np.average(x_vals), 583 np.average(y_vals), 584 np.average(z_vals)], dtype=corn.dtype) 585 return center
Mass center of the grid. This function will always assume that the density is equal throughout the field.
Parameters
- corn: Coordinate values for each corner. This matrix can be
constructed with the
corner_coordinates
function. - filtr: Active corners; use scatter of ACTNUM if no filtering
Returns
Center of mass. This should be the focal point of the grid.
>>> corn = cp.corner_coordinates(grid['COORD'], grid['ZCORN'])
>>> focal_point = cp.mass_center(corn, cp.scatter(grid['ACTNUM']))