def face_center(self, i): """Return the coordinates (x, y, z) of the center of the face number i""" if i == 0: kx1, ky1, kz1 = 0, 0, 0 kx2, ky2, kz2 = 0, 0, -1 kx3, ky3, kz3 = -1, 0, 0 kx4, ky4, kz4 = -1, 0, -1 elif i == 1: kx1, ky1, kz1 = -1, 0, 0 kx2, ky2, kz2 = -1, 0, -1 kx3, ky3, kz3 = -1, -1, 0 kx4, ky4, kz4 = -1, -1, -1 elif i == 2: kx1, ky1, kz1 = 0, -1, 0 kx2, ky2, kz2 = 0, -1, -1 kx3, ky3, kz3 = -1, -1, 0 kx4, ky4, kz4 = -1, -1, -1 elif i == 3: kx1, ky1, kz1 = 0, 0, 0 kx2, ky2, kz2 = 0, 0, -1 kx3, ky3, kz3 = 0, -1, 0 kx4, ky4, kz4 = 0, -1, -1 elif i == 4: kx1, ky1, kz1 = 0, 0, 0 kx2, ky2, kz2 = 0, -1, 0 kx3, ky3, kz3 = -1, 0, 0 kx4, ky4, kz4 = -1, -1, 0 elif i == 5: kx1, ky1, kz1 = 0, 0, -1 kx2, ky2, kz2 = 0, -1, -1 kx3, ky3, kz3 = -1, 0, -1 kx4, ky4, kz4 = -1, -1, -1 else: logger.error(f"Invalid face number {i} (must be between 0 and 5)") (x1, y1, z1) = self.pos[:, kz1, ky1, kx1] (x2, y2, z2) = self.pos[:, kz2, ky2, kx2] (x3, y3, z3) = self.pos[:, kz3, ky3, kx3] (x4, y4, z4) = self.pos[:, kz4, ky4, kx4] return ( 0.25 * (x1 + x2 + x3 + x4), 0.25 * (y1 + y2 + y3 + y4), 0.25 * (z1 + z2 + z3 + z4), )
def check_connectivity(self): """Check element connectivity, specifically for matching boundary conditions and geometry. Errors are reported as logging messages. """ dim = self.ndim err = False for (iel, el), ibc, iface in product(enumerate(self.elem), range(self.nbc), range(2 * dim)): cbc = el.bcs[ibc, iface][0] if cbc == "E" or cbc == "P": connected_iel = int(el.bcs[ibc, iface][3]) - 1 connected_face = int(el.bcs[ibc, iface][4]) - 1 cbc1 = self.elem[connected_iel].bcs[ibc, connected_face][0] iel1 = int(self.elem[connected_iel].bcs[ibc, connected_face][3]) - 1 iface1 = int( self.elem[connected_iel].bcs[ibc, connected_face][4]) - 1 if iel1 < 0 or iel1 >= self.nel: err = True logger.error( f"face {iface} of element {iel} is connected to face " f"{connected_face} of the nonexistent element {connected_iel}" ) else: if cbc1 != cbc: err = True logger.error( "mismatched boundary conditions: " f"face {iface + 1} of element {iel + 1} with " f"condition {cbc} is connected to face {connected_face + 1} " f"of element {connected_iel + 1} with condition {cbc1}" ) if iel1 != iel: err = True logger.error( "mismatched elements: " f"face {iface + 1} of element {iel + 1} " f"is connected to face {connected_face + 1} " f"of element {connected_iel + 1} but that face is " f"connected to face {iface + 1} of element {iel + 1}" ) if iface1 != iface: err = True logger.error( "mismatched faces: " f"face {iface + 1} of element {iel + 1} " f"is connected to face {connected_face + 1} " f"of element {connected_iel + 1} but that face is " f"connected to face {iface + 1} of element {iel + 1}" ) return not err
def check_connectivity(self): """Check element connectivity, specifically for matching boundary conditions and geometry. Errors are reported as logging messages. """ dim = self.ndim err = False for (iel, el) in enumerate(self.elem): for ibc in range(self.nbc): for iface in range(2 * dim): cbc = el.bcs[ibc, iface][0] if cbc == 'E' or cbc == 'P': connected_iel = int(el.bcs[ibc, iface][3]) - 1 connected_face = int(el.bcs[ibc, iface][4]) - 1 cbc1 = self.elem[connected_iel].bcs[ibc, connected_face][0] iel1 = int(self.elem[connected_iel].bcs[ ibc, connected_face][3]) - 1 iface1 = int(self.elem[connected_iel].bcs[ ibc, connected_face][4]) - 1 if iel1 < 0 or iel1 >= self.nel: err = True logger.error( "face {:} of element {:} is connected to face {:} of the nonexistent element {:}" .format(iface, iel, connected_face, connected_iel)) else: if cbc1 != cbc: err = True logger.error( "mismatched boundary conditions: face {:} of element {:} with condition {:} is connected to face {:} of element {:} with condition {:}" .format(iface + 1, iel + 1, cbc, connected_face + 1, connected_iel + 1, cbc1)) if iel1 != iel: err = True logger.error( "mismatched elements: face {:} of element {:} is connected to face {:} of element {:} but that face is connected to face {:} of element {:}" .format(iface + 1, iel + 1, connected_face + 1, connected_iel + 1, iface1 + 1, iel1 + 1)) if iface1 != iface: err = True logger.error( "mismatched faces: face {:} of element {:} is connected to face {:} of element {:} but that face is connected to face {:} of element {:}" .format(iface + 1, iel + 1, connected_face + 1, connected_iel + 1, iface1 + 1, iel1 + 1)) return not err
def readre2(fname): """A function for reading .re2 files for nek5000 Parameters ---------- fname : str file name """ # try: infile = open(fname, 'rb') except OSError as e: logger.critical(f'I/O error ({e.errno}): {e.strerror}') return -1 # the header for re2 files is 80 ASCII bytes, something like # #v002 18669 2 18669 this is the hdr % header = infile.read(80).split() nel = int(header[1]) ndim = int(header[2]) # always double precision wdsz = 8 realtype = 'd' # detect endianness etagb = infile.read(4) etagL = struct.unpack('<f', etagb)[0] etagL = int(etagL * 1e5) / 1e5 etagB = struct.unpack('>f', etagb)[0] etagB = int(etagB * 1e5) / 1e5 if (etagL == 6.54321): logger.debug('Reading little-endian file\n') emode = '<' endian = 'little' elif (etagB == 6.54321): logger.debug('Reading big-endian file\n') emode = '>' endian = 'big' else: logger.error('Could not interpret endianness') return -3 # # there are no GLL points here, only quad/hex vertices lr1 = [2, 2, ndim - 1] npel = 2**ndim # the file only contains geometry var = [ndim, 0, 0, 0, 0] # allocate structure data = exdat.exadata(ndim, nel, lr1, var, 1) # # some metadata data.wdsz = wdsz data.endian = endian # # read the whole geometry into a buffer # for some reason each element is prefixed with 8 bytes of zeros, it's not clear to me why. # This is the reason for the +1 here, then the first number is ignored. buf = infile.read((ndim * npel + 1) * wdsz * nel) elem_shape = [ndim, ndim - 1, 2, 2] # nvar, lz, ly, lx for (iel, el) in enumerate(data.elem): fi = np.frombuffer(buf, dtype=emode + realtype, count=ndim * npel + 1, offset=(ndim * npel + 1) * wdsz * iel) # the data is stored in the following order (2D|3D): # x1, x2, x4, x3; | x5, x6, x8, x7; # y1, y2, y4, y3; | y5, y6, y8, y7; # ---------------- # z1, z2, z4, z3; z5, z6, z8, z7; # where 1-8 is the ordering of the points in memory for idim in range(ndim): # x, y, [z] for iz in range( ndim - 1 ): # this does only one iteration in 2D, and in 3D does one iteration for 1-4 and one for 5-8 el.pos[idim, iz, 0, 0] = fi[npel * idim + 4 * iz + 1] el.pos[idim, iz, 0, 1] = fi[npel * idim + 4 * iz + 2] el.pos[idim, iz, 1, 1] = fi[npel * idim + 4 * iz + 3] el.pos[idim, iz, 1, 0] = fi[npel * idim + 4 * iz + 4] # # read curved sides # the number of curved sides is stored as a double, # then each curved side is 64 bytes: # iel iface p1 p2 p3 p4 p5 ctype # where p1-5 are f64 parameters and ctype is the type of curvature in ASCII ncparam = 8 buf = infile.read(wdsz) ncurv = int(np.frombuffer(buf)[0]) logger.debug(f'Found {ncurv} curved sides') data.ncurv = ncurv buf = infile.read(wdsz * (ncparam * ncurv)) for icurv in range(ncurv): # interpret the data curv = np.frombuffer(buf, dtype=emode + realtype, count=ncparam, offset=icurv * ncparam * wdsz) iel = int(curv[0]) - 1 iedge = int(curv[1]) - 1 cparams = curv[2:7] # select only the first byte, because it turns out the later bytes may contain garbage. # typically, it's b'C\x00\x00\x00\x00\x00\x00\x00' or b'C\x00\x00\x00\x00\x00\xe0?'. # AFAIK the curvature types are always one character long anyway. ctype = curv[7].tobytes()[:1].decode('utf-8') # fill in the data structure data.elem[iel].curv[iedge, :] = cparams data.elem[iel].ccurv[iedge] = ctype # # read boundary conditions # there can be more than one field, and we won't know until we'vre reached the end nbcparam = 8 buf = infile.read(wdsz) ifield = 0 while buf != b'': # the data is initialized with one BC field, we might need to allocate another if ifield > 0: data.nbc = data.nbc + 1 for el in data.elem: empty_bcs = np.zeros(el.bcs[:1, :].shape, dtype=el.bcs.dtype) el.bcs = np.concatenate((el.bcs, empty_bcs)) nbclines = int(np.frombuffer(buf)[0]) logger.debug( f'Found {nbclines} external boundary conditions for field {ifield}' ) buf = infile.read(wdsz * (nbcparam * nbclines)) for ibc in range(nbclines): # interpret the data bc = np.frombuffer(buf, dtype=emode + realtype, count=nbcparam, offset=ibc * nbcparam * wdsz) iel = int(bc[0]) - 1 iface = int(bc[1]) - 1 bcparams = bc[2:7] bctype = bc[7].tobytes().decode( 'utf-8').rstrip() # remove trailing spaces # fill in the data structure data.elem[iel].bcs[ifield, iface][0] = bctype data.elem[iel].bcs[ifield, iface][1] = iel + 1 data.elem[iel].bcs[ifield, iface][2] = iface + 1 for ipar in range(5): data.elem[iel].bcs[ifield, iface][3 + ipar] = bcparams[ipar] ifield = ifield + 1 # try reading the number of conditions in the next field buf = infile.read(wdsz) infile.close() return data
def writenek(fname, data): """A function for writing binary data in the nek5000 binary format Parameters ---------- fname : str file name data : exadata data organised as readnek() output """ # try: outfile = open(fname, 'wb') except OSError as e: logger.critical(f'I/O error ({e.errno}): {e.strerror}') return -1 # #--------------------------------------------------------------------------- # WRITE HEADER #--------------------------------------------------------------------------- # # multiple files (not implemented) fid = 0 nf = 1 nelf = data.nel # # get fields to be written vars = '' if (data.var[0] > 0): vars += 'X' if (data.var[1] > 0): vars += 'U' if (data.var[2] > 0): vars += 'P' if (data.var[3] > 0): vars += 'T' if (data.var[4] > 0): # TODO: check if header for scalars are written with zeros filled as S01 vars += 'S{:02d}'.format(data.var[4]) # # get word size if (data.wdsz == 4): logger.debug('Writing single-precision file') elif (data.wdsz == 8): logger.debug('Writing double-precision file') else: logger.error('Could not interpret real type (wdsz = %i)' % (data.wdsz)) return -2 # # generate header header = '#std %1i %2i %2i %2i %10i %10i %20.13E %9i %6i %6i %s' % ( data.wdsz, data.lr1[0], data.lr1[1], data.lr1[2], data.nel, nelf, data.time, data.istep, fid, nf, vars) # # write header header = header.ljust(132) outfile.write(header.encode('utf-8')) # # decide endianness if data.endian in ('big', 'little'): byteswap = data.endian != sys.byteorder logger.debug(f'Writing {data.endian}-endian file') else: byteswap = False logger.warning(f'Unrecognized endianness {data.endian}, ' f'writing native {sys.byteorder}-endian file') # def correct_endianness(a): ''' Return the array with the requested endianness''' if byteswap: return a.byteswap() else: return a # # write tag (to specify endianness) endianbytes = np.array([6.54321], dtype=np.float32) correct_endianness(endianbytes).tofile(outfile) # # write element map for the file correct_endianness(data.elmap).tofile(outfile) # #--------------------------------------------------------------------------- # WRITE DATA #--------------------------------------------------------------------------- # # compute total number of points per element npel = data.lr1[0] * data.lr1[1] * data.lr1[2] # def write_ndarray_to_file(a): '''Write a data array to the output file in the requested precision and endianness''' if data.wdsz == 4: correct_endianness(a.astype(np.float32)).tofile(outfile) else: correct_endianness(a).tofile(outfile) # # write geometry for iel in data.elmap: for idim in range( data.var[0]): # if var[0] == 0, geometry is not written write_ndarray_to_file(data.elem[iel - 1].pos[idim, :, :, :]) # # write velocity for iel in data.elmap: for idim in range( data.var[1]): # if var[1] == 0, velocity is not written write_ndarray_to_file(data.elem[iel - 1].vel[idim, :, :, :]) # # write pressure for iel in data.elmap: for ivar in range( data.var[2]): # if var[2] == 0, pressure is not written write_ndarray_to_file(data.elem[iel - 1].pres[ivar, :, :, :]) # # write temperature for iel in data.elmap: for ivar in range( data.var[3]): # if var[3] == 0, temperature is not written write_ndarray_to_file(data.elem[iel - 1].temp[ivar, :, :, :]) # # write scalars # # NOTE: This is not a bug! # Unlike other variables, scalars are in the outer loop and elements # are in the inner loop # for ivar in range(data.var[4]): # if var[4] == 0, scalars are not written for iel in data.elmap: write_ndarray_to_file(data.elem[iel - 1].scal[ivar, :, :, :]) # # write max and min of every field in every element (forced to single precision) if (data.ndim == 3): # for iel in data.elmap: for idim in range(data.var[0]): correct_endianness( np.min(data.elem[iel - 1].pos[idim, :, :, :]).astype( np.float32)).tofile(outfile) correct_endianness( np.max(data.elem[iel - 1].pos[idim, :, :, :]).astype( np.float32)).tofile(outfile) for iel in data.elmap: for idim in range(data.var[1]): correct_endianness( np.min(data.elem[iel - 1].vel[idim, :, :, :]).astype( np.float32)).tofile(outfile) correct_endianness( np.max(data.elem[iel - 1].vel[idim, :, :, :]).astype( np.float32)).tofile(outfile) for iel in data.elmap: for ivar in range(data.var[2]): correct_endianness( np.min(data.elem[iel - 1].pres[ivar, :, :, :]).astype( np.float32)).tofile(outfile) correct_endianness( np.max(data.elem[iel - 1].pres[ivar, :, :, :]).astype( np.float32)).tofile(outfile) for iel in data.elmap: for ivar in range(data.var[3]): correct_endianness( np.min(data.elem[iel - 1].temp[ivar, :, :, :]).astype( np.float32)).tofile(outfile) correct_endianness( np.max(data.elem[iel - 1].temp[ivar, :, :, :]).astype( np.float32)).tofile(outfile) for iel in data.elmap: for ivar in range(data.var[4]): correct_endianness( np.min(data.elem[iel - 1].scal[ivar, :, :, :]).astype( np.float32)).tofile(outfile) correct_endianness( np.max(data.elem[iel - 1].scal[ivar, :, :, :]).astype( np.float32)).tofile(outfile) # close file outfile.close() # # output return 0
def readnek(fname): """A function for reading binary data from the nek5000 binary format Parameters ---------- fname : str file name """ # try: infile = open(fname, 'rb') except OSError as e: logger.critical(f'I/O error ({e.errno}): {e.strerror}') return -1 # #--------------------------------------------------------------------------- # READ HEADER #--------------------------------------------------------------------------- # # read header header = infile.read(132).split() logger.debug('Header: {}'.format(b' '.join(header).decode('utf-8'))) # get word size: single or double precision wdsz = int(header[1]) if (wdsz == 4): realtype = 'f' elif (wdsz == 8): realtype = 'd' else: logger.error('Could not interpret real type (wdsz = %i)' % (wdsz)) return -2 # # get polynomial order lr1 = [int(header[2]), int(header[3]), int(header[4])] # # compute total number of points per element npel = lr1[0] * lr1[1] * lr1[2] # # get number of physical dimensions ndim = 2 + (lr1[2] > 1) # # get number of elements nel = int(header[5]) # # get number of elements in the file nelf = int(header[6]) # # get current time time = float(header[7]) # # get current time step istep = int(header[8]) # # get file id fid = int(header[9]) # # get tot number of files nf = int(header[10]) # # get variables [XUPTS[01-99]] variables = header[11].decode('utf-8') logger.debug(f"Variables: {variables}") var = [0 for i in range(5)] for v in variables: if (v == 'X'): var[0] = ndim elif (v == 'U'): var[1] = ndim elif (v == 'P'): var[2] = 1 elif (v == 'T'): var[3] = 1 elif (v == 'S'): # For example: variables = 'XS44' index_s = variables.index('S') nb_scalars = int(variables[index_s + 1:]) var[4] = nb_scalars # # identify endian encoding etagb = infile.read(4) etagL = struct.unpack('<f', etagb)[0] etagL = int(etagL * 1e5) / 1e5 etagB = struct.unpack('>f', etagb)[0] etagB = int(etagB * 1e5) / 1e5 if (etagL == 6.54321): logger.debug('Reading little-endian file\n') emode = '<' elif (etagB == 6.54321): logger.debug('Reading big-endian file\n') emode = '>' else: logger.error('Could not interpret endianness') return -3 # # read element map for the file elmap = infile.read(4 * nelf) elmap = list(struct.unpack(emode + nelf * 'i', elmap)) # #--------------------------------------------------------------------------- # READ DATA #--------------------------------------------------------------------------- # # initialize data structure data = exdat.exadata(ndim, nel, lr1, var, 0) data.time = time data.istep = istep data.wdsz = wdsz data.elmap = np.array(elmap, dtype=np.int32) if (emode == '<'): data.endian = 'little' elif (emode == '>'): data.endian = 'big' # def read_file_into_data(data_var, index_var): """Read binary file into an array attribute of ``data.elem``""" fi = infile.read(npel * wdsz) fi = np.frombuffer(fi, dtype=emode + realtype, count=npel) # Replace elem array in-place with # array read from file after reshaping as elem_shape = lr1[::-1] # lz, ly, lx data_var[index_var, ...] = fi.reshape(elem_shape) # # read geometry for iel in elmap: el = data.elem[iel - 1] for idim in range(var[0]): # if var[0] == 0, geometry is not read read_file_into_data(el.pos, idim) # # read velocity for iel in elmap: el = data.elem[iel - 1] for idim in range(var[1]): # if var[1] == 0, velocity is not read read_file_into_data(el.vel, idim) # # read pressure for iel in elmap: el = data.elem[iel - 1] for ivar in range(var[2]): # if var[2] == 0, pressure is not read read_file_into_data(el.pres, ivar) # # read temperature for iel in elmap: el = data.elem[iel - 1] for ivar in range(var[3]): # if var[3] == 0, temperature is not read read_file_into_data(el.temp, ivar) # # read scalar fields # # NOTE: This is not a bug! # Unlike other variables, scalars are in the outer loop and elements # are in the inner loop # for ivar in range(var[4]): # if var[4] == 0, scalars are not read for iel in elmap: el = data.elem[iel - 1] read_file_into_data(el.scal, ivar) # # # close file infile.close() # # output return data
def writenek(fname, data): """A function for writing binary data in the nek5000 binary format Parameters ---------- fname : str file name data : exadata data organised as readnek() output """ # try: outfile = open(fname, 'wb') except IOError as e: logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror)) return -1 # #--------------------------------------------------------------------------- # WRITE HEADER #--------------------------------------------------------------------------- # # multiple files (not implemented) fid = 0 nf = 1 nelf = data.nel # # get fields to be written vars = '' if (data.var[0] > 0): vars += 'X' if (data.var[1] > 0): vars += 'U' if (data.var[2] > 0): vars += 'P' if (data.var[3] > 0): vars += 'T' if (data.var[4] > 0): logger.warning("Writing scalar variables is an experimental feature") # TODO: check if header for scalars are written with zeros filled as S01 vars += 'S{:02d}'.format(data.var[4]) # # get word size if (data.wdsz == 4): realtype = 'f' elif (data.wdsz == 8): realtype = 'd' else: logger.error('Could not interpret real type (wdsz = %i)' % (data.wdsz)) return -2 # # generate header header = '#std %1i %2i %2i %2i %10i %10i %20.13E %9i %6i %6i %s\n' %( data.wdsz, data.lr1[0], data.lr1[1], data.lr1[2], data.nel, nelf, data.time, data.istep, fid, nf, vars) # # write header header = header.ljust(132) outfile.write(header.encode('utf-8')) # # write tag (to specify endianness) endianbytes = np.array([6.54321], dtype=np.float32) endianbytes.tofile(outfile) # # generate and write element map for the file elmap = np.linspace(1, data.nel, data.nel, dtype=np.int32) elmap.tofile(outfile) # #--------------------------------------------------------------------------- # WRITE DATA #--------------------------------------------------------------------------- # # compute total number of points per element npel = data.lr1[0] * data.lr1[1] * data.lr1[2] # def write_ndarray_to_file(a): if data.wdsz == 4: np.float32(a).tofile(outfile) else: a.tofile(outfile) # # write geometry for iel in elmap: for idim in range(data.var[0]): # if var[0] == 0, geometry is not written write_ndarray_to_file(data.elem[iel-1].pos[idim, :, :, :]) # # write velocity for iel in elmap: for idim in range(data.var[1]): # if var[1] == 0, velocity is not written write_ndarray_to_file(data.elem[iel-1].vel[idim, :, :, :]) # # write pressure for iel in elmap: for ivar in range(data.var[2]): # if var[2] == 0, pressure is not written write_ndarray_to_file(data.elem[iel-1].pres[ivar, :, :, :]) # # write temperature for iel in elmap: for ivar in range(data.var[3]): # if var[3] == 0, temperature is not written write_ndarray_to_file(data.elem[iel-1].temp[ivar, :, :, :]) # # write scalars # # NOTE: This is not a bug! # Unlike other variables, scalars are in the outer loop and elements # are in the inner loop # for ivar in range(data.var[4]): # if var[4] == 0, scalars are not written for iel in elmap: write_ndarray_to_file(data.elem[iel-1].scal[ivar, :, :, :]) # # write max and min of every field in every element (forced to single precision) if (data.ndim==3): # for iel in elmap: for idim in range(data.var[0]): np.min(data.elem[iel-1].pos[idim, :,:,:]).tofile(outfile) np.max(data.elem[iel-1].pos[idim, :,:,:]).tofile(outfile) for idim in range(data.var[1]): np.min(data.elem[iel-1].vel[idim, :,:,:]).tofile(outfile) np.max(data.elem[iel-1].vel[idim, :,:,:]).tofile(outfile) for idim in range(data.var[2]): np.min(data.elem[iel-1].pres[idim, :,:,:]).tofile(outfile) np.max(data.elem[iel-1].pres[idim, :,:,:]).tofile(outfile) for idim in range(data.var[3]): np.min(data.elem[iel-1].temp[idim, :,:,:]).tofile(outfile) np.max(data.elem[iel-1].temp[idim, :,:,:]).tofile(outfile) # close file outfile.close() # # output return 0
def merge(self, other, tol=1e-9, ignore_empty=True): """ Merges another exadata into the current one and connects it Parameters ---------- other: exadata mesh to merge into self tol: float maximum distance at which points are considered identical ignore_empty: bool if True, the faces with an empty boundary condition ('') will be treated as internal faces and will not be merged. This is useful if internal boundary conditions are not defined and will make the operation *much* faster, but requires boundary conditions to be defined on the faces to be merged. """ # perform some consistency checks if self.ndim != other.ndim: logger.error( f"Cannot merge meshes of dimensions {self.ndim} and {other.ndim}!" ) return -1 if self.lr1[0] != other.lr1[0]: logger.error( "Cannot merge meshes of different polynomial orders ({} != {})" .format(self.lr1[0], other.lr1[0])) return -2 # add the new elements (in an inconsistent state if there are internal boundary conditions) nel1 = self.nel self.nel = self.nel + other.nel self.ncurv = self.ncurv + other.ncurv self.elmap = np.concatenate((self.elmap, other.elmap + nel1)) # the deep copy is required here to avoid leaving the 'other' mesh in an inconsistent state by modifying its elements self.elem = self.elem + copy.deepcopy(other.elem) # check how many boundary condition fields we have nbc = min(self.nbc, other.nbc) # correct the boundary condition numbers: # the index of the elements and neighbours have changed for iel, ibc, iface in product(range(nel1, self.nel), range(other.nbc), range(6)): self.elem[iel].bcs[ibc, iface][1] = iel + 1 bc = self.elem[iel].bcs[ibc, iface][0] if bc == "E" or bc == "P": neighbour = self.elem[iel].bcs[ibc, iface][3] self.elem[iel].bcs[ibc, iface][3] = neighbour + nel1 # glue common faces together # only look for the neighbour in the first BC field because it should be the same in all fields. # FIXME: this will fail to correct periodic conditions if periodic domains are merged together. nfaces = 2 * self.ndim nchanges = 0 # counter for the boundary conditions connected if nbc == 0: # Quickly exit the function logger.debug("no pairs of faces to merge") return nchanges for iel0, iface0 in product(range(nel1, self.nel), range(nfaces)): elem0 = self.elem[iel0] bc0 = elem0.bcs[0, iface0][0] if bc0 != "E" and not (ignore_empty and bc0 == ""): # boundary element, look if it can be connected to something for iel1, iface1 in product(range(nel1), range(nfaces)): elem1 = self.elem[iel1] bc1 = elem1.bcs[0, iface1][0] if bc1 != "E" and not (ignore_empty and bc1 == ""): # if the centers of the faces are close, connect them together x0, y0, z0 = elem0.face_center(iface0) x1, y1, z1 = elem1.face_center(iface1) dist2 = (x1 - x0)**2 + (y1 - y0)**2 + (z1 - z0)**2 if dist2 <= tol**2: # reconnect the periodic faces together (assumes that all fields are periodic) if bc0 == bc1 == "P": iel_p0 = int(elem0.bcs[0, iface0][3]) - 1 iel_p1 = int(elem1.bcs[0, iface1][3]) - 1 iface_p0 = int(elem0.bcs[0, iface0][4]) - 1 iface_p1 = int(elem1.bcs[0, iface1][4]) - 1 for ibc in range(nbc): elem_p0_bcs = self.elem[iel_p0].bcs[ ibc, iface_p0] elem_p1_bcs = self.elem[iel_p1].bcs[ ibc, iface_p1] elem_p0_bcs[0] = "P" elem_p1_bcs[0] = "P" elem_p0_bcs[3] = iel_p1 + 1 elem_p1_bcs[3] = iel_p0 + 1 elem_p0_bcs[4] = iface_p1 + 1 elem_p1_bcs[4] = iface_p0 + 1 for ibc in range(nbc): elem0.bcs[ibc, iface0][0] = "E" elem1.bcs[ibc, iface1][0] = "E" elem0.bcs[ibc, iface0][3] = iel1 + 1 elem1.bcs[ibc, iface1][3] = iel0 + 1 elem0.bcs[ibc, iface0][4] = iface1 + 1 elem1.bcs[ibc, iface1][4] = iface0 + 1 nchanges = nchanges + 1 logger.debug(f"merged {nchanges} pairs of faces") return nchanges