Surfaces¶
The geometry snappyHexMesh meshes around (buildings, terrain, trees) and surfaceFeatures
takes feature edges from is a set of STL files in constant/triSurface. umcfGui reads them,
says what in them would break meshing, and repairs what can be repaired safely.
Reading STL files¶
umcfGui reads ASCII and binary STL, gzip-compressed or not, without trusting the file's header:
a binary STL is an 80-byte header, a triangle count and 50 bytes per triangle, so a file of
exactly that size is binary, even when its header starts with solid. A file that starts
with solid but isn't text is binary too. Files with a few bytes after the last triangle, or
a count of 0 followed by triangles, are read as well, with a note.
It also says when OpenFOAM would read the file differently (surface.openfoam):
- OpenFOAM picks the reader by the extension, case-sensitively:
.stl(and.stl.gz) by the first word,.stlbalways as binary. A.STLfile it doesn't read at all (surface.extension);geometry.importtakesBuilding.STLin asBuilding.stl. - A
.stlfile whose first word starts withsolidis ASCII to OpenFOAM, so a binary STL whose header starts withsolid(some CAD programs write one) fails to read. An ASCII.stlbfile fails too. - OpenFOAM refuses a binary file whose triangle count doesn't fit its size.
- OpenFOAM stops at the first line of an ASCII file it can't read: anything but
solid,facet normal,outer loop,vertex,endloop,endfacet,endsolidandcolor; keywords in mixed case (Solid); a number such asinfor a normal ofnan; words after a vertex orendfacet; a solid name that isn't ASCII. umcfGui skips facets it can't read either, reads the rest, and counts both.
Writing the surface again gives a file OpenFOAM reads: geometry.repair does. With
"degenerate": false it repairs nothing, but the file is still written anew: triangles grouped
by solid, solid names as OpenFOAM reads them, facet normals from the winding, color lines and
a binary file's header dropped, and triangles with coordinates that are not numbers left out
(no reader takes them; the message counts them).
An ASCII file's solids are the surface's regions, which become patches after
snappyHexMesh. OpenFOAM names a region after its solid without the characters a word can't
have (Building 1 is Building1), calls an unnamed one solid, and joins solids of the same
name; a solid without triangles is still a region (an empty patch). A binary file's regions are
its triangles' attribute values: OpenFOAM makes patch0 up to patch<N> for the largest value
N, empty ones included, so a program that keeps a colour there gives thousands of empty
patches (surface.regions). umcfGui names and counts them the same way.
Checks¶
Vertices are joined as OpenFOAM joins them when it reads an STL file: where they are within
1e-13 m (ASCII) or 1e-14 m (binary) of each other. Edges then follow from the triangles. On the
local cases and OpenFOAM's tutorial surfaces the vertex, open and non-manifold edge, region
and part counts are the same as surfaceCheck's.
| Code | Level | What it means |
|---|---|---|
surface.unreadable |
Error | The file can't be read as an STL surface (the message says why) |
surface.openfoam |
Error | OpenFOAM can't read the file as it is (see above) |
surface.non-finite |
Error | Triangles with a coordinate that is not a finite number |
surface.read |
Info | Something odd that did not stop the file from being read (facets or lines skipped, bytes after the last triangle) |
surface.summary |
Info | Format, triangles, regions, extent, parts and how many of them are closed |
surface.empty |
Warning | The file has no triangles |
surface.extension |
Error | The file's extension is an STL one in the wrong case (.STL): OpenFOAM doesn't read it |
surface.renamed |
Info | A solid whose name OpenFOAM changes, and the name it uses |
surface.regions |
Warning | Regions without triangles: empty solids, or attribute values no triangle has below the largest one |
surface.degenerate |
Warning | Triangles without area: two vertices in the same place, or all three in a line. Measured as OpenFOAM measures a triangle's quality (its area over that of the equilateral triangle with the same circumcircle), below 1e-15: where surfaceCheck warns the surface "might give problems in self-intersection testing" |
surface.duplicate |
Warning | Triangles with the same three vertices as another |
surface.non-manifold |
Warning | Edges shared by more than two triangles: the surface has no single inside there |
surface.normals |
Warning | Two triangles on an edge face opposite ways. surfaceFeatures then takes the edge for a fold (on the 3×3 case's tree crowns it classes 880 of 1920 convex edges as concave), and snappyHexMesh decides what is inside a closed surface from its normals (cell zones, inside/outside refinement) |
surface.inward |
Warning | A closed, consistently wound part whose normals point inward: its outside is taken for its inside |
surface.open |
Info | Edges of one triangle. Fine for terrain or a surface that reaches the domain boundary; a solid cut out of the mesh, or a cell zone, must be closed |
surface.stored-normals |
Info | Facet normals in the file that point against the winding. OpenFOAM uses the winding and ignores them; other programs may not |
surface.units |
Warning | The surface is more than 20 km across: probably millimetres (OpenFOAM reads STL coordinates as metres) |
surface.far |
Warning | Coordinates more than 131 km from the origin (projected coordinates): an STL file's 32-bit numbers are a centimetre or more apart there (0.25 m at Swiss LV95 coordinates), which moves vertices and can open gaps |
A part is a set of triangles connected through their edges; a part with no open or non-manifold edges is closed. Duplicate triangles stay in the edges, as they do for OpenFOAM: a copy among other triangles makes their edges non-manifold, and two copies on their own make a closed pair (removing one of them leaves a single, open triangle).
With --json (or the commands' data), each finding lists its triangles (the first 1000, by
their index in the file) with their count, so they can be shown.
Repairs¶
| Command | Does |
|---|---|
geometry.repair with weld |
Moves every vertex within the given distance (metres) of one kept before it onto the nearest such one, which closes gaps narrower than that; every copy of a vertex goes to the same place, and triangles that lose their area are removed. A distance close to the size of the surface's features can join vertices that shouldn't be: keep it well below the smallest edge |
geometry.repair with degenerate |
Removes triangles that are not finite, copies of another, and triangles without area. A triangle without area whose vertices are in a line shares its long edge with a neighbour: removing it alone would open a crack there, so the neighbour is split in two at the middle vertex instead, and the surface stays closed. One that shares no edge with exactly one other triangle is removed as it is. The others are left and counted: their long edge is open or shared by more than two triangles while a short one is shared, or they lie in a chain of triangles without area that splitting doesn't undo |
geometry.repair with orient |
Makes the normals of each part consistent across the edges two triangles share, then turns closed parts that point inward outward. An open part keeps the side most of its triangles already face. Where no consistent choice exists (a Möbius-like part) the triangles are left as they are and counted |
geometry.scale |
Multiplies every coordinate by factor (0.001 turns millimetres into metres); a factor that is not positive is refused, as it would mirror the surface, and so is one that takes a coordinate beyond what STL's 32-bit numbers hold |
Without weld, degenerate or orient, geometry.repair does degenerate and orient. They
run in that order, and the surface is written again in its own format, as described under
Reading STL files (binary for a .stlb, with the triangles' attribute
values). A repair is checked again after it runs, so what is left shows straight away.
On the 3×3 case, geometry.repair removes all 2252 zero-area triangles of walls.stl (in the
ground) by splitting their neighbours: surfaceCheck then finds
the same 320948 triangles, 4 regions and 1088 open edges, and its lowest triangle quality goes
from 0 to 2e-4. It makes the normals of the 40 tree crowns of air_to_vegetation.stl
consistent (720 triangles turned over), after which surfaceFeatures classes all 1920 of
their edges as convex.
Importing¶
geometry.import copies an STL file into constant/triSurface, as it is, under its own name or
name. It is written again only when it is scaled (scale), when OpenFOAM can't read it as it
is (an ASCII file named .stlb is written binary), or when its compression doesn't match its
name (.gz). A surface already there is replaced only with "replace": true. The name must
end in .stl, .stlb, .stl.gz or .stlb.gz, in lower case, which OpenFOAM reads as STL.
Tree crowns¶
geometry.trees writes the trees' crowns as one surface (trees.stl, or name), for meshing
the vegetation: a rhombicuboctahedron per tree, of its crown diameter and height, standing on
its z (the bottom of the foliage). Each crown is closed and wound outward, 44 triangles. The
trees come from a table, in the case (table) or outside it (path), or as a list (trees):
x y z CD CH
260 260 2.5 5.5 5.5
330 330 2.5 5.5 5.5
One tree a line, the numbers separated by tabs, commas or spaces, one kind for the whole table (the
first line's): with tabs or commas an empty cell is an error, not skipped, and a cell may hold
spaces (a species name), and one in double quotes commas too, as spreadsheets and R's write.csv
write them. The first line names the columns, x y z CD CH in any order, with other columns left
alone (a tree height H beside the crown's CH, say); D/H or diameter/height stand for
the crown's when there is no CD/CH. Without that line a tree is exactly five numbers, in that
order. A line with more cells than the header has names (R's row names) is refused, and so is a
space-separated line with fewer: spaces can't mark an empty cell. Lines starting with # or //
are skipped. At most 80 000 trees, which keeps the surface within the case's file size limit; a
crown much smaller than its coordinates (beyond the seven digits STL holds) is refused. The surface
has one region, trees (or solid, in printable ASCII); a binary .stlb has no region names, so
solid is refused with one. Only the crowns are made, no trunks: the solver's vegetation model
works on the leaf area density of the tree zone, which the crowns bound.
The shape is the one the vegetation workflow's tree script (tree_shape_simple.py) makes. That
script writes the eight triangular faces as squares with a corner twice (16 triangles without
area for two trees), winds some faces inward, and centres each crown on its z, which the table
calls the bottom of the foliage: a crown of 5.5 m at z = 2.5 reaches 0.25 m below the ground. A
table written for it gives crowns half their height higher here.
Imported and repaired surfaces are part of the case's unsaved changes: case.save writes them,
after a backup of the file each replaces, and refuses to overwrite a file that changed on disk
since the change was made from it. Until then, edit.undo takes a change back; after a save,
undo doesn't put the old file back on disk.
A surface that is a symbolic link (to a field, a shared surface, or nothing) is not written,
neither by the command nor by the save if the link appeared since: the save would write the file
it points to. A linked directory, such as a shared constant/triSurface, is followed as for any
save.
On the command line¶
umcf-cli surface path/to/case [--file walls.stl] [--json]
umcf-cli surface path/to/file.stl [--json]
Given a case, it checks every STL surface in constant/triSurface (or the one --file
names); given a file, that file:
info surface.summary constant/triSurface/walls.stl: ASCII 320948 triangles, 4 regions (roofs, ground, street, buildings), 590 × 590 × 10 m from (0 0 0); 1 part (0 closed)
warning surface.degenerate constant/triSurface/walls.stl: 2252 triangles without area (…)
1 surface; 0 errors, 1 warning, 2 notes
The exit status is 0 when there are no errors, 1 when there are (a file that can't be read is
one), and 2 when the case or file can't be opened. Repairs run through umcf-cli run:
umcf-cli run --case path/to/case --cmd 'geometry.repair={"file": "walls.stl"}' --cmd case.save
Limits¶
- Only STL is read. OBJ, VTK and the other formats OpenFOAM reads in
constant/triSurfaceare not checked. - Whether a surface lies inside the background mesh, and whether
snappyHexMeshneeds it closed (a cell zone, aninsiderefinement), is not checked yet: that comes with the meshing set-up checks. - A surface is read whole, up to 1 GB (decompressed) and 10 million triangles. Checking 5
million triangles takes about 5 s and 1 GB of memory. Each change keeps its own copy of the
file's bytes in memory for undo, up to the 100 steps undo keeps: repairing a large surface
many times takes that many copies (
case.savedoesn't release them).