├── requirements.txt
├── resource
├── images
│ ├── banner_1_full.png
│ ├── banner_1_thumb.png
│ ├── example_simple_1.png
│ ├── example_simple_2.png
│ ├── example_simple_3.png
│ └── example_advanced_1.png
└── rocket_stubby.csm
├── src
├── write_mapbc.py
├── extrude_config.py
├── parse_dat.py
├── gmsh_helpers.py
├── gen_blmesh.py
├── gen_farfield.py
└── ugrid_tools.py
├── LICENSE
├── example_simple.py
├── example_advanced.py
├── .gitignore
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | gmsh==4.13.1
2 | matplotlib==3.8.4
3 | numpy==2.1.0
4 | pandas==2.2.2
5 | scipy==1.14.1
6 |
--------------------------------------------------------------------------------
/resource/images/banner_1_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/banner_1_full.png
--------------------------------------------------------------------------------
/resource/images/banner_1_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/banner_1_thumb.png
--------------------------------------------------------------------------------
/resource/images/example_simple_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/example_simple_1.png
--------------------------------------------------------------------------------
/resource/images/example_simple_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/example_simple_2.png
--------------------------------------------------------------------------------
/resource/images/example_simple_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/example_simple_3.png
--------------------------------------------------------------------------------
/resource/images/example_advanced_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliottmckee/cfd-meshman/HEAD/resource/images/example_advanced_1.png
--------------------------------------------------------------------------------
/src/write_mapbc.py:
--------------------------------------------------------------------------------
1 |
2 | def write_mapbc(filename, tags, bctypes, names):
3 |
4 | lines = [f'{tag}\t{bc}\t{name}\n' for tag, bc, name in zip(tags, bctypes, names)]
5 |
6 | with open(filename, 'w') as f:
7 | f.write(f'{len(tags)}\n')
8 | f.writelines(lines)
9 |
10 | if __name__ == '__main__':
11 | write_mapbc('t16.mapbc', tags=[1,3], bctypes=[3000, 5050],names=['wall', 'farfield'])
12 |
--------------------------------------------------------------------------------
/src/extrude_config.py:
--------------------------------------------------------------------------------
1 | EXTRUDE_CONFIG = '''input_file: {surfmesh_stem}.vtp
2 | layers_file: layers.csv
3 | output_file: {blmesh_ugrid}
4 | direction: nominal
5 | smooth_normals: true
6 | smooth_normals_iterations: 25
7 | smooth_null_space: true
8 | null_space_iterations: 20
9 | null_space_limit: 2.0
10 | null_space_relaxation: 0.5
11 | eigenvector_threshold: 0.5
12 | smooth_curvature: true
13 | curvature_factor: 4.5
14 | symmetry: none
15 | symmetry_threshold: 2.0
16 | unmodified_layers: 3
17 | debug_mode: false
18 | debug_start_layer: 1'''
19 |
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 elliottmckee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/parse_dat.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 |
4 | import matplotlib.pyplot as plt
5 |
6 |
7 | def parse_dat(filename):
8 |
9 | with open(filename, 'r') as fid:
10 | fid.readline()
11 |
12 | var_line = fid.readline()
13 | var_line = var_line.replace('VARIABLES=', '')
14 | var_line = var_line.replace('"\n', '')
15 | varnames = [ var.strip('"')for var in var_line.split(' ')]
16 |
17 | fid.readline()
18 |
19 | return pd.read_csv(fid, sep=' ', names=varnames, engine='python')
20 |
21 |
22 |
23 | def plot_residuals(df):
24 |
25 | for col in df.columns:
26 | if 'R_' in col:
27 |
28 | plt.figure()
29 | plt.plot(df['Iteration'], df[col])
30 |
31 | plt.yscale('log')
32 | plt.xlabel('Iteration')
33 | plt.ylabel('Value')
34 | plt.title(col)
35 |
36 | plt.show()
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | if __name__ == '__main__':
51 |
52 | data = parse_dat('Fin_Can_Stubby_trisurf_v1p2_FINAL_hist.dat')
53 |
54 | plot_residuals(data)
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/example_simple.py:
--------------------------------------------------------------------------------
1 | '''
2 | Example driver showing simplified end-to-end workflow
3 |
4 | NOTE
5 | - Mesh build is performed wherever the .msh file is by default (so for this script, in resource/)
6 | - There are a lot of intermediate files that get written in this workflow, that arent cleaned up.
7 | '''
8 | import os
9 | from src.ugrid_tools import UMesh
10 | from src.gen_blmesh import gen_blmesh
11 | from src.gen_farfield import gen_farfield
12 |
13 | # input .msh surface mesh
14 | # can make in gmsh gui, or alternatively, use gmsh python api (see advanced example)
15 | # must export from GMSH as version 2 ascii!
16 | SurfMesh = UMesh('resource/rocket_stubby_surf.msh')
17 |
18 | # convert surface mesh from .msh to ugrid
19 | SurfMesh.write('resource/rocket_stubby_surf.ugrid')
20 |
21 | # mesh_tools: extrude BoundaryLayer mesh from surface mesh, convert back to .msh
22 | BoundLayerMesh = gen_blmesh('resource/rocket_stubby_surf.ugrid', num_bl_layers=10, near_wall_spacing=4.2e-5, bl_growth_rate=1.5)
23 | BoundLayerMesh.write('resource/rocket_stubby_BLMESH.msh')
24 |
25 | # gmsh: generate BoundaryLayer+Farfield mesh, by building around/outward-from the boundary layer mesh
26 | VolumeMesh = gen_farfield('resource/rocket_stubby_BLMESH.msh', farfield_radius=15, farfield_Lc=25, extend_power=.2)
27 |
28 | # finally, convert volume mesh from .msh to .ugrid
29 | VolumeMesh.write('resource/rocket_stubby_VOLMESH_FINAL.ugrid')
30 |
31 |
--------------------------------------------------------------------------------
/src/gmsh_helpers.py:
--------------------------------------------------------------------------------
1 | import gmsh
2 |
3 |
4 | def sphere_surf(x, y, z, r, lc, surf_tag, physical_group):
5 | # This function will create a spherical shell (surf_tag)
6 |
7 | p1 = gmsh.model.geo.addPoint(x, y, z, lc)
8 | p2 = gmsh.model.geo.addPoint(x + r, y, z, lc)
9 | p3 = gmsh.model.geo.addPoint(x, y + r, z, lc)
10 | p4 = gmsh.model.geo.addPoint(x, y, z + r, lc)
11 | p5 = gmsh.model.geo.addPoint(x - r, y, z, lc)
12 | p6 = gmsh.model.geo.addPoint(x, y - r, z, lc)
13 | p7 = gmsh.model.geo.addPoint(x, y, z - r, lc)
14 |
15 | c1 = gmsh.model.geo.addCircleArc(p2, p1, p7)
16 | c2 = gmsh.model.geo.addCircleArc(p7, p1, p5)
17 | c3 = gmsh.model.geo.addCircleArc(p5, p1, p4)
18 | c4 = gmsh.model.geo.addCircleArc(p4, p1, p2)
19 | c5 = gmsh.model.geo.addCircleArc(p2, p1, p3)
20 | c6 = gmsh.model.geo.addCircleArc(p3, p1, p5)
21 | c7 = gmsh.model.geo.addCircleArc(p5, p1, p6)
22 | c8 = gmsh.model.geo.addCircleArc(p6, p1, p2)
23 | c9 = gmsh.model.geo.addCircleArc(p7, p1, p3)
24 | c10 = gmsh.model.geo.addCircleArc(p3, p1, p4)
25 | c11 = gmsh.model.geo.addCircleArc(p4, p1, p6)
26 | c12 = gmsh.model.geo.addCircleArc(p6, p1, p7)
27 |
28 | l1 = gmsh.model.geo.addCurveLoop([c5, c10, c4])
29 | l2 = gmsh.model.geo.addCurveLoop([c9, -c5, c1])
30 | l3 = gmsh.model.geo.addCurveLoop([c12, -c8, -c1])
31 | l4 = gmsh.model.geo.addCurveLoop([c8, -c4, c11])
32 | l5 = gmsh.model.geo.addCurveLoop([-c10, c6, c3])
33 | l6 = gmsh.model.geo.addCurveLoop([-c11, -c3, c7])
34 | l7 = gmsh.model.geo.addCurveLoop([-c2, -c7, -c12])
35 | l8 = gmsh.model.geo.addCurveLoop([-c6, -c9, c2])
36 |
37 | s1 = gmsh.model.geo.addSurfaceFilling([l1])
38 | s2 = gmsh.model.geo.addSurfaceFilling([l2])
39 | s3 = gmsh.model.geo.addSurfaceFilling([l3])
40 | s4 = gmsh.model.geo.addSurfaceFilling([l4])
41 | s5 = gmsh.model.geo.addSurfaceFilling([l5])
42 | s6 = gmsh.model.geo.addSurfaceFilling([l6])
43 | s7 = gmsh.model.geo.addSurfaceFilling([l7])
44 | s8 = gmsh.model.geo.addSurfaceFilling([l8])
45 |
46 | sl = gmsh.model.geo.addSurfaceLoop([s1, s2, s3, s4, s5, s6, s7, s8], surf_tag)
47 |
48 | gmsh.model.geo.synchronize()
49 | gmsh.model.addPhysicalGroup(2, [s1, s2, s3, s4, s5, s6, s7, s8], physical_group)
50 | return sl
51 |
52 |
53 | def collect_size_fields(size_fields_dict):
54 | # GMSH didn't like me trying to pass size fields directly through/into functions that build meshes
55 | # so just using a dictionary outside the function + this helper to mirror this functionality
56 | # in a pythonic fashion
57 |
58 | gmsh_size_fields = [];
59 |
60 | # for each size field
61 | for size_field, params in size_fields_dict.items():
62 | sf_curr = gmsh.model.mesh.field.add(size_field)
63 | gmsh_size_fields.append(sf_curr)
64 |
65 | # for each parameter in size field
66 | for param, val in params.items():
67 | gmsh.model.mesh.field.setNumber(sf_curr, param, val)
68 |
69 | return gmsh_size_fields
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/gen_blmesh.py:
--------------------------------------------------------------------------------
1 | import os
2 | import csv
3 | import subprocess
4 | import warnings
5 | from pathlib import Path
6 |
7 | from .ugrid_tools import UMesh
8 | from .extrude_config import EXTRUDE_CONFIG
9 |
10 |
11 | def gen_blmesh(surfmesh_ugrid_path, num_bl_layers=10, near_wall_spacing=1e-4, bl_growth_rate=1.3, write_vtk=False):
12 | '''
13 | TODO:
14 | - MAKE THIS USE A TEMP DIR? Add cleanup command?
15 | - more explicit warnings if subprocess calls fail
16 | - Allow parameter overwrites, or pointing to new default extrude inputs file
17 | - Remove clunky interaction with mesh_tools mesh_convert.py, and just add VTP writer to Umesh
18 | - Output stream of extrude command to pipe for realtime outputs
19 |
20 | NOTES:
21 | - All work is performed in directory where surface mesh file lives
22 |
23 | INPUTS:
24 | surfmesh_ugrid_path: str, path to .ugrid surface mesh file to exrude
25 |
26 | OUTPUTS:
27 |
28 | blmesh_msh: str, path to output BL mesh file in .msh format
29 | '''
30 |
31 | # input checking
32 | if not surfmesh_ugrid_path.endswith('.ugrid'):
33 | raise TypeError('input must be a .ugrid')
34 |
35 | # paths
36 | base_dir = os.getcwd()
37 | work_dir = os.path.dirname(surfmesh_ugrid_path) # forcing to build mesh location for now
38 |
39 | surfmesh_stem = Path(surfmesh_ugrid_path).stem
40 | blmesh_ugrid = f'{surfmesh_stem}_BLMESH.ugrid'
41 | blmesh_vtk = f'{surfmesh_stem}_BLMESH.vtk'
42 | blmesh_ugrid_path = os.path.join(work_dir, blmesh_ugrid)
43 | blmesh_vtk_path = os.path.join(work_dir, blmesh_vtk)
44 |
45 | # generate layer spacing
46 | layers = [near_wall_spacing]
47 | for i in range(0, num_bl_layers):
48 | layers.append(layers[i]*bl_growth_rate)
49 | print(f'Layers: {layers}')
50 |
51 | # cd into "workdir"
52 | os.chdir(work_dir)
53 |
54 | # Call mesh_tools mesh_convert.py to convert .ugrid to .vtp
55 | print('Calling mesh_tools mesh_convert to convert .ugrid to .vtp...\n')
56 | cmd = f'conda run --verbose -n mesh_tools_p3p7 mesh_convert.py -i {surfmesh_stem}.ugrid'
57 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
58 | print(result.stdout); print(result.stderr)
59 |
60 | # Write extrude.inputs
61 | with open('extrude.inputs', 'w') as fid:
62 | for line in iter(EXTRUDE_CONFIG.splitlines()):
63 | fid.write(eval(f"f'{line}'")+"\n")
64 |
65 | # Write layers.csv
66 | with open('layers.csv', 'w') as fid:
67 | write = csv.writer(fid)
68 | write.writerow(layers)
69 |
70 | # Call mesh_tools extrude to get BL mesh
71 | print('Calling mesh_tools extrude...\n')
72 | cmd = 'extrude'
73 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
74 | print(result.stdout); print(result.stderr)
75 |
76 | # hacky vtk conversion, since its better at visualization than GMSH
77 | if write_vtk:
78 | print(f'Converting BL mesh to VTK using meshio (workaround)...\n')
79 | cmd = f'meshio convert {blmesh_ugrid} {blmesh_vtk}'
80 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
81 | print(result.stdout); print(result.stderr)
82 |
83 | # Read in resultant mesh
84 | BLMesh = UMesh(blmesh_ugrid)
85 |
86 | # Change back to base dir
87 | os.chdir(base_dir)
88 |
89 | return BLMesh
90 |
91 |
--------------------------------------------------------------------------------
/example_advanced.py:
--------------------------------------------------------------------------------
1 | '''
2 | Example driver showing advanced end-to-end workflow highlighting
3 | - using gmsh api for "semi-automated" surface meshing
4 | - gmsh size fields common between surface and volume
5 |
6 | NOTE
7 | - Mesh build is performed wherever the .msh file is by default (so for this script, in resource/)
8 | - There are a lot of intermediate files that get written in this workflow, that arent cleaned up.
9 | '''
10 |
11 | import os
12 | import gmsh
13 | from src.ugrid_tools import UMesh
14 | from src.gen_blmesh import gen_blmesh
15 | from src.gen_farfield import gen_farfield
16 | from src.gmsh_helpers import collect_size_fields
17 |
18 |
19 | ############################
20 | # SURFACE MESH WITH GMSH API
21 | ###
22 | gmsh.initialize()
23 | gmsh.option.setNumber("General.NumThreads", 4)
24 |
25 | # merge in BL mesh file
26 | gmsh.merge('resource/rocket_stubby.step')
27 | gmsh.model.geo.synchronize()
28 |
29 | # lets selectively refine *a few* of the point sizings around the fin edge radii
30 | points_refine = [68,69,102,103,106,107,72,73,88,89,114,115,84,85,111,110,56,57,94,95,90,91,52,53,33,34,75,76,79,80,37,38,25,26,64,41,42,64,6,7,51,18,19,27,25,26,64]
31 | points_dimTags = [(0, point) for point in points_refine]
32 | gmsh.model.mesh.setSize(points_dimTags, 0.008)
33 |
34 | # Lets pass some additional size-fields in
35 | # This will get applied congrously to the surface and volume meshes!
36 | # This dictionary mirrors how you specify size fields in the GMSH api
37 | size_fields_dict = {'Cylinder':{'VIn': 0.02,
38 | 'VOut': 1e22,
39 | 'XAxis': 0.45,
40 | 'YAxis': 0.0,
41 | 'ZAxis': 0.0,
42 | 'XCenter': -0.1,
43 | 'YCenter': 0.0,
44 | 'ZCenter': 0.0,
45 | 'Radius': 0.3},
46 | 'Ball':{'Radius': 0.05,
47 | 'Thickness':0.05,
48 | 'VIn': 0.015,
49 | 'VOut': 1e22,
50 | 'XCenter': 0.718}}
51 |
52 | # have to massage a bit into gmsh format
53 | size_field_tags = collect_size_fields(size_fields_dict)
54 |
55 | # take min of all size fields
56 | min_all_sizefields = gmsh.model.mesh.field.add("Min")
57 | gmsh.model.mesh.field.setNumbers(min_all_sizefields, "FieldsList", size_field_tags)
58 | gmsh.model.mesh.field.setAsBackgroundMesh(min_all_sizefields)
59 |
60 | # gmsh options
61 | gmsh.option.setNumber("Mesh.MeshSizeFromPoints", 1)
62 | gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 1)
63 | gmsh.option.setNumber("Mesh.MeshSizeMax", 0.03)
64 |
65 | # generate mesh
66 | gmsh.model.mesh.generate(2)
67 |
68 | # save out
69 | gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
70 | gmsh.write('resource/rocket_stubby_advanced.msh')
71 | gmsh.finalize()
72 | ############################
73 |
74 |
75 | # convert surface mesh from .msh to ugrid
76 | SurfMesh = UMesh('resource/rocket_stubby_advanced.msh')
77 | SurfMesh.write('resource/rocket_stubby_advanced.ugrid')
78 |
79 | # mesh_tools: extrude BoundaryLayer mesh from surface mesh, convert back to .msh
80 | BoundLayerMesh = gen_blmesh('resource/rocket_stubby_advanced.ugrid', num_bl_layers=9, near_wall_spacing=4.2e-5, bl_growth_rate=1.5)
81 | BoundLayerMesh.write('resource/rocket_stubby_advanced_BLMESH.msh')
82 |
83 | # gmsh: generate BoundaryLayer+Farfield mesh, by building around/outward-from the boundary layer mesh
84 | VolumeMesh = gen_farfield('resource/rocket_stubby_advanced_BLMESH.msh', farfield_radius=15, farfield_Lc=25, extend_power=.2, size_fields_dict=size_fields_dict)
85 |
86 | # finally, convert volume mesh from .msh to .ugrid
87 | VolumeMesh.write('resource/rocket_stubby_advanced_VOLMESH_FINAL.ugrid')
88 |
89 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110 | .pdm.toml
111 | .pdm-python
112 | .pdm-build/
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
163 |
164 | # Project-specific
165 | /workdirs
166 | /src/mesh_io.py
167 |
--------------------------------------------------------------------------------
/src/gen_farfield.py:
--------------------------------------------------------------------------------
1 | import os
2 | import math
3 | import csv
4 | import subprocess
5 | import warnings
6 | import gmsh
7 | import numpy as np
8 |
9 | from pathlib import Path
10 |
11 | from .gmsh_helpers import sphere_surf, collect_size_fields
12 | from .ugrid_tools import UMesh
13 |
14 |
15 | def gen_farfield(bl_msh_path, farfield_radius=10, farfield_Lc=2, extend_power=0.5, numthreads=4, size_fields_dict={}):
16 | '''
17 | TODO:
18 | - I TRIED TO MAKE THIS WORK WITH OPENCASCADE BUT WAS HAVING ISSUES. AM PROBABLY JUST DUMB. TRY AGAIN LATER
19 | See e.g. 't1.py', `t16.py', `t18.py', `t19.py' or `t20.py' for complete examples based on OpenCASCADE, and `examples/api' for more.
20 | - Robustify identification of which tag represents the boundary layer interface/top-cap.
21 | - Clean up "Extend" field implementation
22 | - Allow size fields to be passed-in
23 | - Do in tempdir? Add file cleanup functionality?
24 |
25 | INPUTS:
26 | bl_msh_path: str, path to .msh formatted boundary layer mesh (from gen_blmesh.py)
27 |
28 | OUTPUTS:
29 | VolMesh: UMesh, volume mesh (boundary layer + farfield)
30 |
31 | NOTES:
32 | - If things break below- it is likely due to assumptions about surface tagging, mainly for the BLMESH outer-most interface/top-cap surface. Double check these assumptions if having issues
33 |
34 | GMSH tidbits
35 | # t10.py, Mesh size fields
36 | # Finally, while the default "Frontal-Delaunay" 2D meshing algorithm
37 | # (Mesh.Algorithm = 6) usually leads to the highest quality meshes, the
38 | # "Delaunay" algorithm (Mesh.Algorithm = 5) will handle complex mesh size fields
39 | # better - in particular size fields with large element size gradients:
40 |
41 | # t12.py
42 | # // "Compound" meshing constraints allow to generate meshes across surface
43 | # // boundaries, which can be useful e.g. for imported CAD models (e.g. STEP) with
44 | # // undesired small features.
45 |
46 | # t16.py
47 | # If we had wanted five empty holes we would have used `cut()' again. Here we
48 | # want five spherical inclusions, whose mesh should be conformal with the mesh
49 | # of the cube: we thus use `fragment()', which intersects all volumes in a
50 | # conformal manner (without creating duplicate interfaces):
51 | # ov, ovv = gmsh.model.occ.fragment([(3, 3)], holes)
52 |
53 | # x1.py
54 | # Create a geometry for the discrete curves and surfaces, so that we can remesh
55 | # them later on:
56 | # gmsh.model.mesh.createGeometry()
57 | # Note that for more complicated meshes, e.g. for on input unstructured STL
58 | # mesh, we could use `classifySurfaces()' to automatically create the discrete
59 | # entities and the topology; but we would then have to extract the boundaries
60 | # afterwards.
61 |
62 | '''
63 |
64 | # paths
65 | volmesh_msh = os.path.splitext(bl_msh_path)[0]+'_VOLMESH.msh'
66 |
67 | # init gmsh
68 | gmsh.initialize()
69 | gmsh.option.setNumber('Geometry.Tolerance', 1e-16)
70 | gmsh.option.setNumber("General.NumThreads", numthreads)
71 | gmsh.model.add("model_1")
72 |
73 | # merge in BL mesh file
74 | gmsh.merge(bl_msh_path)
75 |
76 | # make surface loop based on the tagged surfaces of the blmesh
77 | # (expected: 0 is the geometric/wall surface, 1 is the "top cap" of the Bl mesh, but I don't think that is 100% guaranteed)
78 | # syntax: surfaceTags (vector of integers), tag (integer)
79 | eltag_surf_wall = gmsh.model.geo.addSurfaceLoop([0], 1)
80 | eltag_surf_bl_topcap = gmsh.model.geo.addSurfaceLoop([1], 2)
81 |
82 | # Create Farfield extents
83 | eltag_surf_sphere = sphere_surf(x=0, y=0, z=0, r=farfield_radius, lc=farfield_Lc, surf_tag=50, physical_group=3)
84 |
85 | # Create Farfield volume (between SPHERE farfield extent and BLMESH outer-interface)
86 | eltag_vol_farfield = gmsh.model.geo.addVolume([eltag_surf_sphere, eltag_surf_bl_topcap], 60)
87 |
88 | # have to synchronize before physical groups
89 | gmsh.model.geo.remove_all_duplicates() #not sure if necessary
90 | gmsh.model.geo.synchronize()
91 |
92 | # assign physical groups
93 | # reminder: all meshes in gmsh must have physical group or will not be written (unless you use the write_all_elements flag in gmsh)
94 | # syntax: dim (integer), tags (vector of integers for model entities), tag (integer for physical surface tag), name (string)
95 | phystag_surf_wall = gmsh.model.addPhysicalGroup(2, [0], 1)
96 | phystag_vol_bl = gmsh.model.addPhysicalGroup(3, [0], 1) # FOR THIS BL MESH VOLUME- I ARBITRARILY SET THIS TO 0 IN UGRID READER
97 | phystag_vol_farfield = gmsh.model.addPhysicalGroup(3, [60], 61) # FARFIELD MESH VOLUME
98 |
99 | # Extend size field, see extend_field.py example
100 | # Can't figure out how to get this field to act on sphere farfield. But we can just be kinda smart about
101 | # how we set DistMax and SizeMax, relative to the sphere size to get basically the same result
102 | f_extend = gmsh.model.mesh.field.add("Extend")
103 | gmsh.model.mesh.field.setNumbers(f_extend, "SurfacesList", [1])
104 | # # gmsh.model.mesh.field.setNumbers(f, "CurvesList", [e[1] for e in gmsh.model.getEntities(1)])
105 | gmsh.model.mesh.field.setNumber(f_extend, "DistMax", farfield_radius)
106 | gmsh.model.mesh.field.setNumber(f_extend, "SizeMax", farfield_Lc)
107 | gmsh.model.mesh.field.setNumber(f_extend, "Power", extend_power)
108 |
109 | # Collect additional size fields
110 | f_additional_size_fields = collect_size_fields(size_fields_dict)
111 |
112 | # take min across all size fields
113 | f_min_all = gmsh.model.mesh.field.add("Min")
114 | gmsh.model.mesh.field.setNumbers(f_min_all, "FieldsList", [f_extend]+f_additional_size_fields)
115 |
116 | # gmsh.model.mesh.field.setAsBackgroundMesh(f_extend)
117 | gmsh.model.mesh.field.setAsBackgroundMesh(f_min_all)
118 |
119 | # Options
120 | gmsh.option.setNumber("Mesh.Algorithm3D", 10)
121 | gmsh.option.setNumber("Mesh.MeshSizeFromPoints", 1)
122 | gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 0)
123 | gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", -2) # Need to force extend from boundary to only occur on 2D surfaces (farfield)
124 |
125 | # Generate
126 | gmsh.model.mesh.generate(3)
127 |
128 | # Postprocess
129 | gmsh.model.mesh.remove_duplicate_nodes()
130 | gmsh.model.mesh.remove_duplicate_elements()
131 |
132 | # Remove interface
133 | gmsh.model.mesh.removeElements(eltag_surf_bl_topcap, 1)
134 |
135 | # Save out
136 | # gmsh.option.setNumber("Mesh.SaveAll", 1)
137 | gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
138 | gmsh.write(volmesh_msh)
139 | gmsh.finalize()
140 |
141 | # Read back into python as UMesh
142 | VolMesh = UMesh(volmesh_msh)
143 |
144 | return VolMesh
145 |
146 |
147 |
--------------------------------------------------------------------------------
/resource/rocket_stubby.csm:
--------------------------------------------------------------------------------
1 | # Engineering Sketchpad
2 | # parametric rocket geo generator
3 | # written by Elliott McKee
4 | # units m
5 | # TODO
6 | # - Make fins a udprim?
7 |
8 |
9 |
10 |
11 | ############################
12 | # PARAMETERS
13 | ######
14 | DESPMTR B:diam 6*0.0254
15 | DESPMTR B:len 15*0.0254
16 |
17 | DESPMTR F:Croot 12*0.0254
18 | DESPMTR F:Ctip 5*0.0254
19 | DESPMTR F:Span 6*0.0254
20 | DESPMTR F:th 0.5*0.0254
21 | DESPMTR F:Swp_LE_deg 60
22 | DESPMTR F:Cham_deg 10
23 | DESPMTR F:tip_R .003 # NEED TO COMMENT/UNCOMMENT BELOW WHEN GOING FROM SHARP/ROUND OR VV
24 |
25 | DESPMTR F:fillet .7*0.0254
26 | DESPMTR F:base_offset .75*0.0254
27 | DESPMTR F:num 4
28 |
29 | DESPMTR NOSE:rad 0.02
30 | DESPMTR NOSE:angd 10
31 |
32 | DESPMTR BOAT:len 2*0.0254
33 | DESPMTR BOAT:aft_diam 5*0.0254
34 |
35 |
36 |
37 |
38 | ############################
39 | # SINGLE FIN
40 | ######
41 | # Making this ruled instead, for simplicity
42 | # JK, all the grown surfaces cause issues when filleting. Going back to old way
43 |
44 |
45 | ### FIN PLANFORM
46 | SKBEG 0 0 0
47 | linseg F:Croot 0 0
48 | linseg F:Croot-F:Span*tand(F:Swp_LE_deg) F:Span 0
49 | linseg F:Croot-F:Span*tand(F:Swp_LE_deg)-F:Ctip F:Span 0
50 | linseg 0 0 0
51 | SKEND
52 | EXTRUDE 0 0 F:th
53 | TRANSLATE 0 0 -F:th/2
54 |
55 |
56 | ### LEADING EDGE - boolean for chamfers (UGH)
57 | set OFF (F:th/2/tand(F:Cham_deg))
58 | skbeg F:Croot 0 0
59 | linseg F:Croot-OFF 0 F:th/2
60 | linseg F:Croot-OFF+0.1 0 F:th/2
61 | linseg F:Croot-OFF+0.1 0 -F:th/2
62 | linseg F:Croot-OFF 0 -F:th/2
63 | linseg F:Croot 0 0
64 | skend
65 |
66 | extrude -sind(F:Swp_LE_deg) cosd(F:Swp_LE_deg) 0
67 | subtract
68 |
69 |
70 | ### TRAILING EDGE - boolean for chamfers (UGH)
71 | skbeg 0 0 0
72 | linseg OFF 0 F:th/2
73 | linseg OFF-0.1 0 F:th/2
74 | linseg OFF-0.1 0 -F:th/2
75 | linseg OFF 0 -F:th/2
76 | #linseg -0.1 0 0
77 | linseg 0 0 0
78 | skend
79 |
80 | extrude F:Croot-F:Span*tand(F:Swp_LE_deg)-F:Ctip F:Span 0
81 | subtract
82 |
83 |
84 | ### RADII
85 | IFTHEN F:tip_R GT 0
86 | SELECT EDGE 16 3 16 7 1
87 | SELECT ADD 25 3 25 7 1
88 | fillet F:tip_R @sellist 1
89 | ENDIF
90 |
91 | STORE _FIN
92 |
93 |
94 | ### FILLET IMPRINTER
95 | set dx F:fillet/2 # axial length buffer for fillet, kinda arbitrary
96 |
97 | # draw root cross section
98 | set fx1 -dx
99 | set fx2 F:th/2/tand(F:Cham_deg)
100 | set fx3 F:Croot-fx2
101 | set fx4 F:Croot+dx
102 |
103 | set fz F:th/2+F:fillet
104 |
105 | skbeg fx1 0 0
106 | linseg fx2 0 -fz
107 | linseg fx3 0 -fz
108 | linseg fx4 0 0
109 | linseg fx3 0 fz
110 | linseg fx2 0 fz
111 | linseg fx1 0 0
112 | skend
113 |
114 | EXTRUDE 0 2*F:fillet 0
115 |
116 | # grab min/max x edges to fillet
117 | SELECT EDGE
118 | SELECT SORT $xmin
119 | DIMENSION foo 18 1
120 | SET foo @sellist
121 | SELECT EDGE foo[1]
122 | SELECT ADD foo[18]
123 | ATTRIBUTE _color $red
124 |
125 |
126 | IFTHEN F:tip_R GT 0
127 | fillet 4.0*F:tip_R @sellist 1
128 | ENDIF
129 |
130 | EXTRACT 0
131 | STORE _IMPRINTER
132 |
133 |
134 |
135 |
136 | ############################
137 | # BODY
138 | ######
139 | CYLINDER 0 0 0 B:len 0 0 B:diam/2
140 | SELECT FACE
141 | ATTRIBUTE tagComp $body
142 | SELECT EDGE
143 | ATTRIBUTE tagComp $body
144 | STORE _BODY
145 |
146 |
147 |
148 |
149 | ############################
150 | # FIN ASSEMBLY
151 | ######
152 |
153 | # workaround, since fin base will be tangent to cylinder, if we dont do the math to correct for it
154 | set OFF_FIN sqrt((B:diam/2)^2-(F:th/2)^2)
155 | set F_deg_inc 360/F:num
156 |
157 | # tried offset udprim with faces and edges, the smart way- it generated, but didnt work as intended
158 | # now imprinting manually
159 | #UDPRIM offset dist F:fillet # I think this is ill defined for things with sharp edges? I dont think we'd want this for imprinter anyways
160 |
161 |
162 | ### IMPRINT FIN
163 | RESTORE _FIN
164 | BOX -1 F:fillet -1 2 0 2
165 | SUBTRACT
166 | TRANSLATE F:base_offset OFF_FIN 0
167 | STORE _FIN_IMPRINTED
168 |
169 |
170 | ### IMPRINT BODY
171 | RESTORE _BODY
172 | RESTORE _IMPRINTER
173 | TRANSLATE F:base_offset OFF_FIN-F:fillet 0
174 | SUBTRACT
175 | STORE _BODY_IMPRINTED
176 |
177 | # Pick faces on FIN
178 | RESTORE _FIN_IMPRINTED
179 | SELECT FACE
180 | SELECT SORT $ymin
181 | DIMENSION foo 9 1
182 | SET foo @sellist
183 | SELECT FACE foo[1]
184 | SELECT ADD foo[2]
185 | SELECT ADD foo[3]
186 | SELECT ADD foo[4]
187 | SELECT ADD foo[5]
188 | SELECT ADD foo[6]
189 | SELECT ADD foo[7]
190 | SELECT ADD foo[8] # COMMENT THIS AND BELOW IF NO LE RADIUS
191 | SELECT ADD foo[9] #
192 | ATTRIBUTE _color $cyan
193 | ATTRIBUTE _flend $remove
194 |
195 | # Pick faces on BODY
196 | RESTORE _BODY_IMPRINTED
197 | SELECT FACE
198 | SELECT SORT $ymax
199 | DIMENSION bar 3 1
200 | SET bar @sellist # idk why this indexing is borked
201 | SELECT FACE bar[3]
202 | #SELECT ADD bar[2]
203 | #SELECT ADD bar[3]
204 | ATTRIBUTE _color $cyan
205 | ATTRIBUTE _flend $remove
206 |
207 | # FLEND
208 | UDPARG flend slopea 1
209 | UDPARG flend slopeb 1
210 | UDPARG flend equis 1
211 | UDPARG flend toler 0.0001
212 | UDPARG flend npnt 12
213 | UDPRIM flend method 2
214 |
215 |
216 | # LETS JUST UNION SHIT TO GET MULTIPLE FINS
217 | STORE _BODY_FIN
218 | RESTORE _BODY_FIN
219 |
220 | PATBEG idx_fin F:num
221 | RESTORE _BODY_FIN
222 | ROTATEX idx_fin*F_deg_inc
223 | UNION 0 0 1e-4
224 | PATEND
225 |
226 | STORE _BODY_FINS
227 |
228 |
229 |
230 |
231 |
232 | ############################
233 | # BOATTAIL
234 | ######
235 |
236 | UDPRIM supell rx B:diam/2 ry B:diam/2 n 2.0
237 |
238 | UDPRIM supell rx B:diam/2 ry B:diam/2 n 2.0
239 | TRANSLATE 0 0 -.00001
240 |
241 | UDPRIM supell rx BOAT:aft_diam/2 ry BOAT:aft_diam/2 n 2.0
242 | TRANSLATE 0 0 -BOAT:len
243 |
244 | #RULE
245 | BLEND
246 |
247 | ROTATEY 90
248 |
249 | RESTORE _BODY_FINS
250 | UNION 0 0 1e-4
251 |
252 | STORE _BODY_FINS_BOAT
253 |
254 |
255 |
256 |
257 |
258 | ############################
259 | # NOSECONE - SPHERICALLY BLUNTED
260 | ######
261 | # See wikipedia: https://en.wikipedia.org/wiki/Nose_cone_design
262 | SET R B:diam/2
263 | SET L R/tand(NOSE:angd)
264 |
265 | SET x_t L^2/R*sqrt(NOSE:rad^2/(R^2+L^2))
266 | SET y_t x_t*R/L
267 | SET x_0 x_t+sqrt(NOSE:rad^2-y_t^2)
268 | SET x_a x_0-NOSE:rad
269 |
270 |
271 | SKBEG L 0 0
272 | LINSEG L R 0
273 | LINSEG x_t y_t 0
274 | CIRARC x_a 1E-6 0 x_a 0 0
275 | LINSEG L 0 0
276 | SKEND
277 | REVOLVE 0 0 0 1 0 0 180 # doing the mirror approach as getting weird things in GMSH
278 | STORE _temp
279 | RESTORE _temp
280 | MIRROR 0 0 1
281 | RESTORE _temp
282 | UNION 0 0 1e-4
283 | ROTATEZ 180
284 | TRANSLATE L 0 0
285 | TRANSLATE B:len 0 0
286 |
287 | # Attach to body
288 | RESTORE _BODY_FINS_BOAT
289 | UNION 0 0 1e-4
290 |
291 |
292 |
293 | ############################
294 | # OUTPUT
295 | ######
296 | #DUMP $/dump_1.brep 0 0 0
297 | DUMP $/rocket_stubby.step 0 0 0
298 |
299 |
300 |
301 | end
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [
](https://github.com/elliottmckee/cfd-meshman/blob/main/resource/images/banner_1_full.png?raw=true)
2 |
3 | # cfd-meshman
4 |
5 | Update 9/12/2025 - I am currently working on a unified tool in Rust so this workflow is less clunky.
6 |
7 | 3D unstructured mesh manufacturing/manipulation for a (currently) GMSH+NASA Mesh_Tools viscous CFD meshing stack:
8 | - [GMSH](https://gmsh.info/) for surface meshing
9 | - [NASA Mesh_Tools](https://software.nasa.gov/software/MSC-26648-1) for extruded boundary layer meshes
10 | - [GMSH](https://gmsh.info/) for farfield volume meshing
11 |
12 | cfd-meshman consists of the python tools required to get these components to talk to each other, to build combined extruded-boundary-layer + farfield volume meshes with only a handful of lines of code (see [example_simple.py](https://github.com/elliottmckee/cfd-meshman/blob/main/example_simple.py)).
13 |
14 | **The goal here is to try and create an acceptable viscous-CFD meshing workflow that is 100% free** (assuming you can access Mesh_Tools through the [NASA Software Catalog/Technology Transfer Program](https://software.nasa.gov/)).
15 |
16 | The main components/functionalities of cfd-meshman are:
17 | - **Umesh**: a "pythonic" representation of unstructured meshes, that primarily facilitates the conversion of mesh formats (GMSH v2.2 .msh <-> .ugrid)
18 | - **gen_blmesh.py**: given a surface mesh, uses NASA Mesh_Tools to extrude a boundary layer mesh
19 | - **gen_farfield.py**: given a boundary layer mesh (from above), uses GMSH to generate the farfield mesh between the boundary-layer and domain extents- and stitches everything together into a single domain.
20 |
21 | This workflow is by no means perfect. It is a WIP and thus has limitations and can be brittle; but is decent enough for my personal usage. I have tried to make it somewhat modular, so if you have a better solution for any of these steps, you can ideally just use what you need.
22 |
23 | _I welcome any help and/or feedback :)_
24 |
25 | > [!NOTE]
26 | > LINK TO LONGER-FORM BLOG POST TO BE ADDED HERE
27 |
28 |
29 |
30 | # Limitations
31 | - cfd-meshman only supports triangular surfaces meshes right now. Going to fix shortly. All the other tools (Mesh_Tools, GMSH) should be able to handle quads just fine.
32 | - this spits out meshes in GMSH .msh and/or (FUN3D) .ugrid formats. If you need another format for your solver, would recommend trying [meshio](https://github.com/nschloe/meshio).
33 | - no support for symmetry planes yet, although Mesh_Tools, GMSH should be able to support them.
34 | - this is currently focused on external-aero workflows, and there are a good few assumptions baked-in currently for domain tagging. See Usage below.
35 |
36 |
37 |
38 | # Examples
39 |
40 | ## [example_simple.py](https://github.com/elliottmckee/cfd-meshman/blob/main/example_simple.py)
41 | Just showing the simplest implementation of extruded boundary-layer + tet farfield:
42 | | **Near** | **Detail** | **Far** |
43 | | ----------- | ----------- | ----------- |
44 | | [
](https://github.com/elliottmckee/cfd-meshman/blob/main/resource/images/example_simple_1.png?raw=true) | [
](https://github.com/elliottmckee/cfd-meshman/blob/main/resource/images/example_simple_2.png?raw=true) | [
](https://github.com/elliottmckee/cfd-meshman/blob/main/resource/images/example_simple_3.png?raw=true) |
45 |
46 | ## [example_advanced.py](https://github.com/elliottmckee/cfd-meshman/blob/main/example_advanced.py)
47 | Showing the use of GMSH size fields to do things like refine the fins/wake or increase resolution at nose tip (note that size fields get applied congrously between the surface and volume meshes):
48 | | **Near** |
49 | | ----------- |
50 | | [
](https://github.com/elliottmckee/cfd-meshman/blob/main/resource/images/example_advanced_1.png?raw=true) |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | # Installation
60 | I have only used this on Linux. If you're on Windows i'd recommend using WSL2.
61 |
62 | The main dependency here that requires instruction is NASA Mesh_Tools. I recommend getting this installed and verifying it is working before playing around with this repo (as I have somewhat outlined below).
63 |
64 | > [!NOTE]
65 | > If you are having any issues with installation things, please feel free to reach out to me ([elliottmckee](https://github.com/elliottmckee)).
66 |
67 |
68 | ## [NASA Mesh_Tools](https://software.nasa.gov/software/MSC-26648-1)
69 |
70 | > [!CAUTION]
71 | > DO **NOT** USE VERSION 1.2. USE VERSION 1.1 (all versions should be included in the ZIP from the NASA request). The "extrude" functionality of v1.2 did not work for me out of the box, and requires code modifications to make it not segfault. Just use 1.1.
72 |
73 | You have to request this from the NASA Software Catalog. See links above.
74 |
75 | See [this paper](https://ntrs.nasa.gov/api/citations/20190000067/downloads/20190000067.pdf) for more detail on Mesh_Tools itself.
76 |
77 | Unfortunately, this one is a bit complicated to install, but if you follow the README included with it, it should get you up and running. It does requires you to build an older version of [VTK](https://docs.vtk.org/en/latest/build_instructions/index.html) with certain flags enabled, which is inconvenient at best, and can be really horrifying at its worst.
78 |
79 | I recomend just using the default Anaconda install for simplicity (**This is actually required for cfd-meshman to work currently**. I used miniconda personally, but shouldn't really matter though).
80 |
81 | Make sure to add the path to the Mesh_Tools 'extrude' binary (mesh_tools-v1.1.0/bin/extrude) to the system PATH.
82 |
83 | To confirm you've installed this correctly, try running the included extrude_for_char and extrude_shock examples and see if it breaks.
84 |
85 |
86 | ## cfd-meshman
87 | 1. Clone this repo
88 | 2. (recommended) create a virtual environment
89 | 3. cd into repo, `pip install -r requirements.txt`
90 | 4. Once installed, see if the simple cfd-meshman example above works. If any of the examples don't work, you're more than welcome to bug me.
91 |
92 | > [!NOTE]
93 | > The [current implementation](https://github.com/elliottmckee/cfd-meshman/blob/main/src/gen_blmesh.py) relies on the mesh_convert.py functionality included in Mesh_Tools. This is currently invoked using ['conda run'](https://docs.conda.io/projects/conda/en/latest/commands/run.html) functionality. If you have installed Mesh_Tools using conda above, you _shouldn't_ have any issue, **assuming** that the mesh-tools conda environement is named 'mesh_tools_p3p7'.
94 |
95 |
96 | # Usage
97 | - See examples.
98 | - If you need to modify the Mesh_Tools extrusion parameters (this is likely, it can be a bit finicky about these), modify [extrude_config.py](https://github.com/elliottmckee/cfd-meshman/blob/main/src/extrude_config.py).
99 |
100 |
101 | # Tips
102 | Mesh_Tools 'extrude':
103 | - The default extrude_config.py inputs seem to work fine-ish in my experience, but often need fiddling. I don't claim to know what all of them mean, though.
104 | - This will try and extrude as far as you tell it, and maintains a fixed extrude sizing across all elements. If you have a surface mesh with both large and small faces, and you want to extrude the big ones to near-isotropy, you're going to have some very high aspect-ratio cells grown from the smaller faces.
105 | - If extrusion is failing, try:
106 | - reducing size/number of layers, and/or increasing these incrementally
107 | - decreasing null space relaxation
108 | - increasing/decreasing null space iterations (or try disabling it?)
109 | - increasing/decreasing curvature_factor
110 |
111 | > [!NOTE]
112 | > If you're using FUN3D, the wall is tagged with 1, the farfield is tagged with 3
113 | > In MAPBC file format:
114 | ```
115 | 2
116 | 1 3000 wall
117 | 3 5050 farfield
118 | ```
119 |
120 |
121 | # Other useful things
122 | - [meshio](https://github.com/nschloe/meshio) _can_ be useful for translating meshes into other formats. This is probably better than my own custom .msh<->.ugrid conversion functions, but I just couldn't get it to do what I needed it to at the time.
123 | - [Paraview](https://www.paraview.org/) is what I use for viewing meshes once completed (after using meshio to convert from .msh to .vtk, for example)
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/ugrid_tools.py:
--------------------------------------------------------------------------------
1 | import os
2 | import io
3 | import time
4 | import numpy as np
5 | from scipy.spatial import KDTree
6 |
7 |
8 | class UMesh:
9 | '''
10 | DOESNT HANDLE THE OPTIONAL FLAGS N THINGS
11 |
12 | TODO: Can simplify and make less redundant
13 | - Still kinda think the empty numpy business is funny
14 | - Add negative volume checking
15 | - Get ugrid writer to work with .18e? not .18f? I think mesh_tools is what is breaking when trying to read in exponential formatted ugrid
16 | - VTP writer
17 | '''
18 |
19 | float_fmt = {'float_kind':lambda x: "%.18f" % x};
20 | int_fmt = {'int_kind':lambda x: "%i" % x}; # Force ints to not have an extra space
21 |
22 | el_type_node_counts = {'tris':3, 'quads':4, 'tets':4, 'pyrmds':5, 'prisms':6, 'hexes':8}
23 |
24 | gmsh_tag_types = {2:'tris', 3:'quads', 4:'tets', 5:'hexes', 6:'prisms', 7:'pyrmds'}
25 | gmsh_type_tags = {v: k for k, v in gmsh_tag_types.items()}
26 |
27 | def __init__(self, filename=''):
28 |
29 | self.filename = filename
30 | _, self.file_extension = os.path.splitext(self.filename)
31 |
32 | print(f'Reading in meshfile: {self.filename}')
33 |
34 | # Initialize empty numpy arrays for supported element types
35 | self.nodes = np.empty((0, 3), dtype=np.double)
36 | for el_type, nodecount in self.el_type_node_counts.items():
37 | temp = {'defs': np.empty((0, nodecount), dtype=np.uint32),
38 | 'tags': np.empty((0, 1), dtype=np.uint32)}
39 | setattr(self, el_type, temp)
40 |
41 | if self.file_extension == '.ugrid':
42 | self.read_ugrid()
43 | elif self.file_extension == '.msh':
44 | self.read_gmsh_v2()
45 | else:
46 | raise Exception('Unrecognized mesh file extension!')
47 |
48 |
49 | @property
50 | def num_nodes(self): return self.nodes.shape[0]
51 | @property
52 | def num_tris(self): return self.tris['defs'].shape[0]
53 | @property
54 | def num_quads(self): return self.quads['defs'].shape[0]
55 | @property
56 | def num_tets(self): return self.tets['defs'].shape[0]
57 | @property
58 | def num_pyrmds(self): return self.pyrmds['defs'].shape[0]
59 | @property
60 | def num_prisms(self): return self.prisms['defs'].shape[0]
61 | @property
62 | def num_hexes(self): return self.hexes['defs'].shape[0]
63 |
64 | @property
65 | def num_bdr_elems(self): return self.num_tris + self.num_quads
66 | @property
67 | def num_vol_elems(self): return self.num_tets + self.num_pyrmds + self.num_prisms + self.num_hexes
68 | @property
69 | def num_elements(self): return self.num_bdr_elems + self.num_vol_elems
70 |
71 | @property
72 | def iter_elem_data(self): return [self.tris, self.quads, self.tets, self.pyrmds, self.prisms, self.hexes]
73 | @property
74 | def iter_elem_counts(self): return [self.num_tris, self.num_quads, self.num_tets, self.num_pyrmds, self.num_prisms, self.num_hexes]
75 | @property
76 | def iter_elem_type_strs(self): return ['tris', 'quads', 'tets', 'pyrmds', 'prisms', 'hexes']
77 | @property
78 | def iter_boundary_data(self): return [self.tris, self.quads]
79 | @property
80 | def iter_volume_data(self): return [self.tets, self.pyrmds, self.prisms, self.hexes]
81 |
82 | def scale(self, scaleFac):
83 | self.nodes = scaleFac * self.nodes
84 |
85 |
86 |
87 | def read_ugrid(self):
88 | '''
89 | https://www.simcenter.msstate.edu/software/documentation/ug_io/3d_grid_file_type_ugrid.html
90 |
91 | ASSUMES VOLUME TAGS ARE 0 (not modified)
92 | '''
93 |
94 | with open(self.filename) as ufile:
95 | # get, parse header
96 | header = ufile.readline().strip().split(' ')
97 |
98 | self.nodes = np.resize(self.nodes, (int(header[0]), 3))
99 |
100 | # resize numpy array based on number of each geometry type
101 | for gdata, gdims, n_geoms in zip(self.iter_elem_data, self.el_type_node_counts.values(), header[1:]):
102 | gdata['defs'] = np.resize(gdata['defs'], (int(n_geoms), gdims))
103 | gdata['tags'] = np.resize(gdata['tags'], (int(n_geoms), 1))
104 |
105 | # Get nodes
106 | for i_pts in range(self.num_nodes):
107 | self.nodes[i_pts, :] = ufile.readline().strip().split(' ')
108 |
109 | # Get boundary defs
110 | for geom_data in self.iter_boundary_data:
111 | for i_geom in range(geom_data['defs'].shape[0]):
112 | geom_data['defs'][i_geom, :] = ufile.readline().strip().split(' ')
113 |
114 | # Get boundary tags
115 | for geom_data in self.iter_boundary_data:
116 | for i_geom in range(geom_data['defs'].shape[0]):
117 | geom_data['tags'][i_geom, :] = ufile.readline().strip().split(' ')
118 |
119 | # Get volume defs
120 | for geom_data in self.iter_volume_data:
121 | for i_geom in range(geom_data['defs'].shape[0]):
122 | geom_data['defs'][i_geom, :] = ufile.readline().strip().split(' ')
123 |
124 | if ufile.readline():
125 | print("It looks like there is more file... additional/optional tags may exist, but aren't being read in!")
126 |
127 |
128 |
129 | def read_gmsh_v2(self):
130 | '''
131 | https://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format-version-2-_0028Legacy_0029
132 |
133 | ASSUMES
134 | - NODES START AT 1 AND COUNT UP
135 | - PHYSICAL REGION TAG (first one following geomtype tag) IS WHAT WE CARE ABOUT
136 | TODO:
137 | - Make syntax at end less bad
138 | '''
139 | print(f'Reading gmsh v2.2 ASCII file: {self.filename} \n')
140 |
141 | nodes = []
142 | elements = []
143 |
144 | with open(self.filename) as mshfile:
145 |
146 | # burn first lines
147 | line = mshfile.readline() #$MeshFormat
148 | line = mshfile.readline().strip().split(' ') #2.2 0 8
149 | if line[0]!='2.2': raise Exception('Needs to be .msh v2.2 you ding dong')
150 | line = mshfile.readline() #$EndMeshFormat
151 | line = mshfile.readline() #$Nodes
152 |
153 | # read-in block data
154 | while line:
155 | if line.strip() == '$Nodes':
156 | num_nodes = int(mshfile.readline().strip()) #num nodes
157 | for i_node in range(num_nodes):
158 | line = mshfile.readline().strip().split(' ')
159 | nodes.append([float(x) for x in line[1:]])
160 | line = mshfile.readline() #$EndNodes
161 |
162 | if line.strip() == '$Elements':
163 | num_elems = int(mshfile.readline().strip()) #num elems
164 | for i_elem in range(num_elems):
165 | line = mshfile.readline().strip().split(' ')
166 | elements.append([int(x) for x in line])
167 | line = mshfile.readline() #$EndElements
168 | line = mshfile.readline()
169 |
170 | # Parse elements by appending to lists
171 | for data in self.iter_elem_data:
172 | data['defs'] = []
173 | data['tags'] = []
174 |
175 | for el in elements:
176 | if el[1] in self.gmsh_tag_types.keys():
177 | if el[2] != 2: raise Exception('Currently assuming only 2x tags exist per element in GMSH file')
178 | data = getattr(self, self.gmsh_tag_types[el[1]])
179 | data['defs'].append(el[5:])
180 | data['tags'].append(el[3])
181 |
182 | # Convert all back to numpy
183 | self.nodes = np.array(nodes, dtype=np.double)
184 | for data in self.iter_elem_data:
185 | data['defs'] = np.array(data['defs'], dtype=np.uint32)
186 | data['tags'] = np.array(data['tags'], dtype=np.uint32)
187 |
188 |
189 | def write(self, outfile):
190 |
191 | start_time = time.time()
192 | _, ext = os.path.splitext(outfile)
193 |
194 | match ext:
195 | case '.ugrid':
196 | self.write_ugrid(outfile)
197 | case '.msh':
198 | self.write_gmsh_v2(outfile)
199 | case _:
200 | raise Exception('Invalid extension specified!')
201 |
202 | print(f'Completed in {time.time()-start_time}!\n')
203 |
204 |
205 |
206 | def write_ugrid(self, outfile):
207 | '''
208 | https://www.simcenter.msstate.edu/software/documentation/ug_io/3d_grid_file_type_ugrid.html
209 | '''
210 | print(f'Writing ugrid to {outfile}...')
211 |
212 | with open(outfile, 'w') as outfile:
213 |
214 | # write header
215 | header = f"{self.num_nodes} {self.num_tris} {self.num_quads} {self.num_tets} {self.num_pyrmds} {self.num_prisms} {self.num_hexes}"
216 | outfile.write(header+'\n')
217 |
218 | # write nodes
219 | np.savetxt(outfile, self.nodes, fmt='%.18f')
220 |
221 | # write boundary faces
222 | for geom_data in self.iter_boundary_data:
223 | np.savetxt(outfile, geom_data['defs'], fmt='%i')
224 |
225 | # write boundary tags
226 | for geom_data in self.iter_boundary_data:
227 | np.savetxt(outfile, geom_data['tags'], fmt='%i')
228 |
229 | # write volumes
230 | for geom_data in self.iter_volume_data:
231 | np.savetxt(outfile, geom_data['defs'], fmt='%i')
232 |
233 |
234 |
235 | def write_gmsh_v2(self, outfile):
236 | '''
237 | Making this able to write volumes, to see if I can just read everything into gmsh and not have to stitch together outside
238 | https://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format-version-2-_0028Legacy_0029
239 |
240 | ASSUMING THAT, WHEN WRITING, THAT PHYSICAL AND ELEMENTARY TAGS ARE THE SAME
241 | '''
242 | print(f'Writing gmsh v2 ASCII to: {outfile}')
243 | start_time = time.time()
244 |
245 | # pre-formatting nodes block
246 | node_block = np.concatenate((np.arange(1, self.num_nodes+1).reshape(-1,1), self.nodes), axis=1)
247 |
248 | #pre-formatting elements blocks to try and speed things up
249 | element_blocks = []
250 | ctr_el = 1
251 | for geom_data, geomtype_str, geom_num_members in zip(self.iter_elem_data, self.iter_elem_type_strs, self.iter_elem_counts):
252 |
253 | # assuming only 2x tags (element, physical) for now
254 | gmsh_tag = self.gmsh_type_tags[geomtype_str]
255 | type_numtag_data = np.tile(np.array([gmsh_tag, 2]), (geom_num_members,1))
256 |
257 | # assemble, append block
258 | if geom_num_members > 0:
259 | data_block = np.concatenate((np.arange(ctr_el, ctr_el+geom_num_members).reshape(-1,1), #god i fucking hate numpy
260 | type_numtag_data, geom_data['tags'], geom_data['tags'],
261 | geom_data['defs']), axis=1)
262 | element_blocks.append(data_block)
263 |
264 | #update counter
265 | ctr_el = ctr_el+geom_num_members
266 |
267 |
268 | with open(outfile, 'w') as outfile:
269 | outfile.write('$MeshFormat\n')
270 | outfile.write('2.2 0 8\n')
271 | outfile.write('$EndMeshFormat\n')
272 |
273 | outfile.write('$Nodes\n')
274 | outfile.write(f'{self.num_nodes}\n')
275 | np.savetxt(outfile, node_block, fmt=['%i','%.18e','%.18e','%.18e'])
276 | outfile.write('$EndNodes\n')
277 |
278 | outfile.write('$Elements\n')
279 | outfile.write(f'{self.num_elements}\n')
280 | for block in element_blocks:
281 | np.savetxt(outfile, block, fmt='%i')
282 | outfile.write('$EndElements\n')
283 |
284 |
285 |
286 | def extract_surface(self, bc_target):
287 |
288 | print(f'Extracting boundary faces tagged with {bc_target} to new UMesh...')
289 | OutMesh = UMesh()
290 |
291 | # get index of boundary tris, quads with given target
292 | tri_idx = np.argwhere(self.tris['tags'] == bc_target)[:,0]
293 | quad_idx = np.argwhere(self.quads['tags'] == bc_target)[:,0]
294 |
295 | # assign boundary connectivity data to new object
296 | OutMesh.tris['defs'] = self.tris['defs'][tri_idx]
297 | OutMesh.tris['tags'] = self.tris['tags'][tri_idx]
298 | OutMesh.quads['defs'] = self.quads['defs'][quad_idx]
299 | OutMesh.quads['tags'] = self.quads['tags'][quad_idx]
300 |
301 | # get node ID's on targeted boundary
302 | out_nodes_unique = set()
303 |
304 | for geom_data in OutMesh.iter_boundary_data:
305 | for i_geom in range(geom_data['defs'].shape[0]):
306 | out_nodes_unique.update(geom_data['defs'][i_geom, :])
307 |
308 | out_nodelist_1dx = list(out_nodes_unique)
309 | out_nodelist_1dx.sort()
310 | out_nodelist_0dx = [x-1 for x in out_nodelist_1dx]
311 |
312 | # assign relevant nodes to new object
313 | OutMesh.nodes = self.nodes[out_nodelist_0dx, :]
314 |
315 | # Renumber to contiguous nodes (counting up from one) in boundary element definitions
316 | mapping = {orig: new+1 for orig, new in zip(out_nodelist_1dx, range(OutMesh.num_nodes))}
317 |
318 | for geom_data in OutMesh.iter_boundary_data:
319 | for ir, ic in np.ndindex(geom_data['defs'].shape):
320 | geom_data['defs'][ir,ic] = mapping[geom_data['defs'][ir,ic]]
321 |
322 | return OutMesh
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
--------------------------------------------------------------------------------