Source code for polytex.io

# !/usr/bin/env python
# -*- coding: utf-8 -*-

import contextlib
import os
import pickle
import re
import logging
import shutil
import copy
import glob
import zipfile
from zipfile import ZipFile

from tkinter import Tk, filedialog, messagebox
from tqdm import trange
from itertools import cycle

import numpy as np
import pandas as pd
import pyvista as pv
import sympy
import vtk
from numpy.compat import os_fspath
from numpy.lib import format
from PIL import Image
from scipy.interpolate import RectBivariateSpline

from .kriging.curve2D import addPoints
from .misc import gebart, perm_rotation
from .thirdparty.bcolors import bcolors
from .__dataset__ import example

import matplotlib as mpl
# if os.environ.get('DISPLAY','') == '':
#     print('no display found. Using non-interactive Agg backend')
#     mpl.use('Agg')


file_header = """/*--------------------------------*- C++ -*----------------------------------*\\
  =========                 |
  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
   \\    /   O peration     | Website:  https://openfoam.org
    \\  /    A nd           | Version:  8
     \\/     M anipulation  |
\*---------------------------------------------------------------------------*/"""

top_separator = """// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
"""

bottom_separator = """
// ************************************************************************* //"""


[docs]def write_FoamFile(ver, fmt, cls, location, obj, top_separator): return """ FoamFile { version %.1f; format %s; class %s; location %s; object %s; } %s """ % (ver, fmt, cls, location, obj, top_separator) # 格式化输出
## boundaryFields for vol<Type>Field """ Left: x- TOP : z + Front: y + """ bFields = """ boundaryField { top { type zeroGradient; } bottom { type zeroGradient; } left { type zeroGradient; } right { type zeroGradient; } back { type zeroGradient; } front { type zeroGradient; } } """
[docs]def mkdir(path): """ Create the output directory if it does not exist. Parameters ---------- path: str The path of the output directory. Returns ------- output_dir: str The path of the output directory. """ output_dir = path + "/constant/polyMesh" cellDataDir = path + "/0" try: # os.mkdir(output_dir) # 创建单层目录 os.makedirs(output_dir) # 创建多层目录 os.makedirs(cellDataDir) except FileExistsError: logging.warning("Directory already exists. Aborting to be safe.") return output_dir
[docs]def write_points(points, output_dir='./constant/polyMesh/', scale=1.0): """ Write points to OpenFOAM format Parameters ---------- points : array-like The points to be written. The shape of the array should be (n, 3). output_dir : str, optional The directory to store the converted data. The default is './', which means the current directory. scale : float, optional The scale factor to convert the unit of points. The default is 1.0. Returns ------- : int 1 if the writing is successful. """ if not os.path.exists(output_dir): os.makedirs(output_dir) if not isinstance(points, np.ndarray): points = np.array(points) n_points = points.shape[0] # np: number of points print("Points data writing...") points_file = os.path.join(output_dir, "points") with open(points_file, "w") as f: f.write(file_header) f.write(write_FoamFile(2.0, "ascii", "vectorField", "\"constant/polyMesh\"", "points", top_separator)) f.write("%d\n(\n" % n_points) for i in trange(n_points): point = points[i] * scale f.write("(%f %f %f)\n" % tuple(point)) f.write(")\n") f.write(bottom_separator) f.close() print(" Points data writing finished.") return 1
[docs]def write_cell_data(cellDataDict, outputDir='./0/', array_list=None): """ Write cell data to OpenFOAM format Parameters ---------- cellDataDict : dict A dictionary to store all cell data sets in vtu file using the data set name as key. outputDir : str, optional The directory to store the converted data. The default is './0/'. array_list : list, optional A list containing the names of the cell data to be written. The default is None, """ if not os.path.exists(outputDir): os.makedirs(outputDir) else: shutil.rmtree(outputDir) os.makedirs(outputDir) keys = cellDataDict.keys() print("Cell data writing...") for key in keys: # skip the cell data not in the array_list if array_list is not None if array_list is not None and key not in array_list: continue print(" - " + key) cellData = cellDataDict[key] cellDataFile = os.path.join(outputDir, key) fileData = open(cellDataFile, "w") fileData.write(file_header) totalItem = cellData.shape[0] if cellData.ndim == 1: n_components = 1 fileData.write(write_FoamFile(2.0, "ascii", "volScalarField", "\"0\"", key, top_separator)) fileData.write("dimensions [0 0 0 0 0 0 0];\n") fileData.write("internalField nonuniform List<scalar>\n ") fileData.write("%d\n(\n" % totalItem) elif cellData.ndim == 2 and cellData.shape[1] == 3: n_components = 3 fileData.write(write_FoamFile(2.0, "ascii", "volVectorField", "\"0\"", key, top_separator)) fileData.write("dimensions [0 0 0 0 0 0 0];\n") fileData.write("internalField nonuniform List<vector>\n ") fileData.write("%d\n(\n" % totalItem) elif cellData.ndim == 2 and cellData.shape[1] == 9: n_components = 9 fileData.write(write_FoamFile(2.0, "ascii", "volTensorField", "\"0\"", key, top_separator)) if key == "permeability" or key == "K": fileData.write("dimensions [0 2 0 0 0 0 0];\n") elif key == "D": fileData.write("dimensions [0 -2 0 0 0 0 0];\n") fileData.write("internalField nonuniform List<tensor>\n ") fileData.write("%d\n(\n" % totalItem) for j in range(totalItem): if n_components > 1: data = cellData[j] fileData.write(str(tuple(data)).replace(",", "")) else: fileData.write(str(cellData[j])) fileData.write("\n") fileData.write(");\n") fileData.write(bFields) fileData.write(bottom_separator) fileData.close()
[docs]def cell_faces(mesh, ind, neighbor=False): """ Get all faces of a 3D cell. Parameters ---------- mesh : vtkUnstructuredGrid The volume mesh. ind : int The cell id. neighbor : bool, optional If True, return the neighbor cell ids. The default is False. Returns ------- faces : list A list containing all faces of the cell. neighbors : list A list containing all neighbor cell ids of the cell. Not returned if neighbor is False. """ cell = mesh.GetCell(ind) faces = [] neighbors = set() for i in range(cell.GetNumberOfFaces()): face_ = cell.GetFace(i) point_ids = face_.GetPointIds() face = [point_ids.GetId(j) for j in range(face_.GetNumberOfPoints())] faces.append(face) if neighbor: cell_ids = vtk.vtkIdList() mesh.GetCellNeighbors(ind, point_ids, cell_ids) neighbors.update([cell_ids.GetId(i) for i in range(cell_ids.GetNumberOfIds())]) if neighbor: return faces, list(neighbors) else: return faces
[docs]def get_internel_faces(volume): """ Extract internel faces from a vtkUnstructuredGrid object. Parameters ---------- volume : pyvista.UnstructuredGrid The volume mesh. Returns ------- internal_faces : list A list containing all internal faces. owner : list A list containing all owner cell ids corresponding to the internal faces. neighbour : list A list containing all neighbour cell ids corresponding to the internal faces. """ print("Internal faces data extracting...") n_cells = volume.n_cells cells = volume.cells.reshape(n_cells, -1)[:, 1:] internal_faces = [] owner = [] neighbour = [] for cell_id in trange(n_cells): # print(cell_id) cell = cells[cell_id] # the point ids of the cells cell_face_lst, neighbour_cell_ids = cell_faces(volume, cell_id, neighbor=True) cell_face_lst = np.array(cell_face_lst) cell_face_lst_copy = copy.deepcopy(cell_face_lst) cell_face_lst.sort(axis=1) neighbour_cell_ids = np.array(neighbour_cell_ids) mask = neighbour_cell_ids > cell_id for idx in neighbour_cell_ids[mask]: face = np.intersect1d(cell, cells[idx, :]) # sort the row of cell_faces face.sort(axis=0) mask = np.all(cell_face_lst == face, axis=1) face = cell_face_lst_copy[mask][0] internal_faces.append(face.tolist()) owner.append(cell_id) neighbour.append(idx) print(" Internal faces data extracting finished.") return internal_faces, owner, neighbour
[docs]def get_boundary_faces(volume): """ Extract boundary faces from a vtkUnstructuredGrid object. Parameters ---------- volume : pyvista.UnstructuredGrid The volume mesh. Returns ------- boundary_faces : numpy.ndarray A numpy array containing all boundary faces. surf : pyvista.PolyData A pyvista surface mesh object. """ print("Boundary faces data extracting...") surf = volume.extract_surface(pass_pointid=True, pass_cellid=True) surf = surf.cast_to_unstructured_grid() vtkOriginalPointIds = surf['vtkOriginalPointIds'] vtkOriginalCellIds = surf['vtkOriginalCellIds'] points = surf.points cells = surf.cells.reshape(-1, 5)[:, 1:] ncells = surf.n_cells cellCenters = surf.cell_centers().points # the centers of each cell # Normal of the three coordinate planes map = {"YZ": (1, 0, 0), "XZ": (0, 1, 0), "XY": (0, 0, 1)} axis = [map.get("YZ"), map.get("XZ"), map.get("XY"), ] bbox = surf.bounds # the bounding box of the surface mesh centroid = surf.center # the center of the surface mesh face_boundary = np.zeros((ncells, 4), dtype=int) owner_boundary = np.zeros(ncells, dtype=int) for i in trange(ncells): originalCellId = vtkOriginalCellIds[i] originalPointId = vtkOriginalPointIds[cells[i]] cell_face_lst = np.array(cell_faces(volume, originalCellId)) cell_face_lst_copy = copy.deepcopy(cell_face_lst) # sort the row of cell_faces cell_face_lst.sort(axis=1) originalPointId.sort(axis=0) mask = np.all(cell_face_lst == originalPointId, axis=1) face_boundary[i] = cell_face_lst_copy[mask] owner_boundary[i] = originalCellId vector_in_face = points[cells[:, 0]] - cellCenters mask_center_x = cellCenters[:, 0] < centroid[0] mask_center_y = cellCenters[:, 1] < centroid[1] mask_center_z = cellCenters[:, 2] < centroid[2] mask_yz = np.dot(vector_in_face, axis[0]) == 0 mask_xz = np.abs(np.dot(vector_in_face, axis[1])) == 0 mask_xy = np.dot(vector_in_face, axis[2]) == 0 face_boundary_left = face_boundary[mask_yz & mask_center_x] face_boundary_right = face_boundary[mask_yz & ~mask_center_x] face_boundary_back = face_boundary[mask_xz & mask_center_y] face_boundary_front = face_boundary[mask_xz & ~mask_center_y] face_boundary_bottom = face_boundary[mask_xy & mask_center_z] face_boundary_top = face_boundary[mask_xy & ~mask_center_z] owner_boundary_left = owner_boundary[mask_yz & mask_center_x] owner_boundary_right = owner_boundary[mask_yz & ~mask_center_x] owner_boundary_back = owner_boundary[mask_xz & mask_center_y] owner_boundary_front = owner_boundary[mask_xz & ~mask_center_y] owner_boundary_bottom = owner_boundary[mask_xy & mask_center_z] owner_boundary_top = owner_boundary[mask_xy & ~mask_center_z] face_boundary_dict = {"left": face_boundary_left, "right": face_boundary_right, "back": face_boundary_back, "front": face_boundary_front, "bottom": face_boundary_bottom, "top": face_boundary_top} owner_boundary_dict = {"left": owner_boundary_left, "right": owner_boundary_right, "back": owner_boundary_back, "front": owner_boundary_front, "bottom": owner_boundary_bottom, "top": owner_boundary_top} return face_boundary_dict, owner_boundary_dict
[docs]def write_cell_zone(cell_zone, output_dir='./constant/polyMesh/', ): """ Write the cells to a file for porous properties setting. Parameters ---------- cell_zone : dict A dict containing all cell ids corresponding to the cell zones. The key is the cell zone name and the value (a list) is the cell ids in the zone. output_dir : str, optional The output directory. The default is './constant/polyMesh/'. Returns ------- None. """ if not isinstance(cell_zone, dict): print("The type of cell_zone should be dict with key as " "the zone name and value (list) as cell ids in the zone.") print("Owner of faces data writing failed.") return 0 num_zones = len(cell_zone) print("Cell zone data writing...") zone_file = open(os.path.join(output_dir, "cellZones"), "w") zone_file.write(file_header) zone_file.write(write_FoamFile(2.0, "ascii", "regIOobject", "\"constant/polyMesh\"", "cellZones", top_separator)) zone_file.write("%d\n(\n" % num_zones) """ Write cell zones as dictionary """ for key, value in cell_zone.items(): zone_file.write("%s\n{\n type cellZone; \ncellLabels List<label>\n" % key) zone_file.write(str(len(value)) + "\n( \n") for cell_id in value: zone_file.write(str(cell_id) + '\n') zone_file.write(")\n;\n}\n") zone_file.write(")\n") zone_file.write(bottom_separator) zone_file.close() print(" Cell zone data writing finished.") return num_zones
[docs]def write_neighbors(neighbour, output_dir='./constant/polyMesh/'): """ Write the neighbors file. Parameters ---------- neighbour : list A list containing all neighbor cell ids corresponding to the internal faces. output_dir : str, optional The output directory. The default is './constant/polyMesh/'. Returns ------- None. """ print("Neighbors data writing...") neighbour_file = open(os.path.join(output_dir, "neighbour"), "w") neighbour_file.write(file_header) neighbour_file.write( write_FoamFile(2.0, "ascii", "labelList", "\"constant/polyMesh\"", "neighbour", top_separator)) neighbour_file.write("%d\n(\n" % len(neighbour)) for i in neighbour: neighbour_file.write(str(i) + '\n') neighbour_file.write(")\n") neighbour_file.write(bottom_separator) neighbour_file.close() print(" Neighbors data writing finished.")
[docs]def write_owner(owner_internal, owner_boundary, output_dir='./constant/polyMesh/', ): """ Write the owner file. Parameters ---------- owner_internal : array-like A list containing all owner cell ids corresponding to the internal faces. owner_boundary : dict A list containing all owner cell ids corresponding to the boundary faces. output_dir : str, optional The output directory. The default is './constant/polyMesh/'. Returns ------- None. """ if not isinstance(owner_internal, np.ndarray): owner_internal = np.array(owner_internal) if not isinstance(owner_boundary, dict): print("The type of owner_boundary should be dict with key as " "boundary patch name and value as owner cell id.") print("Owner of faces data writing failed.") return 0 total_faces = owner_internal.shape[0] for key, value in owner_boundary.items(): total_faces += len(value) print("Owner of faces data writing...") owner_file = open(os.path.join(output_dir, "owner"), "w") owner_file.write(file_header) owner_file.write(write_FoamFile(2.0, "ascii", "labelList", "\"constant/polyMesh\"", "owner", top_separator)) owner_file.write("%d\n(\n" % total_faces) """ Write internal face owners first """ for i in range(len(owner_internal)): owner_file.write(str(owner_internal[i]) + '\n') """ Write boundary face owners """ for key, value in owner_boundary.items(): for cell_id in value: owner_file.write(str(cell_id) + '\n') owner_file.write(")\n") owner_file.write(bottom_separator) owner_file.close() print(" Owner of faces data writing finished.") return total_faces
[docs]def write_face(face_points): """ Parameters ---------- face_points : list A list containing all face node indices. """ return "%d(%s)\n" % (len(face_points), " ".join([str(p) for p in face_points]))
[docs]def write_faces(internal_faces, face_boundary, output_dir='./constant/polyMesh/'): """ Write the faces file. Parameters ---------- internal_faces : list A list containing all internal face node indices. face_boundary : dict A dict containing all boundary faces. The key is the boundary patch name, and the value is a numpy array containing all boundary face node indices. output_dir : str, optional The output directory. The default is './constant/polyMesh/'. Returns ------- None. """ print("faces data writing...") total_faces = len(internal_faces) for key, value in face_boundary.items(): total_faces += len(value) print(" Total faces: %d" % total_faces) faces_file = open(os.path.join(output_dir, "faces"), "w") faces_file.write(file_header) faces_file.write(write_FoamFile(2.0, "ascii", "faceList", "\"constant/polyMesh\"", "faces", top_separator)) faces_file.write("%d\n(\n" % total_faces) """ Write internal faces first """ for face in internal_faces: faces_file.write(write_face(face[::-1])) """ Write boundary faces """ for key, value in face_boundary.items(): for face in value: faces_file.write(write_face(face[::-1])) faces_file.write(")\n") faces_file.write(bottom_separator) faces_file.close() print(" faces data writing finished.") return 1
[docs]def write_boundary(face_boundary_dict, start_face, output_dir='./constant/polyMesh/', type=None): """ Boundary file writing. Parameters ---------- face_boundary_dict : dict A dict contains boundary category. The key is the boundary name and the value is a numpy array containing all boundary face node indices. The boundary name should be the same as the boundary patch name in the boundary file. start_face : int The start face index of the boundary faces. equal to the number of internal faces. output_dir : str, optional The output directory. The default is './constant/polyMesh/'. type : dict, optional The type of each boundary. The default is None. If None, the type of the boundary is set as "patch". The key is the boundary name and the value is the boundary type. The key should be the same as the face_boundary_dict. The boundary type can be "patch", "wall", "empty", "symmetryPlane", "wedge", "cyclic", etc. See OpenFOAM user guide for more details. Returns ------- None. """ print("Boundary data writing...") n_boundaries = len(face_boundary_dict) if type is None: type = {} for key in face_boundary_dict.keys(): type[key] = "patch" elif not isinstance(type, dict): print("The type of type should be dict with key as " "boundary patch name and value as boundary type.") print("Boundary data writing failed.") return 0 boundary_file = open(os.path.join(output_dir, "boundary"), "w") boundary_file.write(file_header) boundary_file.write( write_FoamFile(2.0, "ascii", "polyBoundaryMesh", "\"constant/polyMesh\"", "boundary", top_separator)) boundary_file.write("%d\n(\n " % n_boundaries) for key in list(face_boundary_dict.keys()): print(" - " + key) boundary_file.write("""%s { type %s; nFaces %d; startFace %d; } """ % (key, type[key], len(face_boundary_dict[key]), start_face)) start_face += len(face_boundary_dict[key]) boundary_file.write(")\n") boundary_file.write(bottom_separator) boundary_file.close() print(" Boundary data writing finished.") return 1
[docs]def voxel2foam(mesh, scale=1, outputDir="./", boundary_type=None, cell_data_list=None) -> None: """ Convert a voxel mesh to OpenFOAM mesh. The cell data is converted to OpenFOAM initial conditions and saved in the 0 timestep folder. Parameters ---------- mesh : pyvista.UnstructuredGrid or pyvista.DataSet The voxel mesh. scale : float, optional The scale factor to convert the unit of points. The default is 1.0. outputDir : str, optional The output directory. The default is './'. boundary_type : dict, optional The type of each boundary. The default is None. If None, the type of the boundary is set as "patch". The key is the boundary name and the value is the boundary type. The key should be the same as the face_boundary_dict. The boundary type can be "patch", "wall", "empty", "symmetryPlane", "wedge", "cyclic", etc. See OpenFOAM user guide for more details. cell_data_list : list, optional A list containing the names of the cell data to be written. The default is None. Returns ------- None. """ cwd = os.getcwd() print("Current working directory: ", cwd) if not isinstance(mesh, pv.UnstructuredGrid): raise TypeError("mesh must be a pyvista.UnstructuredGrid. If you have a vtu file, " "use pyvista.read('filename.vtu') to read the file.") # Create the output directory if it does not exist. if not os.path.exists(outputDir): path = mkdir(outputDir) else: path = outputDir os.chdir(outputDir) # set the output directory as the current working directory path_polyMesh = "./constant/polyMesh" path_0 = "./0" """ 1. Write points """ pts = mesh.points # numpy.ndarray (npts, 3) write_points(pts, output_dir=path_polyMesh, scale=scale) """ 2. Write cell data """ cellDataDict = mesh.cell_data write_cell_data(cellDataDict, outputDir=path_0, array_list=cell_data_list) """ 3. Write faces """ # Get internal faces internal_faces, owner_internal, neighbour = get_internel_faces(mesh) # Get boundary faces face_boundary_dict, owner_boundary_dict = get_boundary_faces(mesh) """ 4. Write cell zones for porous region """ cell_zone = {"porousLayer": np.arange(mesh.n_cells)[mesh.cell_data["porosity"] < 0.995]} write_cell_zone(cell_zone, output_dir=path_polyMesh) """ 5. Write neighbor file """ write_neighbors(neighbour, output_dir=path_polyMesh) """ 6. Write owner file """ write_owner(owner_internal, owner_boundary_dict, output_dir=path_polyMesh) """ 7. Write face file """ write_faces(internal_faces, face_boundary_dict, output_dir=path_polyMesh) """ 8. Write boundary file """ write_boundary(face_boundary_dict, len(internal_faces), output_dir=path_polyMesh, type=boundary_type) """ 9. Write boundary conditions """ print("Mesh writing finished!") os.chdir(cwd) # set the current working directory back to the original directory return None
# def case_preparation(src, dst, verbose=False): # """ # Copy files from the template case to the output directory # # Parameters # ---------- # src : str # The path of the template case. # dst : str # The path of the output case. # """ # # if not os.path.exists(dst + "system"): # os.makedirs(os.path.join(dst, "system")) # # # system directory # files = glob.glob(src + "system/*") # for file in files: # if os.path.isdir(file): # continue # if not os.path.exists(os.path.join(dst + file.split("/")[-1])): # if verbose: # print("Copying {} ...".format(file.split("/")[-1])) # shutil.copy(file, dst + "system/") # else: # print("File {} already exists.".format(file.split("/")[-1])) # # # constant directory # files = glob.glob(src + "constant/*") # for file in files: # if os.path.isdir(file): # neglect directories such as polyMesh # continue # # if not exist in the destination directory, copy the file # if not os.path.exists(os.path.join(dst, file.split("/")[-1])): # if verbose: # print("Copying {} ...".format(file.split("/")[-1])) # shutil.copy(file, dst + "constant/") # else: # print("File {} already exists.".format(file.split("/")[-1])) # # # 0 directory # files = glob.glob(src + "0/*") # for file in files: # print(os.path.join(dst, file.split("/")[-1])) # if not os.path.exists(os.path.join(dst, file.split("/")[-1])): # if verbose: # print("Copying {} ...".format(file.split("/")[-1])) # shutil.copy(file, dst + "0/") # else: # print("File {} already exists.".format(file.split("/")[-1])) # # # root directory # files = glob.glob(src + "*") # # copy the files in the root directory to the destination directory and neglect directories # for file in files: # if os.path.isdir(file): # continue # if not os.path.exists(os.path.join(dst, file.split("/")[-1])): # # this check here seems does not work, why? # if verbose: # print("Copying {} ...".format(file.split("/")[-1])) # shutil.copy(file, dst) # else: # print("File {} already exists.".format(file.split("/")[-1]))
[docs]def texgen_voxel(mesh, rf, perm_model="Gebart", fiber_packing="Hex", plot=False, scalar="YarnIndex", progress_bar=True): """ Read the vtu voxel mesh exported from TexGen and calculate necessary information for OpenFOAM polyMesh conversion. Parameters ---------- mesh : pyvista.DataSet The voxel mesh exported from TexGen. rf : float The fiber radius (m). perm_model : str, optional The yarn permeability model. The default is "Gebart". fiber_packing : str, optional The fiber packing pattern used for yarn permeability calculation. The default is "Hex". Valid options are "Quad" and "Hex". plot : bool, optional If True, plot the mesh. The default is False. scalar : str, optional The scalar to plot. The default is "YarnIndex". Returns ------- mesh : pyvista.UnstructuredGrid The voxel mesh with the new data. """ fvf = mesh.cell_data['VolumeFraction'] orientation = mesh.cell_data['Orientation'] if np.all(fvf == 0): raise ValueError(bcolors.WARNING + "The volume fraction is zero! Set the correct " "yarn properties before exporting the mesh from " "TexGen." + bcolors.ENDC) mesh.cell_data["porosity"] = 1 - fvf # permeability tensor in local coordinates if perm_model == "Gebart": permeability = gebart(fvf, rf, packing=fiber_packing, tensorial=True) # Permeability tensor in global coordinates perm_inv = True # inverse the permeability tensor yarn_permeability, yarn_resistance = perm_rotation(permeability, orientation, inverse=perm_inv, disable_tqdm=not progress_bar) yarn_resistance[yarn_resistance < 1e-15] = 0 # set small values to zero yarn_permeability[yarn_permeability > 1] = 0 # set large values to zero # add the new data to the mesh mesh.cell_data['K'] = yarn_permeability mesh.cell_data['D'] = yarn_resistance mesh = mesh.cast_to_unstructured_grid() if plot: mesh.plot(scalars=scalar) return mesh
[docs]def case_prepare(output_dir): """ Load openFoam case template and prepare the case for simulation. Parameters ---------- output_dir : str The output directory where the 0 and constant folders are located. Returns ------- None. """ example("case template", outdir=output_dir) with ZipFile(output_dir + "/CaseTemplate.zip", 'r') as zip_ref: filelist = zip_ref.namelist() for file in filelist: if file.split("/")[-1] in ["controlDict", "decomposeParDict", "flowRatePatch", "fvSchemes", "fvSolution"]: sys_path = os.path.join(output_dir, "system") if not os.path.exists(sys_path): os.makedirs(sys_path) with open(os.path.join(sys_path, file.split("/")[-1]), 'wb') as f: f.write(zip_ref.read(file)) elif file.split("/")[-1] in ["Allrun", "Allclean", "PyFoamFileClean.py"]: with open(os.path.join(output_dir, file.split("/")[-1]), 'wb') as f: f.write(zip_ref.read(file)) elif file.split("/")[-1] in ["F", "p", "U"]: ini_path = os.path.join(output_dir, "0") if not os.path.exists(ini_path): os.makedirs(ini_path) with open(os.path.join(ini_path, file.split("/")[-1]), 'wb') as f: f.write(zip_ref.read(file)) elif file.split("/")[-1] in ["momentumTransport", "transportProperties"]: const_path = os.path.join(output_dir, "constant") if not os.path.exists(const_path): os.makedirs(const_path) with open(os.path.join(const_path, file.split("/")[-1]), 'wb') as f: f.write(zip_ref.read(file)) zip_ref.close() os.remove(output_dir + "/CaseTemplate.zip")
# print(bcolors.OKGREEN + "Case preparation is done!" + bcolors.ENDC)
[docs]def voxel2img(mesh, mesh_shape, dataset="YarnIndex", save_path="./img/", scale=None, img_name="img", format="tif", scale_algrithm="linear"): """ Convert a voxel mesh to a series of images. Parameters ---------- mesh : pyvista.UnstructuredGrid The voxel mesh to convert. mesh_shape : list The number of cells in each direction of the mesh [nx, ny, nz]. dataset : str, optional The name of the cell data to convert. The default is "YarnIndex". save_path : str, optional The path to save the images. The default is "./img/". scale : int The scale factor of the image. The default is None. img_name : str, optional The name of the output image. The default is "img". The slice number will be added to the end of the name and separated by an underscore. format : str, optional The format of the output image. The default is "tif". scale_algrithm : str, optional The algorithm used to scale the pixel numbers of the image. The default is "linear". The other option is "spline". TODO: The "spline" algorithm is only working for x and y directions yet. The z direction is to be implemented. Returns ------- None Examples -------- >>> import pyvista as pv >>> import polytex as ptx >>> mesh = pv.read("./v2i.vtu") >>> mesh_shape = [20, 20, 5] >>> ptx.io.voxel2img(mesh, mesh_shape, dataset="YarnIndex", save_path="./img/", scale=50, img_name="img", format="tif", scale_algrithm="linear") """ nx, ny, nz = mesh_shape # number of cells in each direction yarnIndex = mesh.cell_data[dataset] + 1 yarnIndex = yarnIndex / np.max(yarnIndex) * 255 img_sequence = np.reshape(yarnIndex, [nz, nx, ny]) # print("The shape of the mesh is: ", mesh_shape) x = np.arange(0, ny) y = np.arange(0, nx) if scale is not None: nx2 = nx * scale ny2 = ny * scale x2 = np.linspace(0, ny, ny2, endpoint=True) y2 = np.linspace(0, nx, nx2, endpoint=True) if scale_algrithm == "linear": for i in range(3): img_sequence = np.repeat(img_sequence, scale, axis=i) nx = nx * scale ny = ny * scale nz = nz * scale if not os.path.exists(save_path): os.makedirs(save_path) # check if the save path is empty. If not, print a warning if os.listdir(save_path): print(os.listdir(save_path)) print("Warning: The save path is not empty! The images with the " "same name will be overwritten!") for i in range(nz): img = img_sequence[i, :, :] # interpolate the numpy array to get a smooth image if scale is not None and scale_algrithm == "spline": img = RectBivariateSpline(x, y, img, kx=3, ky=3)(x2, y2) # save the image img = Image.fromarray(img) img = img.convert('L') path = os.path.join(save_path, img_name + "_" + str(i) + "." + format) img.save(path) return None
[docs]def cwd_chdir(path=""): """ Set given directory or the folder where the code file is as current working directory Parameters ---------- path: the path of current working directory. if empty, the path of the code file is used. Returns ------- cwd: the current working directory. """ import sys if path == "": cwd = str(sys.argv[0]) cwd, pyname = os.path.split(cwd) else: cwd = path os.chdir(cwd) return cwd
[docs]def choose_directory(titl='Select the target directory:'): """ Choose a directory with GUI and return its path. Parameters ---------- titl: String. The title of the open folder dialog window. Returns ------- path: String. The path of the selected directory. """ print(titl) # pointing root to Tk() to use it as Tk() in program. # like a window (container) where we can put widgets. directory_root = Tk() directory_root.withdraw() # Hides small tkinter window. directory_root.attributes('-topmost', True) # Opened windows will be active. above all windows. path_work = filedialog.askdirectory(title=titl) # Returns opened path as str if path_work == '': top = Tk() top.withdraw() top.geometry("150x150") message = messagebox.askquestion("Warning", "You did not select any folder! " "Do you wish to select again?") if message == 'yes': return choose_directory() elif message == 'no': return None else: # replace the forward slash returned by askdirectory # with backslash (\) on Windows. return path_work.replace('/', os.sep)
[docs]def filenames(path, filter="csv"): """ Get the list of files in the given folder. Parameters ---------- path: the path of the folder filter: filter for file selection.++ Returns ------- flst: the list of files in the given folder. """ filenamels = os.listdir(path) # filter the file list by the given filter. flst = [x for x in filenamels if (filter in x)] flst.sort() return flst
[docs]def zip_files(directory, file_list, filename, remove="True"): """ Add multiple files to a zip file. Parameters ---------- directory: String. The directory of the files to be added to zip file. Therefore, all the files in the file_list should be in the same directory. file_list : List. The list of file names to be added to the zip file (without directory). filename: String. The name of the zip file. The zip file is saved in the same directory remove: Whether to remove original files after adding to zip file. Default is True. If False, the original files will not be removed. Returns ------- None. """ from zipfile import ZipFile # check extension of the zip file if filename[-4:] != ".zip": filename += ".zip" with ZipFile(filename, 'w') as zipObj: for i in range(len(file_list)): zipObj.write(directory + file_list[i]) if remove == "True": os.remove(directory + file_list[i]) print(bcolors.ok("Zip file saved as " + filename + bcolors.ENDC))
[docs]def choose_file(titl='Select the target directory:', format='csv'): """ Choose a file with GUI and return its path. Parameters ---------- titl: String. The title of the window. Returns ------- path: String. The path of the file. """ print(titl) directory_root = Tk() directory_root.withdraw() # Hides small tkinter window. directory_root.attributes('-topmost', True) # Opened windows will be active (appears above all windows) path_work = filedialog.askopenfilename( title=titl, filetypes=[(format, format), ('All files', '*.*')]) # Returns opened path as str # replace the forward slash returned by askdirectory # with backslash (\) on Windows. return path_work.replace('/', os.sep)
[docs]def save_nrrd(cell_label, file_name, file_path='./'): """ Save the labels of a hexahedral mesh to a nrrd file. The labels should be starting from 0 and increasing by 1. Parameters ---------- cell_label: numpy array(int, int, int) The cell label of the mesh. file_name: String The name of the .nrrd file. file_path: String The save path of the .nrrd file. Returns ------- None """ import nrrd indicator = np.zeros_like(cell_label) for i, label in enumerate(np.unique(cell_label)): mask = cell_label == label indicator[mask] = i header = {'space origin': [0, 0, 0], "space directions": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'space': 'left-anterior-superior'} # Write to a NRRD file with pynrrd if file_name[-5:] != '.nrrd': file_name += '.nrrd' nrrd.write(file_path + file_name, indicator, header, index_order='C') return indicator
[docs]class save_krig(dict): """ This class saves a dictonary of sympy expressions to a file in human readable form and then load as sympy expressions directly without other conversion. It is called by polytex.io.pk_save to save kriging expressions to a ".krig" file and by polytex.io.pk_load to load these files. Therefore, the class is not intended to be used directly by the user. Notes ----- This class is taken from: https://github.com/sympy/sympy/issues/7974. A bug in exec() is fixed and some modifications are made to make it fit for the purpose of this project (store the kriging expression). Examples -------- >>> import sympy >>> from polytex.io import save_krig >>> a, b = sympy.symbols('a, b') >>> d = save_krig({'a':a, 'b':b}) >>> d.save('name.krig') >>> del d >>> d2 = save_krig.load('name.krig') """ def __init__(self, *args, **kwargs): super(save_krig, self).__init__(*args, **kwargs) def __repr__(self): d = dict(self) for key in d.keys(): d[key] = sympy.srepr(d[key]) # regex is just used here to insert a new line after # each dict key, value pair to make it more readable return re.sub('(: \"[^"]*\",)', r'\1\n', d.__repr__())
[docs] def save(self, file): with open(file, 'w') as savefile: savefile.write(self.__repr__())
[docs] @classmethod def load(cls, file_path): with open(file_path, 'r') as loadfile: # Note that the variable name temp should not be the same as the other # local variables in the function, otherwise exec will not work and will # raise an NameError: name 'temp' is not defined. exec("temp =" + loadfile.read()) # convert the strings back to sympy expressions and return a new save_krig. # This is done by calling the save_krig constructor with the new dict. # locals() is used to get the sympy symbols from the exec statement above. d = locals()['temp'] for key in d.keys(): d[key] = sympy.sympify(d[key]) return cls(d)
[docs]def save(file, arr, allow_pickle=True, fix_imports=True): """ This is an exact copy of numpy.save, except that it does not check the extensions. Parameters ---------- file : file, str, or pathlib.Path. File or filename to which the data is saved. arr : array_like. Array data to be saved. allow_pickle : bool, optional Allow saving object arrays using Python pickles. Reasons for disallowing pickles include security (loading pickled data can execute arbitrary code) and portability (pickled objects may not be loadable on different Python installations, for example if the stored objects require libraries that are not available, and not all pickled data is compatible between Python 2 and Python 3). Default: True fix_imports : bool, optional Only useful in forcing objects in object arrays on Python 3 to be pickled in a Python 2 compatible way. If `fix_imports` is True, pickle will try to map the new Python 3 names to the old module names used in Python 2, so that the pickle data stream is readable with Python 2. """ if hasattr(file, 'write'): file_ctx = contextlib.nullcontext(file) else: file = os_fspath(file) # if not file.endswith('.npy'): # file = file + '.npy' file_ctx = open(file, "wb") with file_ctx as fid: arr = np.asanyarray(arr) format.write_array(fid, arr, allow_pickle=allow_pickle, pickle_kwargs=dict(fix_imports=fix_imports))
[docs]def pk_save(fp, data, check_format=True): """ Save a Python dict or pandas dataframe as a file format defined in polytex (.coo, geo) file Parameters ---------- fp: str File path and name to which the data is saved. If the file name does not end with a supported file extension, a ValueError will be raised. data: Tow, Tex, or dict The data to be saved. It can be several customised file formats for polytex. Returns ------- None """ filename = os.path.basename(fp) # get file extension ext = os.path.splitext(filename)[1] if check_format: if ext == "": raise ValueError("The file extension is not given. Supported file extensions are " ".coo, .geo, .tow, .tex, and .krig.") elif ext not in ['.coo', '.geo', '.tow', '.tex', '.krig']: raise ValueError("The file extension is not supported. Supported file extensions are " ".coo, .geo, .tow, .tex, and .krig.") if fp.endswith('.krig'): expr = save_krig(data) expr.save(fp) print(bcolors.ok("The Kriging function {} is saved successfully.").format(filename)) elif ext in ['.tow', '.tex']: with open(fp, 'wb') as f: pickle.dump(data, f) f.close() print(bcolors.ok("The file {} is saved successfully.").format(filename)) elif isinstance(data, pd.DataFrame): # save as .coo or .geo file data.to_pickle(fp) print(bcolors.ok("The file {} is saved successfully.").format(filename)) else: raise TypeError("The input data type is not supported.")
[docs]def pk_load(file): """ Load a file format defined in polytex (.coo, .geo, or .stat) file and return as a pandas dataframe or a numpy.array object. Parameters ---------- file: str, or pathlib.Path. File path and name to which the data is stored. Returns ------- df: pandas.DataFrame or numpy.ndarray The data to be loaded. It is a pandas dataframe if the file is a .coo/geo file. Otherwise, it is a numpy array or dict and a warning will be raised. """ filename = os.path.basename(file) ext = os.path.splitext(filename)[1] if ext == "": raise ValueError("The file extension is not given. Supported file extensions are " ".pcd, .coo, .geo, .tow, .tex, and .krig.") elif ext not in ['.pcd', '.coo', '.geo', '.tow', '.tex', '.krig', "stat"]: raise ValueError("The file extension is not supported. Supported file extensions are " ".pcd, .coo, .geo, .tow, .tex, and .krig.") if file.endswith('.krig'): print(bcolors.ok("The Kriging expression {} is loaded successfully.").format(filename)) return save_krig.load(file) if ext in ['.coo', '.geo']: data = pd.read_pickle(file) print(bcolors.ok("The file {} is loaded successfully.").format(filename)) elif ext in ['.tow', '.tex']: with open(file, 'rb') as f: data = pickle.load(f) f.close() else: data = np.load(file, allow_pickle=True, fix_imports=True).tolist() print(bcolors.ok("The file {} is loaded successfully.").format(filename)) return data
[docs]def read_imagej_roi(filename, type="zip", sort=True, resolution=1.0, max_pts=100, verbose=False): print(bcolors.WARNING + "The function read_imagej_roi is deprecated. " "Use read_explicit_data instead." + bcolors.ENDC) return read_explicit_data(filename, type, sort, resolution, max_pts, verbose)
[docs]def read_explicit_data(filename, type="zip", sort=True, resolution=1.0, max_pts=100, verbose=False): """ Read ROI data from csv files exported from manual segmentation in ImageJ/FIJI. See https://www.binyang.fun/manual-segmentation-in-imagej-fiji/ for more details. Parameters ---------- filename : str The path of the roi file. The file should be either a zip of csv files or a directory containing multiple csv files. Each csv file contains the coordinates of the segmented points on a slice. see https://www.binyang.fun/manual-segmentation-in-imagej-fiji/ for more details. The parameter "type" should be set accordingly ("zip" or "dir"). type : str, optional The type of saved file. The default is "zip". The other option is "dir". sort : bool, optional Whether to sort the coordinates according to the slice number. The default is True. Note that the coordinates on the same slice are not sorted. The sorting is only applied to the slices. resolution : float, optional The resolution of the image. The default is 1.0, the coordinates are not converted to the physical coordinates (namely the unit is pixel). max_pts : int, optional The maximum number of points on each slice. The default is 100. If the number of points on a slice is larger than max_pts, the points will be uniformly sampled to max_pts (approximately). Returns ------- surf_points : numpy.ndarray The coordinates of the segmented points on the surface of the tow in shape (N, 3), where N is the total number of points. """ if type == "zip": with zipfile.ZipFile(filename, 'r') as zip_ref: file_lst = zip_ref.namelist() for file in file_lst: with zip_ref.open(file) as f: coor_slice = np.loadtxt(f, comments=file, delimiter=",", skiprows=1) if coor_slice.shape[0] == 0: continue n_pts_org = coor_slice.shape[0] if coor_slice.shape[0] > max_pts: coor_slice = coor_slice[:: n_pts_org // max_pts] if verbose: print("Warning: The number of points {} on slice {} is larger than {}. It is " "uniformly sampled to {}.".format(n_pts_org, file, max_pts, coor_slice.shape[0])) try: coor_unsort = np.vstack((coor_unsort, coor_slice)) except NameError: coor_unsort = coor_slice elif type == "dir": print("Not implemented yet.") # sort the coordinates according to the slice number if sort: index = np.unique(coor_unsort[:, -1]) for i in index: mask = coor_unsort[:, -1] == i try: coor_sort = np.vstack((coor_sort, coor_unsort[mask])) except NameError: coor_sort = coor_unsort[mask] surf_points = coor_sort[:, 1:] * resolution else: surf_points = coor_unsort[:, 1:] * resolution return surf_points
[docs]def coo_to_ply(file_coo, file_ply, interpolate=False, threshold=0.1): """ Convert a pcd file to ply file. Parameters ---------- file_coo : str The path of the coo file or pathlib.Path. File or filename to which the data is saved. file_ply : str The path of the ply file or pathlib.Path. File or filename to which the data is to be saved. interpolate : bool, optional Whether to interpolate the points. The default is False. threshold : float, optional The threshold of the normalized distance between the neighboring points. The default is 0.1. Returns ------- None """ import meshio df = pk_load(file_coo) vertices = df.to_numpy()[:, [1, 3, 4, 5]] if interpolate: # interpolate vertices = addPoints(vertices, threshold=threshold) mesh = meshio.Mesh(points=vertices[:, 1:], cells=[], # Optionally provide extra data on points, cells, etc. point_data={"nx": vertices[:, 0], "ny": vertices[:, 1], "nz": vertices[:, 2]}, # Each item in cell data must match the cells array # cell_data={"a": [[0.1, 0.2], [0.4]]}, ) meshio.write(file_ply, mesh, binary=False)
[docs]def meshio_save(file, vertices, cells=[], point_data={}, cell_data={}, binary=False): """ Save surface mesh as a mesh file by definition of vertices and faces. Point data and cell data can be added. It is a wrapper of meshio.write() function. Parameters ---------- file : str The path of the ply file or pathlib.Path. File or filename to which the data is saved. vertices : numpy.ndarray The vertices of the mesh. The shape of the array is (n, 3), where n is the number of vertices. cells : list, optional The faces of the mesh stored as the connectivity between vertices. The default is []. point_data : dict, optional The point data of the mesh. The default is {}. cell_data : dict, optional The cell data of the mesh. The default is {}. Note that the cell data should be added as a list of arrays. Each array in the list corresponds to a cell type. For example, if the mesh has 2 triangles and 1 quad, namely, cells = [("triangle", [0, 1, 2], [1,2,3]), ("quad", [3, 4, 5, 6])], then the cell data should be added as cell_data = {"data": [[1, 2], [3]}. binary : bool, optional If True, the data is written in binary format. The default is False. Returns ------- None. Examples -------- >>> import numpy as np >>> import polytex as pk >>> vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) >>> cells = [("triangle", [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]])] >>> point_data = {"a": np.array([0, 1, 2, 3])} >>> cell_data = {"b": np.array([[0, 1, 2, 3],])} >>> ptx.meshio_save("test.ply", vertices, cells, point_data, cell_data) >>> print("Done") Done """ try: import meshio except ModuleNotFoundError: raise ImportError("This function requires meshio package but it is not installed.") mesh = meshio.Mesh(points=vertices, cells=cells, # Optionally provide extra data on points, cells, etc. point_data=point_data, # Each item in cell data must match the cells array cell_data=cell_data, ) # check if the extension in file is in [ply, stl, vtk, vtu] import os filename, file_extension = os.path.splitext(file) if file_extension in [".ply", ".stl", ".vtk", ".vtu"]: meshio.write(file, mesh, binary=binary) print(bcolors.ok("The mesh is saved as " + filename + file_extension + " file successfully." + "\n")) else: raise ValueError("The file extension is not supported. " "Please use ply, stl, vtk, or vtu.") return None
[docs]def get_ply_property(mesh_path, column, skip=11, type="vertex", save_vtk=False): """ This function get a vertex property or cell property from a mesh stored as .ply format. It is intended to be used to get the user-defined properties that most of meshing and rendering software does not support. Notes ----- The mesh must be saved as ASCII format. Parameters ---------- mesh_path : str The path of the mesh file with .ply extension. column : int or list of int The column number of the property. skip : int, optional The number of lines to skip in the header. The default is 11. type : str, optional The type of the property. The default is "vertex" for vertex property. The other possible value is "cell" for cell property. save_vtk : bool, optional If True, the mesh is saved as a vtk file. The default is False. Returns ------- property : numpy.ndarray The property of the mesh. Examples -------- >>> import polytex as pk >>> mesh_path = "./weft_0_lin_lin_krig_30pts.ply" >>> quality = ptx.get_ply_property(mesh_path, -2, skip=11, type="vertex", save_vtk=False) >>> quality """ import pyvista as pv import numpy as np mesh = pv.read(mesh_path) n_pts = mesh.n_points n_cells = mesh.n_cells # load as csv mesh_txt = np.loadtxt(mesh_path, dtype=object, delimiter=" ", skiprows=11) if type == "vertex": vertex = mesh_txt[:n_pts] quality = vertex[:, column].astype(np.float32) elif type == "cell": cell = mesh_txt[n_pts:n_pts + n_cells] quality = cell[:, column].astype(np.float32) if save_vtk: mesh["quality"] = quality mesh.save(mesh_path[:-4] + ".vtk") return quality
[docs]def save_csv(filename, dataset, csv_head): """ Save numpy array to csv file with given info in the first row. Parameters ---------- filename: The path and name of the csv file. dataset: List or numpy.ndarray The dataset to be saved in the csv file csv_head: A list of headers of the csv file. The length of the list should be the same as the number of columns in the dataset. Returns ------- None. """ import csv if filename[-4:] != ".csv": filename = filename + ".csv" path = filename + ".csv" with open(path, 'w', newline="") as f: csv_write = csv.writer(f) csv_write.writerow(csv_head) for row in dataset: csv_write.writerow(row) return 1
################################################################# ### Voxel mesh to Abaqus input file ### ################################################################# import pyvista as pv import numpy as np inpDatabase = { "Title": [ "*Heading", "File generated by Texgen", "************", "*** MESH ***", "************" ], "Orientation": [ "********************", "*** ORIENTATIONS ***", "********************", "** Orientation vectors", "** 1st vector represents the fibre direction", "** 2nd vector is an arbitrary vector perpendicular to the first", "*Distribution Table, Name=TexGenOrientationVectors", "COORD3D,COORD3D", "*Distribution, Location=Element, Table=TexGenOrientationVectors, Name=TexGenOrientationVectors, Input=orientation.ori", "*Orientation, Name=TexGenOrientations, Definition=coordinates", "TexGenOrientationVectors", "1, 0" ], "ElementSets": [ "********************", "*** ELEMENT SETS ***", "********************", "** TexGen generates a number of element sets:", "** All - Contains all elements", "** Matrix - Contains all elements belonging to the matrix", "** YarnX - Where X represents the yarn index" ], "Materials": [ "*****************", "*** MATERIALS ***", "*****************" ], "Surfaces": [ "***************************", "*** SURFACE DEFINITIONS ***", "***************************" ], "Amplitudes": [ "**", "** AMPLITUDE CURVES", "**", "N/A"], "Steps": [ "************", " *** STEP ***", "************", ], "ori_header": [ '********************\n', '*** ORIENTATIONS ***\n', '********************\n', '** Orientation vectors\n', '** 1st vector represents the fibre direction\n', '** 2nd vector is an arbitrary vector perpendicular to the first\n', ', 1.0, 0.0, 0.0, 0.0, 1.0, 0.0\n'] }
[docs]def create_yarn_element_sets(mesh, file_handle, Indices, verbose=False): """ Creates element sets for each unique fiber in the mesh. Parameters ---------- mesh : pyvista.PolyData The input mesh. file_handle : file The file handle to write element set lines. Indices: int The indices of matrix and fiber. Returns ------- element_sets : dict Dictionary of element sets with the set name as the key and the lines as the value. """ element_sets = {} for yarn in np.unique(Indices): fiber_cells = np.where(Indices == yarn)[0] fiber_mesh = mesh.extract_cells(fiber_cells) cell_ids = fiber_mesh.cell_data['vtkOriginalCellIds'] + 1 if verbose: print(f"Yarn index: {yarn}, Cell IDs: {cell_ids}") if yarn == -1: continue # skip matrix elements # element_set_name = "Matrix" # 选择是否输出基体 element_set_name = f"Yarn{int(yarn)}" set_type = 'Elset' element_set = [f"*{set_type}, {set_type.lower()}={element_set_name}"] element_set.extend([', '.join(map(str, cell_ids[i:i + 8])) for i in range(0, len(cell_ids), 8)]) element_sets[element_set_name] = element_set file_handle.write('\n'.join(element_set) + '\n') if verbose: print(f"Created element set: {element_set_name}") return element_sets # 返回后再写入文件
[docs]def create_part_data_lines(prtname, nodes=[], elements=[], nodesets=[], elemsets=[]): """ Creates lines for defining nodes, elements, and sets in the part data. Parameters ---------- prtname : str Name of the part. nodes : list Node data. Each node is a list containing the node label and the x, y, and z coordinates. The number of nodes can be obtained using the len() function. elements : list Element data. Each element is a list containing the element label and the node labels. nodesets : list Node sets containing the node labels. elemsets : list Element sets containing the element labels. Returns ------- lines : list List of lines for the part data. """ lines = [ # f"*Part, name={prtname}", ## TODO : the first parameter is not used currently. "*Node", *[f"{int(nlabel) + 1}, {float(x):14.6f}, {float(y):14.6f}, {float(z):13.6f}" for nlabel, x, y, z in nodes], "*Element, type=C3D8R", *[f"{elabel + 1}, {', '.join(map(str, [n + 1 for n in nodelabels]))}" for [elabel, *nodelabels] in elements], ] n_pts = len(nodes) n_cells = len(elements) # Orientation for line in inpDatabase["Orientation"]: lines.append(line) # append header lines for element sets for line in inpDatabase["ElementSets"]: lines.append(line) for nodeset in nodesets: setname, nodelabels = nodeset if "Fiber" in setname: # node sets of fiber tows lines.append(f"*Nset, Nset={setname}, Generate") lines.append(f"1, {n_pts}, 1") for elemset in elemsets: setname, elemlabels = elemset if "Fiber" in setname: # element sets of fiber tows lines.append(f"*Elset, Elset={setname}, Generate") lines.append(f"1, {n_cells}, 1") return lines
[docs]def create_material_data_lines(matname, rho, e, nu, sta, condition=True, materials=None): """ Creates lines for defining material data in the input file. Parameters ---------- matname : str Name of the material. rho : float Density of the material. e : float Young's modulus of the material. nu : float Poisson's ratio of the material. sta : int Material state variable. condition : bool Condition for material type (default is True). materials: list List to store material data lines (default is None). Returns ------- datalines : list A list of lines for material data. """ datalines = [] # Define material properties lines line1 = f"*Material, name={matname}" line2 = "*Density" line3 = f"{rho}," # Check material condition and add appropriate lines if condition: line4 = "*Elastic" line5 = f"{e}, {nu}" datalines.extend([line1, line2, line3, line4, line5]) else: line4 = "*Depvar" line5 = f"{sta}," line6 = "*User Material, constants=4" line7 = "320.,10.,600.,0.45" datalines.extend([line1, line2, line3, line4, line5, line6, line7]) # Append material data lines to the provided list if it exists if materials is not None: materials.extend(datalines) return datalines
[docs]def create_solid_section_lines(elset_name, material_name, orientation_name=None, controls=""): """ Creates lines for defining the solid section properties of a specified element set and material. Parameters ---------- elset_name : str The name of the element set. material_name : str The name of the material. orientation_name : str The name of the orientation (default is None for the matrix). controls : str Control parameters for the section (default is an empty string). Returns ------- lines : list A list containing lines for defining the solid section properties. """ if controls: if orientation_name: lines = [ f"*Solid Section, ElSet={elset_name}, Material={material_name}, Orientation={orientation_name}, controls={controls}", ] else: lines = [ f"*Solid Section, ElSet={elset_name}, Material={material_name}, controls={controls}", ] else: if orientation_name: lines = [ f"*Solid Section, ElSet={elset_name}, Material={material_name}, Orientation={orientation_name}", ] else: lines = [ f"*Solid Section, ElSet={elset_name}, Material={material_name}", ] return lines
[docs]def create_solid_section_for_all_sets(Indices, fiber_material_name, orientation_name=None, controls=""): """ Creates solid section lines for all fiber element sets in the mesh. Parameters ---------- Indices: int The indices of matrix and fiber. fiber_material_name : str The name of the fiber material. orientation_name : str The name of the orientation (default is None for the matrix). controls : str Control parameters for the section (default is an empty string). Returns ------- lines : list A list of lines for defining solid section properties for all fiber element sets. """ lines = [] unique_yarn_indices = np.unique(Indices) for yarn_index in unique_yarn_indices: if yarn_index == -1: continue elset_name_fiber = f"Yarn{int(yarn_index)}" solid_section_lines_fiber = create_solid_section_lines(elset_name_fiber, fiber_material_name, orientation_name, controls) lines.extend(solid_section_lines_fiber) return lines
[docs]def write_fiber_orientation_to_file(mesh, Indices, file_header="", output_file='fabrictest.ori'): """ Writes fiber orientation information to a file. Parameters ---------- mesh: pyvista.PolyData The input mesh. Indices: int The indices of matrix and fiber. file_header: str The header lines for the ori file (default is ''). output_file: str The output file path for writing fiber orientation information (default is 'fabrictest.ori'). Returns ------- None """ # ori file header ori_header = inpDatabase["ori_header"] # only yarn indices yarn_indices = np.unique(Indices[Indices != -1]) with open(output_file, 'w') as ori_file: ori_file.writelines(ori_header) # Loop through each yarn index for yarn_index in yarn_indices: # Get the cells corresponding to the yarn index yarn_cells = np.where(Indices == yarn_index)[0] # Extract the mesh subset for the yarn yarn_mesh = mesh.extract_cells(yarn_cells) # Get the cell IDs cell_ids = yarn_mesh.cell_data['vtkOriginalCellIds'] # Read the tangent orientation vectors of the cells tangent_orientation = yarn_mesh.cell_data['orientation'] # Normalize the tangent vectors normalized_tangent = tangent_orientation / np.linalg.norm(tangent_orientation, axis=1)[:, np.newaxis] # Define an arbitrary vector perpendicular to the tangent vector arbitrary_vector = np.array([1, 0, 0]) # Calculate normal vectors through cross product normal_vectors = np.cross(normalized_tangent, arbitrary_vector) # Normalize the normal vectors normal_vectors /= np.linalg.norm(normal_vectors, axis=1)[:, np.newaxis] # Merge tangent and normal vectors, and format the data with commas combined_matrix = np.hstack((normalized_tangent, normal_vectors)) combined_matrix_str = [', '.join(map(str, row)) for row in combined_matrix] # Write to the ori file ori_file.write(f'** Yarn {int(yarn_index)} **\n') for i, cell_id in enumerate(cell_ids): ori_file.write(f'{int(cell_id) + 1:<8}, {combined_matrix_str[i]}\n')
[docs]def voxel2inp(mesh, scale=1, outputDir="./mesh-C3D8R.inp", orientation=True) -> None: """ Convert a voxel mesh to an Abaqus input file. Parameters ---------- mesh : pyvista.UnstructuredGrid The voxel mesh. scale : float, optional The scale factor to convert the unit of points. The default is 1.0. outputDir : str, optional The output directory and filename. The default is './mesh-C3D8R.inp'. The file extension is automatically added if not provided. Returns ------- None. Notes ----- voxel2inp is developed by Chao Yang (yangchaogg@whut.edu.cn) & Bin Yang (bin.yang@polymtl.ca) jointly. Please contact us if you have any questions. """ # check if the mesh is a pyvista.UnstructuredGrid object if not isinstance(mesh, pv.UnstructuredGrid): raise TypeError("The input mesh is not a pyvista.UnstructuredGrid object.") print(bcolors.header("Converting the voxel mesh to an Abaqus input file...")) # coordinates of nodes n_pts = mesh.n_points coordinates = mesh.points * scale connectivity = np.array(mesh.cells, copy=True).reshape(-1, 9) connectivity = connectivity[:, 1:] # Rearange the connectivity to match the order of the nodes required by Abaqus connectivity[:, [2, 5]] = connectivity[:, [5, 2]] # Swap columns 2 and 5 connectivity[:, [3, 4]] = connectivity[:, [4, 3]] # Swap columns 3 and 4 # Rearange the nodes to match the storage style of Abaqus: [node index, x, y, z.] fiber_nodes = [[i, *coord] for i, coord in enumerate(coordinates)] fiber_elements = [[i, *nodelabels] for i, nodelabels in enumerate(connectivity) if mesh.cell_data['yarnIndex'][i] != -1] # node and element sets for all nodes and elements fiber_node_sets = [("SET-Fiber-Node-ALL", range(1, n_pts + 1))] fiber_elem_sets = [("SET-Fiber-Element-ALL", range(1, len(fiber_elements) + 1))] # Read the yarn Index try: yarn_Index = mesh.cell_data['yarnIndex'] except: yarn_Index = mesh.cell_data['YarnIndex'] # Part section of inp file part_name = "Part-Textile" fiber_part_data_lines = create_part_data_lines(part_name, nodes=fiber_nodes, elements=fiber_elements, nodesets=fiber_node_sets, elemsets=fiber_elem_sets) # write inp file if not outputDir.endswith('.inp'): outputDir += '.inp' inp_file_path = outputDir with open(inp_file_path, 'w') as f: # 01 file header for line in inpDatabase["Title"]: f.write(line + "\n") # 02 nodes and elements for line in fiber_part_data_lines: if isinstance(line, list): f.write(', '.join(map(str, line)) + "\n") else: f.write(line + "\n") # 03 element sets for fiber tows create_yarn_element_sets(mesh, file_handle=f, Indices=yarn_Index) # 04 material properties for line in inpDatabase["Materials"]: f.write(line + '\n') mat_lines_fiber = create_material_data_lines("fiber", 1.0, 1.0e6, 0.3, 7, condition=False) for mat_line in mat_lines_fiber: f.write(mat_line + "\n") # 05 solid sections for fiber tows fiber_material_name = "fiber" orientation_name = "TexGenOrientations" controls = "HourglassEnhanced" # 如果使用C3D8R 将此行改为controls = "HourglassEnhanced" solid_section_lines_all_sets = create_solid_section_for_all_sets(yarn_Index, fiber_material_name, orientation_name, controls) for line in solid_section_lines_all_sets: f.write(line + "\n") # 06 surfaces for line in inpDatabase["Surfaces"]: f.write(line + '\n') print(f"inp file is written to {inp_file_path}") # 07 fiber orientation if orientation: # split outputDir to get the path outputDir = outputDir.split('/') outputDir = '/'.join(outputDir[:-1]) output_file = outputDir + '/orientation.ori' write_fiber_orientation_to_file(mesh, Indices=yarn_Index, file_header=inpDatabase["ori_header"], output_file=output_file) print(f'Cell orientation is written to "{output_file}" file.') print(bcolors.ok("The voxel mesh has been successfully converted to an Abaqus input file.")) return None
################################################################# ### The following functions will be deprecated in the future. ### #################################################################
[docs]def save_ply(file, vertices, cells=[], point_data={}, cell_data={}, binary=False): print(bcolors.warning( "This function will be deprecated in the future. Please use polytex.meshio_save() instead.")) return meshio_save(file, vertices, cells, point_data, cell_data, binary)
[docs]def pcd_to_ply(file_pcd, file_ply, binary=False): """ Convert a pcd file to ply file. Parameters ---------- file_pcd : str The path of the pcd file or pathlib.Path. File or filename to which the data is saved. file_ply : str The path of the ply file or pathlib.Path. File or filename to which the data is to be saved. binary : bool, optional TODO Returns ------- None """ print(bcolors.warning( "This function will be deprecated in the future. Please use polytex.read_imagej_roi() instead.")) import meshio vertices = pk_load(file_pcd).to_numpy() mesh = meshio.Mesh(points=vertices[:, 1:], cells=[], # Optionally provide extra data on points, cells, etc. point_data={"nx": vertices[:, 0], "ny": vertices[:, 1], "nz": vertices[:, 2]}, # Each item in cell data must match the cells array # cell_data={"a": [[0.1, 0.2], [0.4]]}, ) meshio.write(file_ply, mesh, binary=False)
if "__main__" == __name__: import doctest doctest.testmod()