Cut results and selections#
cut(mesh, level_set) returns a CutResult. Index the result with a
condition built from the level set to obtain a MeshPart:
result = cutcells.cut(mesh, phi)
inside = result[phi < 0]
outside = result[phi > 0]
interface = result[phi == 0]
result["phi < 0"] # the same as inside, as a string
inside, outside and interface are selected mesh parts. They expose
counts, cell IDs, quadrature rules, visualisation meshes and VTU output.
Conditions#
A level set compared with 0 is a condition, and & (and) and | (or)
combine conditions, as boolean masks do in pandas:
result = cutcells.cut(mesh, [body, fluid], names=["body", "fluid"])
solid = result[body < 0]
around = result[(body > 0) & (fluid < 0)]
surface = result[body == 0]
edge = result[(body == 0) & (fluid == 0)]
The clauses are
< 0,> 0and== 0; a number other than 0 raises.&binds more tightly than<, so each comparison needs parentheses.There is no
~:body > 0is the opposite ofbody < 0. A condition has no truth value, soand,orandnotraise.<=,>=and!=give no conditions;<=and>=remain in strings.The result names the level sets of a condition as
cutnamed them, matched by identity. A level set that was not given tocutraises.Level sets still compare and hash by identity with each other, so they work in lists and sets and as dictionary keys.
str(condition) writes the selection string with the level sets’ own names,
for example "body > 0 and fluid < 0". An analytic level set without a name
(name=) prints as a placeholder. The type is cutcells.Condition.
Strings#
Selection strings are the shorthand, as df.query is in pandas. They use the
names of the level sets and comparisons against zero:
expression: term ('or' term)*
term: factor ('and' factor)*
factor: clause | '(' expression ')'
clause: name ('<' | '<=' | '>' | '>=' | '=' | '==') '0'
Examples:
result["phi < 0"]
result["phi = 0"]
result["phi == 0"]
result["sphere < 0 and plane > 0"]
result["sphere = 0 and plane < 0"]
result["sphere < 0 or plane < 0"]
The parser is case-insensitive for the and and or keywords; and binds
more tightly than or, and parentheses group, as in
"(sphere < 0 or plane < 0) and box > 0". A name is a word without spaces,
parentheses, <, > or =. The right-hand side must be exactly 0. The
<= and >= operators select the same sides as < and >, and == the same
as =.
Level sets made of level sets#
A level set made of level sets (see Level sets) is selected
like any other. Its selections expand into selections of its level sets. For
ring with inside F:
ring < 0isF;ring > 0is the opposite ofF:andandorswap, and so do< 0and> 0;ring == 0is its faces: for each of its level setsa, the part ofa = 0whereFwithanegative differs fromFwithapositive.
ring = cutcells.LevelSetFunction(level_sets=[ball, cyl],
inside=(ball < 0) & (cyl > 0), name="ring")
result = cutcells.cut(mesh, [ring, fluid])
solid = result[ring < 0] # ring.ball < 0 and ring.cyl > 0
around = result[(ring > 0) & (fluid < 0)] # (ring.ball > 0 or ring.cyl < 0) and fluid < 0
surface = result[ring == 0] # the sphere zone and the inner cylinder
A level set used with both signs gives no face where the inside holds on both
of its sides. In two boxes side by side whose common face is one level set
p, the inside of their union holds with p negative and with p positive
alike, so that face is not part of the union’s surface.
Faces: part.on(*names) keeps the faces of a surface on the level sets
named. Several names keep several faces, and the name of a level set made of
level sets keeps all its faces:
hole = surface.on("cyl") # the inner cylinder
zone = surface.on("ring.ball") # the sphere zone, by its full name
both = surface.on("ball", "cyl") # the whole surface
Names: in result.level_set_names, the level sets of ring have full
names (ring.ball, ring.cyl), and strings may use them. Short names
("cyl < 0") work wherever only one level set has that short name. In
.on(...), a short name resolves among the part’s faces first.
result.insides maps each level set made of level sets to its inside.
Conditions on its level sets select them by their full names:
result[cyl < 0] is result["ring.cyl < 0"], at any depth. A level set given
to two level sets made of level sets of one cut raises; select it with a string
then.
Dimension inference#
The selected entity dimension is inferred from equality clauses:
No equality clause selects volume cells of dimension
tdim.One equality clause selects an interface of dimension
tdim - 1.Two equality clauses select an intersection of dimension
tdim - 2.
For example, in a tetrahedral mesh:
volume = result[(sphere < 0) & (plane < 0)] # dim 3
disk = result[(plane == 0) & (sphere < 0)] # dim 2
curve = result[(plane == 0) & (sphere == 0)] # dim 1
All or terms of one selection (joined by |, or by or in a string) must
infer the same dimension. Curves get line rules, and points in 2D get unit
weights: curve.quadrature(order=4).
Pieces on facets, edges and vertices#
A selection of lower dimension can hold whole facets, edges or vertices of
the mesh: the interface plane == 0 where the plane runs through grid faces,
the curve (a == 0) & (b == 0) where both vanish along mesh edges, the point
where two lines cross at a mesh vertex in 2D. A term holds such an entity
whole when
every level set of its
== 0clauses vanishes on the entity and changes sign across it: some cell sharing the entity lies on its negative side and some on its positive side (on the mesh boundary, one side is enough);none of its
< 0or> 0clauses is on a level set vanishing there;each of its other clauses holds on a cell sharing the entity, whole.
A level set that vanishes on an entity without changing sign across it is no
surface there: min(box1, box2) vanishes on the face two boxes share and is
negative on both sides. part.entities(dim) lists the entities a part holds,
and part.entity_quadrature(order) gives their rules (see
Quadrature and output).
Result fields#
Useful CutResult fields:
print(result.num_cells)
print(result.num_cut_cells)
print(result.num_level_sets)
print(list(result.level_set_names))
print(result.cut_cells) # cells that some level set cuts
print(result.domains.shape) # (num_level_sets, num_cells): 0 inside, 1 cut, 2 outside
print(result.entities(2)) # the facets of a 3D mesh on which a level set vanishes
print(result.zero_faces) # facets lying in a zero set, with one owning cell each
print(result.insides) # level sets made of level sets, with their insides
print(result.backend) # the default backend of parts selected from now on
result.entities(dim) is an EntityTable of the entities of dimension dim
(vertices, edges, facets) on which a level set vanishes. Each comes with
every cell that shares it, its number in each (Basix numbering), the level sets that
vanish on it (vanishing), those of them that count there (zero: they
change sign across it, or it lies on the mesh boundary), and each sharing
cell’s side of them (negative, positive). Its arrays are CSR:
facets = result.entities(2)
for e in range(facets.num_entities):
nodes = facets.nodes[facets.node_offsets[e]:facets.node_offsets[e + 1]]
cells = facets.cells[facets.cell_offsets[e]:facets.cell_offsets[e + 1]]
A level set vanishes on an entity if the bound of its restriction there (its
Bernstein coefficients restricted to the entity, Taylor bounds over the
entity itself for an analytic level set) is within 64 eps of the level set’s
largest magnitude over the cells at its zero set. The same threshold decides
whether a cell beside the entity touches zero there, so that no zero set is
both on an entity and in a cell next to it. result.zero_faces lists the
facets in a zero set with one owning cell each, the cell on the negative side
(else the lowest).
Useful MeshPart fields:
part = result[phi < 0]
print(part.dim)
print(part.num_cut_cells, part.num_uncut_cells)
print(part.cut_cells)
print(part.uncut_cells)
print(part.backend)
print(result[phi == 0].entities(2)) # facets it holds whole: indices into result.entities(2)
num_uncut_cells matters for volume selections: mode="full" includes those
uncut parent cells, while mode="cut_only" restricts output and quadrature to
cut cells.