Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ The format of this changelog is based on
rendering and meshing thereby improving user experience.
- Deprecate `SolidModels.MeshingParameters` in favour of new `mesh_scale`, `mesh_order`,
`mesh_grading_default` accessed from `SolidModels`.
- Improvements to `SolidModels.render!` to improve stability and performance.
+ Change `SolidModels.restrict_to_volume!` to perform a check if the simulation domain
already bounds all two and three dimensional objects, if so skips operation.
+ Change `SolidModels.render!` to incorporate a two stage `_fragment_and_map!` operation,
reconciling vertices and segments before reconciling all entities. This improves the
robustness of the OpenCascade integration which can error in synchronization if too much
reconciliation is required all at once by `fragment`.
+ These two operations in conjunction with the removal of `MeshSized` entities results in
a ~3x performance improvement in rendering the QPU17 example to `SolidModel`, and ~4.5x
reduction in time from schematic to mesh.

## 1.7.0 (2025-11-26)

Expand Down
38 changes: 34 additions & 4 deletions src/schematics/ExamplePDK/ExamplePDK.jl
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ const SINGLECHIP_SOLIDMODEL_TARGET = SolidModelTarget(
indexed_layers=[:port, :lumped_element, :integration, :wave_port], # Automatically index these layers
wave_port_layers=[:wave_port], # WAVE_PORT are 1D line segments in x-y to be extruded in z
postrender_ops=[ # Manual definition of operations to run after 2D rendering
( # Unify metal negative before removing from writeable_area
"metal_negative", # Output group name
SolidModels.union_geom!, # Operation
("metal_negative", "metal_negative", 2, 2), # (object, tool, object_dim, tool_dim)
:remove_object => true # Remove "metal_negative" entities after operation
),
( # Get metal ground plane by subtracting negative from writeable area
"metal", # Output group name
SolidModels.difference_geom!, # Operation
Expand Down Expand Up @@ -205,7 +211,12 @@ const SINGLECHIP_SOLIDMODEL_TARGET = SolidModelTarget(
:remove_object => true,
:remove_tool => true
),
(("metal", SolidModels.difference_geom!, ("metal", "port")))
((
"metal",
SolidModels.difference_geom!,
("metal", "port"),
:remove_object => true
))
],
# We only want to retain physical groups that we will need for specifying boundary
# conditions in the physical domain.
Expand All @@ -227,7 +238,12 @@ function singlechip_solidmodel_target(boundary_groups...)
target = deepcopy(SINGLECHIP_SOLIDMODEL_TARGET)
push!(
target.postrenderer,
("metal", SolidModels.difference_geom!, ("metal", [boundary_groups...]))
(
"metal",
SolidModels.difference_geom!,
("metal", [boundary_groups...]),
:remove_object => true
)
)
retained_physical_groups = [(x, 2) for x ∈ boundary_groups]
append!(target.rendering_options.retained_physical_groups, retained_physical_groups)
Expand Down Expand Up @@ -261,6 +277,12 @@ const FLIPCHIP_SOLIDMODEL_TARGET = SolidModelTarget(
indexed_layers=[:port, :lumped_element, :integration, :wave_port],
wave_port_layers=[:wave_port],
postrender_ops=[
( # Reconcile L1 negative
"metal_negative_L1",
SolidModels.union_geom!,
("metal_negative_L1", "metal_negative_L1", 2, 2),
:remove_object => true
),
( # Get metal ground plane by subtracting negative from writeable area
"metal_L1",
SolidModels.difference_geom!,
Expand All @@ -273,6 +295,12 @@ const FLIPCHIP_SOLIDMODEL_TARGET = SolidModelTarget(
("metal_L1", "metal_positive_L1", 2, 2),
:remove_tool => true
),
( # Reconcile L2 negative
"metal_negative_L2",
SolidModels.union_geom!,
("metal_negative_L2", "metal_negative_L2", 2, 2),
:remove_object => true
),
( # Same thing on L2
"metal_L2",
SolidModels.difference_geom!,
Expand Down Expand Up @@ -314,9 +342,11 @@ const FLIPCHIP_SOLIDMODEL_TARGET = SolidModelTarget(
( # Union of all physical metal
"metal",
SolidModels.union_geom!,
(["metal_L1", "metal_L2", "bridge_metal", "bump_surface"], 2)
(["metal_L1", "metal_L2", "bridge_metal", "bump_surface"], 2),
:remove_object => true,
:remove_tool => true
),
(("metal", SolidModels.difference_geom!, ("metal", "port")))
("metal", SolidModels.difference_geom!, ("metal", "port"), :remove_object => true)
],
# We only want to retain physical groups that we will need for specifying boundary
# conditions in the physical domain.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ function example_launcher(port_spec)
launch!(path, extround=0μm)
port_cs = CoordinateSystem(uniquename("launcherport"))
gap0 = path[1].sty.gap # Launcher pad gap
render!(port_cs, only_simulated(centered(Rectangle(gap0, gap0))), PORT)
render!(
port_cs,
only_simulated(meshsized_entity(centered(Rectangle(gap0, gap0)), gap0 / 2)),
PORT
)
attach!(path, sref(port_cs), path[1].sty.gap / 2, i=1)
return path
end
Expand Down
6 changes: 1 addition & 5 deletions src/schematics/solidmodels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,7 @@ function intersection_ops(t::SolidModelTarget, sch::Schematic)
]
end
return [
("rendered_volume", SolidModels.union_geom!, (bv[1], bv[2], 3, 3)),
[
("rendered_volume", SolidModels.union_geom!, ("rendered_volume", bv[i], 3, 3))
for i = 3:length(bv)
]...,
("rendered_volume", SolidModels.union_geom!, bv, 3),
("rendered_volume", SolidModels.restrict_to_volume!, ("rendered_volume",)),
("exterior_boundary", SolidModels.get_boundary, ("rendered_volume", 3)),
[
Expand Down
25 changes: 21 additions & 4 deletions src/solidmodels/postrender.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
function _postrender!(sm::SolidModel, operations)
# Operations
for (destination, op, args, kwargs...) in operations
dimtags = op(sm, args...; kwargs...)
sm[destination] = dimtags
sm[destination] = op(sm, args...; kwargs...)
end
end

Expand Down Expand Up @@ -504,7 +503,9 @@ end
"""
restrict_to_volume(sm::SolidModel, volume)

Replace all entities and groups with their intersection with `sm[volume, 3]`.
Checks if all surfaces and volumes are contained within `sm[volume, 3]`, and if not performs
an intersection operation replacing all entities and groups with their intersection with
`sm[volume, 3]`.

Embeds entities if they are on the boundary of higher-dimensional entities and removes
duplicate entities.
Expand All @@ -513,7 +514,23 @@ Preserves the meaning of existing groups by assigning to them the (possibly new)
corresponding to that group's intersection with the volume.
"""
function restrict_to_volume!(sm::SolidModel, volume)
dims = [3, 2, 1, 0]

# Check if the subtraction of the bounding volume from all surfaces and volumes is the
# empty set.
dims = SVector(3, 2, 1)
groups =
[(name, dimtags(pg)) for dim in dims for (name, pg) in pairs(dimgroupdict(sm, dim))]
allents = vcat([gmsh.model.get_entities(dim) for dim in dims]...)

out_dim_tags, _ = kernel(sm).cut(allents, dimtags(sm[volume, 3]), -1, false, false)
isempty(out_dim_tags) && return dimtags(sm[volume, 3])

# There were entities found after cutting, the restricting volume is a subset of the
# rendered geometry, will need to perform the intersection.
kernel(sm).remove(out_dim_tags, true)
_synchronize!(sm)

dims = SVector(3, 2, 1, 0)
groups =
[(name, dimtags(pg)) for dim in dims for (name, pg) in pairs(dimgroupdict(sm, dim))]
allents = vcat([gmsh.model.get_entities(dim) for dim in dims]...)
Expand Down
57 changes: 33 additions & 24 deletions src/solidmodels/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,12 @@ meshsize(::Paths.Segment, sty::Paths.TaperTrace; kwargs...) =
2 * max(sty.width_start, sty.width_end)
meshsize(::Paths.Segment, sty::Paths.TaperCPW; kwargs...) =
2 * max(sty.trace_start, sty.trace_end, sty.gap_start, sty.gap_end)
meshsize(::Paths.Segment, sty::Paths.TraceTermination; kwargs...) = 2 * sty.width
meshsize(::Paths.Segment, sty::Paths.CPWOpenTermination; kwargs...) =
2 * max(sty.trace, sty.gap)
meshsize(::Paths.Segment, sty::Paths.CPWShortTermination; kwargs...) =
2 * max(sty.trace, sty.gap)

# For GeneralCPW and GeneralTrace, just sample.
function meshsize(seg::Paths.Segment, sty::Paths.GeneralTrace; kwargs...)
l = pathlength(seg)
Expand Down Expand Up @@ -458,6 +464,7 @@ Set gmsh configuration options.

- `set_gmsh_option(option_name, value)`: Set a single option to a numeric or string value
- `set_gmsh_option(option_name, dict, default)`: Set option from dict with fallback to default
- `set_gmsh_option(option_name, dict)`: Set option from dict if `option_name` is present
- `set_gmsh_option(dict)`: Set multiple options from a dictionary

# Arguments
Expand All @@ -472,6 +479,7 @@ Set gmsh configuration options.
```julia
set_gmsh_option("Mesh.Algorithm", 6)
set_gmsh_option("General.FileName", "output.msh")
set_gmsh_option("General.FileName", Dict("Mesh.Algorithm" => 6)) # does nothing
set_gmsh_option(Dict("Mesh.Algorithm" => 6, "General.NumThreads" => 4))
```
"""
Expand All @@ -480,6 +488,9 @@ set_gmsh_option(s, o::AbstractString) = SolidModels.gmsh.option.set_string(s, o)
function set_gmsh_option(s, d::Dict, default)
return set_gmsh_option(s, get(d, s, default))
end
function set_gmsh_option(s, d::Dict)
return haskey(d, s) && set_gmsh_option(s, d[s])
end
function set_gmsh_option(d::Dict)
for (k, v) in d
set_gmsh_option(k, v)
Expand Down Expand Up @@ -558,7 +569,8 @@ mesh_grading_default() = MESHSIZE_PARAMS[:global_α]::Float64
function mesh_grading_default(α)
@assert 0 < α <= 1
MESHSIZE_PARAMS[:global_α]::Float64 = α
return finalize_size_fields!()
finalize_size_fields!()
return MESHSIZE_PARAMS[:global_α]
end

"""
Expand Down Expand Up @@ -636,7 +648,8 @@ data.
See [`DeviceLayout.MeshSized`](@ref) for details and the explicit mesh sizing formula
utilizing the control points.
"""
mesh_control_points() = MESHSIZE_PARAMS[:cp]
mesh_control_points() =
MESHSIZE_PARAMS[:cp]::Dict{Tuple{Float64, Float64}, Vector{SVector{3, Float64}}}

"""
mesh_control_trees()
Expand All @@ -649,7 +662,10 @@ tuples and values are KDTrees for fast nearest-neighbor lookups.
See [`DeviceLayout.MeshSized`](@ref) for details and the explicit mesh sizing formula
computed using the control trees.
"""
mesh_control_trees() = MESHSIZE_PARAMS[:ct]
mesh_control_trees() = MESHSIZE_PARAMS[:ct]::Dict{
Tuple{Float64, Float64},
KDTree{SVector{3, Float64}, Euclidean, Float64, SVector{3, Float64}}
}

"""
clear_mesh_control_points!()
Expand Down Expand Up @@ -738,7 +754,8 @@ Base.@kwdef struct MeshingParameters
end

"""
render!(sm::SolidModel, cs::AbstractCoordinateSystem{T}; map_meta=layer, postrender_ops=[], zmap=(_) -> zero(T), kwargs...) where {T}
render!(sm::SolidModel, cs::AbstractCoordinateSystem{T}; map_meta=layer,
postrender_ops=[], zmap=(_) -> zero(T), gmsh_options = Dict(), skip_postrender = false, kwargs...) where {T}

Render `cs` to `sm`.

Expand All @@ -765,6 +782,9 @@ Render `cs` to `sm`.
- `meshing_parameters`: **Deprecated.** Use individual mesh control functions
[`DeviceLayout.SolidModels.mesh_scale`](@ref), [`DeviceLayout.SolidModels.mesh_order`](@ref) and [`DeviceLayout.SolidModels.mesh_grading_default`](@ref), along with
`gmsh_options` instead.
- `skip_postrender`: Whether or not to return early without performing any postrendering
operations. This can be particularly helpful during debugging, as all two dimensional
entities will be placed appropriately but will not have been combined.

Available postrendering operations include [`translate!`](@ref), [`extrude_z!`](@ref), [`revolve!`](@ref),
[`union_geom!`](@ref), [`intersect_geom!`](@ref), [`difference_geom!`](@ref), [`fragment_geom!`](@ref), and [`box_selection`](@ref).
Expand All @@ -783,11 +803,11 @@ function render!(
zmap=(_) -> zero(T),
gmsh_options=Dict{String, Union{String, Int, Float64}}(),
meshing_parameters::Union{Nothing, MeshingParameters}=nothing,
skip_postrender=false,
kwargs...
) where {T}
gmsh.model.set_current(name(sm))

# Check for meshing_parameters, if
if !isnothing(meshing_parameters)
Base.depwarn("Using `MeshingParameters` is deprecated!", :render!, force=true)
mesh_scale(meshing_parameters.mesh_scale)
Expand Down Expand Up @@ -905,31 +925,21 @@ function render!(

# Generate the KDTrees corresponding to the meshing control points.
finalize_size_fields!()

# Extrusions, Booleans, etc
_synchronize!(sm)
skip_postrender && return nothing
_postrender!(sm, postrender_ops)
# Get rid of redundant entities and update groups accordingly
# Get rid of redundant entities and update groups accordingly.
# The first [1,0] call improves robustness of the next fragment significantly.
# Additionally, the decreasing order of dimensions is important, as OCC can error
# sometimes if the higher dimensional entities are not reconciled first.
_synchronize!(sm)
_fragment_and_map!(sm)
_fragment_and_map!(sm, [1, 0]) # Important!
_fragment_and_map!(sm, [3, 2, 1])

# Pass in call back function for meshing against the vertices found previously.
gmsh.model.mesh.setSizeCallback(gmsh_meshsize)

set_gmsh_option("Mesh.MeshSizeFromPoints", gmsh_options, 0)
set_gmsh_option("Mesh.MeshSizeFromCurvature", gmsh_options, 0)
set_gmsh_option("Mesh.MeshSizeExtendFromBoundary", gmsh_options, 0)
set_gmsh_option("Mesh.Algorithm", gmsh_options, 6)
set_gmsh_option("Mesh.Algorithm3D", gmsh_options, 1)

# Default to no threads, as there appear to be race conditions within gmsh.
# If set to zero, Gmsh will look for OMP_NUM_THREADS environment variables;
# this needs to be >1 for HXT algorithm to use parallelism.
set_gmsh_option("General.NumThreads", gmsh_options, 1)

# Always save meshes in binary for faster disk I/O
set_gmsh_option("Mesh.Binary", gmsh_options, 1)

# Remove all physical groups except those on the retained list.
if !isempty(retained_physical_groups)
for d ∈ 0:3
Expand Down Expand Up @@ -986,7 +996,6 @@ function gmsh_meshsize(
lc::Cdouble
)
l = Inf64
# Explicit type tag here to remove hypothetical type instability.
for ((h, α), tree) in mesh_control_trees()
_, d::Float64 = nn(tree, SVector{3}(x, y, z))
l = min(l, h * max(mesh_scale(), (d / h)^α))::Float64
Expand All @@ -1000,7 +1009,7 @@ end
# excluded_physical_groups are physical groups not to be included in the fragmentation.
function _fragment_and_map!(
sm::SolidModel,
frag_dims=[0, 2, 3];
frag_dims=[3, 2, 1, 0];
excluded_physical_groups=PhysicalGroup[]
)
gmsh.model.set_current(name(sm))
Expand Down
18 changes: 16 additions & 2 deletions src/solidmodels/solidmodels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,23 @@ struct SolidModel{T <: SolidModelKernel}
)
iszero(gmsh.is_initialized()) && gmsh.initialize()
# SolidModel initiated gmsh uses μm
gmsh.option.set_string("Geometry.OCCTargetUnit", "UM")
set_gmsh_option("Geometry.OCCTargetUnit", "UM")
# Use threads in open cascade
gmsh.option.set_number("Geometry.OCCParallel", 1)
set_gmsh_option("Geometry.OCCParallel", 1)
# Default to no threads, as there appear to be race conditions within gmsh.
# If set to zero, Gmsh will look for OMP_NUM_THREADS environment variables;
# this needs to be >1 for HXT algorithm to use parallelism.
set_gmsh_option("General.NumThreads", 1)

# Reasonable defaults for meshing.
set_gmsh_option("Mesh.MeshSizeFromPoints", 0)
set_gmsh_option("Mesh.MeshSizeFromCurvature", 0)
set_gmsh_option("Mesh.MeshSizeExtendFromBoundary", 0)
set_gmsh_option("Mesh.Algorithm", 6)
set_gmsh_option("Mesh.Algorithm3D", 1)

# Always save meshes in binary for faster disk I/O
set_gmsh_option("Mesh.Binary", 1)

# If a model with this name exists, throw error or delete it
names = gmsh.model.list()
Expand Down
Loading