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 writere2(fname, data): """A function for writing binary .re2 files for nek5000 Parameters ---------- fname : str file name data : exadata data organised as in exadata.py """ # #--------------------------------------------------------------------------- # CHECK INPUT DATA #--------------------------------------------------------------------------- # # We could extract the corners, but for now just return an error if lr1 is too large if data.lr1 != [2, 2, data.ndim - 1]: logger.critical( 'wrong element dimensions for re2 file! {} != {}'.format( data.lr1, [2, 2, data.ndim - 1])) return -2 # if data.var[0] != data.ndim: logger.critical( 'wrong number of geometric variables for re2 file! expected {}, found {}' .format(data.ndim, data.var[0])) return -3 # # Open file try: outfile = open(fname, 'wb') except OSError as e: logger.critical(f'I/O error ({e.errno}): {e.strerror}') return -1 # #--------------------------------------------------------------------------- # WRITE HEADER #--------------------------------------------------------------------------- # # always double precision wdsz = 8 realtype = 'd' nel = data.nel ndim = data.ndim header = f'#v002{nel:9d}{ndim:3d}{nel:9d} this is the hdr' header = header.ljust(80) 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 DATA #--------------------------------------------------------------------------- # # compute total number of points per element npel = 2**ndim # def write_data_to_file(a): '''Write the geometry of an element to the output file in double precision''' correct_endianness(a).tofile(outfile) # # write geometry (adding eight bytes of zeros before each element) xyz = np.zeros( (npel * ndim + 1, ) ) # array storing reordered geometry data (with a zero in the first position) for el in data.elem: # 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 xyz[npel * idim + 4 * iz + 1] = el.pos[idim, iz, 0, 0] xyz[npel * idim + 4 * iz + 2] = el.pos[idim, iz, 0, 1] xyz[npel * idim + 4 * iz + 3] = el.pos[idim, iz, 1, 1] xyz[npel * idim + 4 * iz + 4] = el.pos[idim, iz, 1, 0] write_data_to_file(xyz) # # write curve sides data # locate curved edges curved_edges = [] for (iel, el) in enumerate(data.elem): for iedge in range(12): if el.ccurv[iedge] != '': curved_edges.append((iel, iedge)) # write number of curved edges ncurv = len(curved_edges) if ncurv != data.ncurv: logger.warning( f'wrong number of curved edges: expected {data.ncurv}, found {ncurv}' ) ncurvf = np.array([ncurv], dtype=np.float64) write_data_to_file(ncurvf) # format curve data cdata = np.zeros((ncurv, ), dtype='f8, f8, f8, f8, f8, f8, f8, S8') for (cdat, (iel, iedge)) in zip(cdata, curved_edges): el = data.elem[iel] cdat[0] = iel + 1 cdat[1] = iedge + 1 # curve parameters for j in range(5): cdat[2 + j] = el.curv[iedge, j] # encode the string as a byte array padded with spaces cdat[7] = el.ccurv[iedge].encode('utf-8') # write to file write_data_to_file(cdata) # # write boundary conditions for each field for ifield in range(data.nbc): # locate faces with boundary conditions bc_faces = [] for (iel, el) in enumerate(data.elem): for iface in range(2 * ndim): bctype = el.bcs[ifield, iface][0] # internal boundary conditions are not written to .re2 files by reatore2 # and are apparently ignored by Nek5000 even in .rea files if bctype != '' and bctype != 'E': bc_faces.append((iel, iface)) nbcs = len(bc_faces) nbcsf = np.array([nbcs], dtype=np.float64) write_data_to_file(nbcsf) # initialize and format data bcdata = np.zeros((nbcs, ), dtype='f8, f8, f8, f8, f8, f8, f8, S8') for (bc, (iel, iface)) in zip(bcdata, bc_faces): el = data.elem[iel] bc[0] = iel + 1 bc[1] = iface + 1 for j in range(5): bc[2 + j] = el.bcs[ifield, iface][3 + j] # encode the string as a byte array padded with spaces bc[7] = el.bcs[ifield, iface][0].encode('utf-8').ljust(8) # write to file write_data_to_file(bcdata) # # close file outfile.close() # rerurn 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