Skip to content

Commit 6865fdd

Browse files
authored
Enhance solidmodel boundary methods (#56)
* Enhance get_boundary and add set_periodic function * Fix formatting * Update docs and changelog * Fix bugs and add tests for get_boundary * Add test for set_periodic * Add test for get_physical_group_bounding_box * Address PR feedback
1 parent b98f9d7 commit 6865fdd

File tree

4 files changed

+199
-6
lines changed

4 files changed

+199
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ The format of this changelog is based on
44
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
55
[Semantic Versioning](https://semver.org/).
66

7+
## Upcoming
8+
9+
- Added `set_periodic!` to `SolidModels` to enable periodic meshes.
10+
711
## 1.2.0 (2025-04-28)
812

913
- Composite components can define `_build_subcomponents` to return a `NamedTuple` with keys that differ from component names

docs/src/solidmodels.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Other operations:
6060
SolidModels.remove_group!
6161
SolidModels.restrict_to_volume!
6262
SolidModels.revolve!
63+
SolidModels.set_periodic!
6364
SolidModels.translate!
6465
```
6566

src/solidmodels/postrender.jl

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -809,8 +809,8 @@ function fragment_geom!(
809809
end
810810

811811
"""
812-
get_boundary(group::AbstractPhysicalGroup; combined=true, oriented=true, recursive=false)
813-
get_boundary(sm::SolidModel, groupname, dim=2; combined=true, oriented=true, recursive=false)
812+
get_boundary(group::AbstractPhysicalGroup; combined=true, oriented=true, recursive=false, direction="all", position="all")
813+
get_boundary(sm::SolidModel, groupname, dim=2; combined=true, oriented=true, recursive=false, direction="all", position="all")
814814
815815
Get the boundary of the model entities in `group`, given as a vector of (dim, tag) tuples.
816816
@@ -821,14 +821,19 @@ Return tags multiplied by the sign of the boundary entity if `oriented` is true.
821821
822822
Apply the boundary operator recursively down to dimension 0 (i.e. to points) if `recursive`
823823
is true.
824+
825+
If `direction` is specified, return only the boundaries perperdicular to the x, y, or z axis. If `position` is also specified,
826+
return only the boundaries at the min or max position along the specified `direction`.
824827
"""
825828
function get_boundary(
826829
sm::SolidModel,
827830
group,
828831
dim=2;
829832
combined=true,
830833
oriented=true,
831-
recursive=false
834+
recursive=false,
835+
direction="all",
836+
position="all"
832837
)
833838
if !hasgroup(sm, group, dim)
834839
@info "get_boundary(sm, $group, $dim): ($group, $dim) is not a physical group, thus has no boundary."
@@ -838,16 +843,129 @@ function get_boundary(
838843
sm[group, dim];
839844
combined=combined,
840845
oriented=oriented,
841-
recursive=recursive
846+
recursive=recursive,
847+
direction=direction,
848+
position=position
842849
)
843850
end
844851
function get_boundary(
845852
group::AbstractPhysicalGroup;
846853
combined=true,
847854
oriented=true,
848-
recursive=false
855+
recursive=false,
856+
direction="all",
857+
position="all"
858+
)
859+
all_bc_entities = gmsh.model.getBoundary(dimtags(group), combined, oriented, recursive)
860+
if direction == "all"
861+
return all_bc_entities
862+
else
863+
if lowercase(direction) ["all", "x", "y", "z"]
864+
@info "get_boundary(sm, $group): direction $direction is not all, X, Y, or Z, thus has no boundary."
865+
return Tuple{Int32, Int32}[]
866+
end
867+
if lowercase(position) ["all", "min", "max"]
868+
@info "get_boundary(sm, $group): position $position is not all, min, or max, thus has no boundary."
869+
return Tuple{Int32, Int32}[]
870+
end
871+
direction_map = Dict("x" => 1, "y" => 2, "z" => 3)
872+
direction_id = direction_map[lowercase(direction)]
873+
bboxes = Dict()
874+
for (dim, tag) in all_bc_entities
875+
bboxes[tag] = gmsh.model.getBoundingBox(dim, abs(tag))
876+
end
877+
target_min = minimum(bbox[direction_id] for bbox in values(bboxes))
878+
target_max = maximum(bbox[direction_id + 3] for bbox in values(bboxes))
879+
880+
bc_entities = []
881+
for (dim, tag) in all_bc_entities
882+
bbox = bboxes[tag]
883+
min_val = bbox[direction_id]
884+
max_val = bbox[direction_id + 3]
885+
886+
# Check if the boundary is perpendicular to the direction
887+
!isapprox(min_val, max_val, atol=1e-6) && continue
888+
889+
# Check if at domain min/max position
890+
if lowercase(position) == "min" || lowercase(position) == "all"
891+
isapprox(min_val, target_min, atol=1e-6) && push!(bc_entities, (dim, tag))
892+
end
893+
if lowercase(position) == "max" || lowercase(position) == "all"
894+
isapprox(max_val, target_max, atol=1e-6) && push!(bc_entities, (dim, tag))
895+
end
896+
end
897+
return unique(bc_entities)
898+
end
899+
end
900+
901+
function get_bounding_box(dim, tags)
902+
xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(dim, tags[1])
903+
904+
for tag in tags[2:end]
905+
x_min, y_min, z_min, x_max, y_max, z_max = gmsh.model.getBoundingBox(dim, tag)
906+
xmin = min(xmin, x_min)
907+
ymin = min(ymin, y_min)
908+
zmin = min(zmin, z_min)
909+
xmax = max(xmax, x_max)
910+
ymax = max(ymax, y_max)
911+
zmax = max(zmax, z_max)
912+
end
913+
914+
return (xmin, ymin, zmin, xmax, ymax, zmax)
915+
end
916+
917+
"""
918+
set_periodic!(group1::AbstractPhysicalGroup, group2::AbstractPhysicalGroup; dim=2)
919+
set_periodic!(sm, group1, group2, d1=2, d2=2)
920+
921+
Set the model entities in `group1` and `group2` to be periodic. Only supports `d1` = `d2` = 2
922+
and surfaces in both groups need to be parallel and axis-aligned.
923+
"""
924+
function set_periodic!(
925+
sm::SolidModel,
926+
group1::Union{String, Symbol},
927+
group2::Union{String, Symbol},
928+
d1=2,
929+
d2=2
849930
)
850-
return gmsh.model.getBoundary(dimtags(group), combined, oriented, recursive)
931+
if (d1 != 2 || d2 != 2)
932+
@info "set_periodic!(sm, $group1, $group2, $d1, $d2) only supports d1 = d2 = 2."
933+
return Tuple{Int32, Int32}[]
934+
end
935+
return set_periodic!(sm[group1, d1], sm[group2, d2]; dim=d1)
936+
end
937+
938+
function set_periodic!(group1::AbstractPhysicalGroup, group2::AbstractPhysicalGroup; dim=2)
939+
tags1 = [dt[2] for dt in dimtags(group1)]
940+
tags2 = [dt[2] for dt in dimtags(group2)]
941+
942+
bbox1 = get_bounding_box(dim, tags1)
943+
bbox2 = get_bounding_box(dim, tags2)
944+
945+
# Check if surfaces are aligned with x, y, or z axis
946+
plane1 = [isapprox(bbox1[i], bbox1[i + 3], atol=1e-6) for i = 1:3]
947+
plane2 = [isapprox(bbox2[i], bbox2[i + 3], atol=1e-6) for i = 1:3]
948+
949+
# Set periodicity if both surfaces are perpendicular to the same axis
950+
dist = [0.0, 0.0, 0.0]
951+
for i = 1:3
952+
if plane1[i] && plane2[i]
953+
dist[i] = bbox1[i] - bbox2[i]
954+
end
955+
end
956+
if isapprox(sum(abs.(dist)), 0.0) || count(!iszero, dist) > 1
957+
@info "set_periodic! only supports distinct parallel axis-aligned surfaces."
958+
return Tuple{Int32, Int32}[]
959+
end
960+
961+
gmsh.model.mesh.set_periodic(
962+
dim,
963+
tags1,
964+
tags2,
965+
[1, 0, 0, dist[1], 0, 1, 0, dist[2], 0, 0, 1, dist[3], 0, 0, 0, 1]
966+
)
967+
968+
return vcat(dimtags(group1), dimtags(group2))
851969
end
852970

853971
"""

test/test_solidmodel.jl

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,47 @@ import DeviceLayout.SolidModels.STP_UNIT
218218
isapprox.([x0, y0, x1, y1], ustrip.(STP_UNIT, [x0d, y0d, x1d, y1d]), atol=1e-6)
219219
)
220220
sm["test_bdy"] = SolidModels.get_boundary(sm["test", 2])
221+
sm["test_bdy_xmin"] =
222+
SolidModels.get_boundary(sm["test", 2]; direction="X", position="min")
223+
sm["test_bdy_xmax"] =
224+
SolidModels.get_boundary(sm["test", 2]; direction="X", position="max")
225+
sm["test_bdy_ymin"] =
226+
SolidModels.get_boundary(sm["test", 2]; direction="Y", position="min")
227+
sm["test_bdy_ymax"] =
228+
SolidModels.get_boundary(sm["test", 2]; direction="Y", position="max")
229+
sm["test_bdy_zmin"] =
230+
SolidModels.get_boundary(sm["test", 2]; direction="Z", position="min")
231+
sm["test_bdy_zmax"] =
232+
SolidModels.get_boundary(sm["test", 2]; direction="Z", position="max")
233+
234+
@test isempty(
235+
@test_logs (
236+
:info,
237+
"get_boundary(sm, test, 3): (test, 3) is not a physical group, thus has no boundary."
238+
) SolidModels.get_boundary(sm, "test", 3)
239+
)
240+
@test isempty(
241+
@test_logs (
242+
:info,
243+
"get_boundary(sm, Physical Group test of dimension 2 with 4 entities): direction a is not all, X, Y, or Z, thus has no boundary."
244+
) SolidModels.get_boundary(sm["test", 2]; direction="a", position="min")
245+
)
246+
@test isempty(
247+
@test_logs (
248+
:info,
249+
"get_boundary(sm, Physical Group test of dimension 2 with 4 entities): position no is not all, min, or max, thus has no boundary."
250+
) SolidModels.get_boundary(sm["test", 2]; direction="X", position="no")
251+
)
252+
221253
SolidModels.remove_group!(sm, "test", 2; recursive=false)
222254
@test !SolidModels.hasgroup(sm, "test", 2)
223255
@test !isempty(SolidModels.dimtags(sm["test_bdy", 1]))
256+
@test !isempty(SolidModels.dimtags(sm["test_bdy_xmin", 1]))
257+
@test !isempty(SolidModels.dimtags(sm["test_bdy_xmax", 1]))
258+
@test !isempty(SolidModels.dimtags(sm["test_bdy_ymin", 1]))
259+
@test !isempty(SolidModels.dimtags(sm["test_bdy_ymax", 1]))
260+
@test !isempty(SolidModels.dimtags(sm["test_bdy_zmin", 1]))
261+
@test !isempty(SolidModels.dimtags(sm["test_bdy_zmax", 1]))
224262

225263
@test SolidModels.dimtags(get(sm, "foo", 2, sm["test_bdy", 1])) ==
226264
SolidModels.dimtags(sm["test_bdy", 1])
@@ -961,6 +999,38 @@ import DeviceLayout.SolidModels.STP_UNIT
961999
sm = test_sm()
9621000
render!(sm, cs) # runs without error
9631001

1002+
# Use get_boundary, get_bounding_box, and set_periodic!
1003+
cs = CoordinateSystem("test", nm)
1004+
place!(cs, centered(Rectangle(500μm, 100μm)), :l1)
1005+
postrender_ops = [("ext", SolidModels.extrude_z!, (:l1, 20μm))]
1006+
sm = SolidModel("test"; overwrite=true)
1007+
zmap = (m) -> (0μm)
1008+
render!(sm, cs, zmap=zmap, postrender_ops=postrender_ops)
1009+
sm["Xmin"] = SolidModels.get_boundary(sm["ext", 3]; direction="X", position="min")
1010+
sm["Xmax"] = SolidModels.get_boundary(sm["ext", 3]; direction="X", position="max")
1011+
sm["Ymax"] = SolidModels.get_boundary(sm["ext", 3]; direction="Y", position="max")
1012+
@test isempty(
1013+
@test_logs (
1014+
:info,
1015+
"set_periodic!(sm, Xmin, Xmax, 1, 1) only supports d1 = d2 = 2."
1016+
) SolidModels.set_periodic!(sm, "Xmin", "Xmax", 1, 1)
1017+
)
1018+
@test isempty(
1019+
@test_logs (
1020+
:info,
1021+
"set_periodic! only supports distinct parallel axis-aligned surfaces."
1022+
) SolidModels.set_periodic!(sm, "Xmin", "Ymax")
1023+
)
1024+
@test all(
1025+
isapprox.(
1026+
SolidModels.get_bounding_box(2, [3, 5]),
1027+
ustrip.(STP_UNIT, (-250μm, -50μm, 0μm, 250μm, 50μm, 20μm)),
1028+
atol=1e-6
1029+
)
1030+
)
1031+
periodic_tags = SolidModels.set_periodic!(sm["Xmin", 2], sm["Xmax", 2])
1032+
@test !isempty(periodic_tags)
1033+
9641034
# TODO: Composing OptionalStyle
9651035

9661036
# Explicitly MeshSized Path.

0 commit comments

Comments
 (0)