Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ The format of this changelog is based on
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
[Semantic Versioning](https://semver.org/).

## Upcoming

- Improved metadata handling for `LayoutTarget` and `SolidModelTarget`

+ SolidModelTargets will now ignore `NORENDER_META` (the `:norender` layer)
+ SolidModelTargets now take `ignored_layers`, a list of layer symbols which are not rendered
+ LayoutTargets now allow overriding the mapping of `GDSMeta` by setting `target.map_meta_dict[my_gdsmeta] = my_override`, allowing changes to different `GDSMeta` or `nothing` rather than always mapping a `GDSMeta` to itself

- Changed `remove_group!` SolidModel postrendering operation to use `remove_entities=true` by default, fixing the unexpected and undesired default behavior that only removed the record of the group and not its entities

## 1.5.0 (2025-10-10)

- Added `auto_speed`, `endpoints_curvature`, and `auto_curvature` keyword options to `bspline!` and `BSplineRouting`
Expand All @@ -12,6 +22,7 @@ The format of this changelog is based on
+ `endpoints_curvature` sets boundary conditions on the curvature (by inserting extra waypoints)
+ `auto_curvature` B-spline sets curvature at endpoints to match previous segment (or to zero if there is no previous segment)
+ Both `endpoints_speed` and `endpoints_curvature` can be specified as two-element iterables to set the start and end boundary conditions separately

- Added `spec_warnings` keyword option for `save` to allow disabling warnings about cell names violating the GDSII specification (modern tools will accept a broader range of names than strictly allowed by the specification)
- Added `unfold` method for point arrays to help construct polygons with mirror symmetry
- Added FAQ entry about MeshSized/OptionalEntity styling on Paths
Expand Down
23 changes: 23 additions & 0 deletions src/schematics/solidmodels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
levelwise_layers::Vector{Symbol}
indexed_layers::Vector{Symbol}
substrate_layers::Vector{Symbol}
ignored_layers::Vector{Symbol}
rendering_options::NamedTuple
postrenderer
end
Expand All @@ -14,6 +15,18 @@ Contains information about how to render a `Schematic` to a 3D `SolidModel`.
The `technology` contains parameters like layer heights and thicknesses that are used
to position and extrude 2D geometry elements.

# Metadata Mapping

When rendering entities, metadata is mapped to physical group names as follows:

1. If `layer(m) == layer(DeviceLayout.NORENDER_META)` (i.e., `:norender`), the entity is skipped and not rendered to the solid model.
2. If `layer(m)` is in `ignored_layers`, the entity is skipped and not rendered to the solid model.
3. The base name is taken from `layername(m)`.
4. If `layer(m)` is in `levelwise_layers`, `"_L\$(level(m))"` is appended.
5. If `layer(m)` is in `indexed_layers` and `layerindex(m) != 0`, `"_\$(layerindex(m))"` is appended.

# Rendering Options

The `rendering_options` include any keyword arguments to be passed down to the lower-level
`render!(::SolidModel, ::CoordinateSystem; kwargs...)`. The target also includes some
3D-specific options:
Expand All @@ -27,6 +40,7 @@ The `rendering_options` include any keyword arguments to be passed down to the l
- `indexed_layers`: A list of layer `Symbol`s to be turned into separate `PhysicalGroup`s with `"_\$i"` appended for each index `i`. These layers will be automatically indexed if not already present in a `Schematic`'s `index_dict`.
- `substrate_layers`: A list of layer `Symbol`s for layers that are extruded by their
`technology` into the substrate, rather than away from it.
- `ignored_layers`: A list of layer `Symbol`s for layers that should be ignored during rendering (mapped to `nothing`). This provides an alternative to using `NORENDER_META` for layers that should be conditionally ignored in solid model rendering but may be needed for other rendering targets.

The `postrenderer` is a list of geometry kernel commands that create new named groups of
entities from other groups, for example by geometric Boolean operations like intersection.
Expand All @@ -39,6 +53,7 @@ struct SolidModelTarget <: Target
levelwise_layers::Vector{Symbol}
indexed_layers::Vector{Symbol}
substrate_layers::Vector{Symbol}
ignored_layers::Vector{Symbol}
preserved_groups::Vector{Tuple{String, Int}}
rendering_options
postrenderer
Expand All @@ -50,6 +65,7 @@ SolidModelTarget(
levelwise_layers=[],
indexed_layers=[],
substrate_layers=[],
ignored_layers=[],
postrender_ops=[],
preserved_groups=[],
kwargs...
Expand All @@ -59,6 +75,7 @@ SolidModelTarget(
levelwise_layers,
indexed_layers,
substrate_layers,
ignored_layers,
preserved_groups,
(; solidmodel=true, kwargs...),
postrender_ops
Expand Down Expand Up @@ -128,6 +145,12 @@ function _map_meta_fn(target::SolidModelTarget)
# By default, target maps a layer to layername (string)
# Append -L$(level) and/or _$(index) if appropriate
return m -> begin
# Skip rendering if this is the NORENDER_META layer
(layer(m) == layer(DeviceLayout.NORENDER_META)) && return nothing

# Skip rendering if layer is in ignored_layers
(layer(m) in target.ignored_layers) && return nothing

name = layername(m)
if layer(m) in levelwise_layers(target)
name = name * "_L$(level(m))"
Expand Down
10 changes: 6 additions & 4 deletions src/schematics/targets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ function map_layer(target::Target, meta::DeviceLayout.Meta)
target.map_meta_dict[meta] = res
return res
end
map_layer(::Target, m::GDSMeta) = m # No need to warn

function _map_layer(target, meta)
# For GDSMeta, return as-is (pass through)
_map_layer(::Target, meta::GDSMeta) = meta

function _map_layer(target::Target, meta)
(layer(meta) == layer(DeviceLayout.NORENDER_META)) && return nothing
!(level(meta) in target.levels) && return nothing
if !haskey(layer_record(target.technology), layer(meta))
Expand Down Expand Up @@ -213,8 +215,8 @@ that determine how or whether entities with an `OptionalStyle` with the correspo

When rendering `ent::GeometryEntity` with `target::LayoutTarget`, its metadata `m` is handled as follows:

0. If `m` is already a `GDSMeta`, use as is.
1. If `target.map_meta_dict[m]` exists (as a `GDSMeta` instance or `nothing`), use that. This can be manually assigned before rendering, overriding the default that would result from the steps below. If it does not yet exist, then the result of the steps below will be stored in `target.map_meta_dict[m]`.
0. If `target.map_meta_dict[m]` exists (as a `GDSMeta` instance or `nothing`), use that. This can be manually assigned before rendering, overriding the default behavior for any metadata type, including `GDSMeta`. Otherwise, the result of the steps below will be stored in `target.map_meta_dict[m]`.
1. If `m` is already a `GDSMeta` and not in `map_meta_dict`, use as is.
2. If `layer(m) == layer(DeviceLayout.NORENDER_META)` (that is, `:norender`), use `nothing`.
3. If `!(level(m) in target.levels)`, use `nothing`.
4. If `layer(m)` is not present as a key in `layer_record(target.technology)` and is not of the form `:GDS<layer>_<datatype>`, then emit a warning and use `GDSMeta(0,0)`, ignoring level and layer index.
Expand Down
34 changes: 20 additions & 14 deletions src/solidmodels/postrender.jl
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,16 @@ function union_geom!(

# Actual entities were deleted as part of the operation, just empty the groups.
if remove_object
remove_group!.(getindex.(sm, valid_object, d1))
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
end
if remove_tool
remove_group!.(
getindex.(
sm,
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
d2
)
),
remove_entities=false
)
end
return dt
Expand Down Expand Up @@ -485,15 +486,16 @@ function intersect_geom!(

# Actual entities were deleted as part of the operation, just empty the groups.
if remove_object
remove_group!.(getindex.(sm, valid_object, d1))
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
end
if remove_tool
remove_group!.(
getindex.(
sm,
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
d2
)
),
remove_entities=false
)
end
return dt
Expand Down Expand Up @@ -650,15 +652,16 @@ function difference_geom!(

# Actual entities were deleted as part of the operation, just empty the groups.
if remove_object
remove_group!.(getindex.(sm, valid_object, d1))
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
end
if remove_tool
remove_group!.(
getindex.(
sm,
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
d2
)
),
remove_entities=false
)
end
return dt
Expand Down Expand Up @@ -794,15 +797,16 @@ function fragment_geom!(

# Actual entities were deleted as part of the operation, just empty the groups.
if remove_object
remove_group!.(getindex.(sm, valid_object, d1))
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
end
if remove_tool
remove_group!.(
getindex.(
sm,
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
d2
)
),
remove_entities=false
)
end
return dt
Expand Down Expand Up @@ -953,21 +957,23 @@ function set_periodic!(group1::AbstractPhysicalGroup, group2::AbstractPhysicalGr
end

"""
remove_group!(sm::SolidModel, group::Union{String, Symbol}, dim; recursive=true, remove_entities=false)
remove_group!(group::AbstractPhysicalGroup; recursive=true, remove_entities=false)
remove_group!(sm::SolidModel, group::Union{String, Symbol}, dim; recursive=true, remove_entities=true)
remove_group!(group::AbstractPhysicalGroup; recursive=true, remove_entities=true)

Remove entities in `group` from the model, unless they are boundaries of higher-dimensional entities.
Remove entities in `group` from the model, unless they are boundaries of higher-dimensional entities or part of another physical group.

If `recursive` is true, remove all entities on their boundaries, down to dimension zero (points).

Also removes the (now-empty) physical group.
Also removes the record of the (now-empty) physical group.

If `remove_entities` is false, only removes the record of the group from the model.
"""
function remove_group!(
sm::SolidModel,
group::Union{String, Symbol},
dim;
recursive=true,
remove_entities=false
remove_entities=true
)
if !hasgroup(sm, group, dim)
@info "remove_group!(sm, $group, $dim; recursive=$recursive, remove_entities=$remove_entities): ($group, $dim) is not a physical group."
Expand All @@ -983,7 +989,7 @@ end
remove_group!(sm::SolidModel, group, dim; kwargs...) =
remove_group!.(sm, group, dim; kwargs...)

function remove_group!(group::PhysicalGroup; recursive=true, remove_entities=false)
function remove_group!(group::PhysicalGroup; recursive=true, remove_entities=true)
if remove_entities
kernel(group).remove(dimtags(group), recursive)
end
Expand Down
22 changes: 21 additions & 1 deletion test/test_schematic_solidmodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ function test_component(component, clip_area, mesh=false, gui=false)
SemanticMeta(:circle)
)

# Add a NORENDER_META element that should be skipped in solid model rendering
render!(
floorplan.coordinate_system,
Rectangle(2μm, 2μm) + Point(50μm, 50μm),
DeviceLayout.NORENDER_META
)
# Also an element in a layer that will be designated as ignored by the target
render!(
floorplan.coordinate_system,
Rectangle(2μm, 2μm) + Point(50μm, 50μm),
SemanticMeta(:ignored)
)

check!(floorplan)
build!(floorplan)

Expand All @@ -160,6 +173,7 @@ function test_component(component, clip_area, mesh=false, gui=false)
substrate_layers=[:chip_outline],
levelwise_layers=[:chip_outline],
indexed_layers=[:port],
ignored_layers=[:ignored],
postrender_ops=[
(
"substrates",
Expand All @@ -181,7 +195,9 @@ function test_component(component, clip_area, mesh=false, gui=false)
("substrates", 3),
("vacuum", 3),
("base_metal", 2),
("circle", 2)
("circle", 2),
("norender", 2),
("ignored", 2)
],
solidmodel=true,
simulation=true
Expand All @@ -202,6 +218,10 @@ function test_component(component, clip_area, mesh=false, gui=false)
@test !SolidModels.hasgroup(sm, "base_negative", 2)
@test !SolidModels.hasgroup(sm, "circle", 2)

# Verify that NORENDER_META element is not present (should be skipped even though it's retained)
@test !SolidModels.hasgroup(sm, "norender", 2)
@test !SolidModels.hasgroup(sm, "ignored", 2) # Same for `ignored_layers=[:ignored]`

# The physical groups should be reindexed in decreasing order of dimension, and then
# alphabetically
@test sm["substrates", 3].grouptag == 1
Expand Down
8 changes: 8 additions & 0 deletions test/test_schematicdriven.jl
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,14 @@ end
[GDSMeta(), GDSMeta(300), GDSMeta(302, 4), GDSMeta(), GDSMeta(2, 2)]
@test SchematicDrivenLayout.map_layer(ArtworkTarget(tech), SemanticMeta(:GDS2)) ==
GDSMeta(2)

# Manual map_meta_dict override
target = ArtworkTarget(tech; levels=[1])
target.map_meta_dict[meta] = nothing
target.map_meta_dict[GDSMeta(2, 2)] = GDSMeta(3, 3)
cell = Cell("test", nm)
render!(cell, cs, target) # undef_meta, GDSMeta(2,2)
@test cell.element_metadata == [GDSMeta(), GDSMeta(3, 3)]
end

@variant TestCompVariant TestComponent new_defaults =
Expand Down
4 changes: 2 additions & 2 deletions test/test_solidmodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ import DeviceLayout.SolidModels.STP_UNIT
) SolidModels.get_boundary(sm["test", 2]; direction="X", position="no")
)

SolidModels.remove_group!(sm, "test", 2; recursive=false)
SolidModels.remove_group!(sm, "test", 2; recursive=false, remove_entities=false)
@test !SolidModels.hasgroup(sm, "test", 2)
@test !isempty(SolidModels.dimtags(sm["test_bdy", 1]))
@test !isempty(SolidModels.dimtags(sm["test_bdy_xmin", 1]))
Expand All @@ -276,7 +276,7 @@ import DeviceLayout.SolidModels.STP_UNIT
@test_logs (
:info,
"remove_group!(sm, foo, 3; recursive=true, remove_entities=false): (foo, 3) is not a physical group."
) SolidModels.remove_group!(sm, "foo", 3)
) SolidModels.remove_group!(sm, "foo", 3; remove_entities=false)
)
@test isempty(
@test_logs (
Expand Down