Skip to content

Commit fde4846

Browse files
authored
Metadata mapping fixes/improvements (#95)
* fix: Make SolidModel ignore NORENDER_META * Allow GDSMeta mapping to be manually overridden in map_meta_dict * Allow specification of ignored_layers in SolidModelTarget * Remove entities by default in remove_group! * Update CHANGELOG.md * Add type annotation to _map_layer
1 parent 41a640e commit fde4846

File tree

7 files changed

+91
-21
lines changed

7 files changed

+91
-21
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ 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+
- Improved metadata handling for `LayoutTarget` and `SolidModelTarget`
10+
11+
+ SolidModelTargets will now ignore `NORENDER_META` (the `:norender` layer)
12+
+ SolidModelTargets now take `ignored_layers`, a list of layer symbols which are not rendered
13+
+ 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
14+
15+
- 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
16+
717
## 1.5.0 (2025-10-10)
818

919
- Added `auto_speed`, `endpoints_curvature`, and `auto_curvature` keyword options to `bspline!` and `BSplineRouting`
@@ -12,6 +22,7 @@ The format of this changelog is based on
1222
+ `endpoints_curvature` sets boundary conditions on the curvature (by inserting extra waypoints)
1323
+ `auto_curvature` B-spline sets curvature at endpoints to match previous segment (or to zero if there is no previous segment)
1424
+ Both `endpoints_speed` and `endpoints_curvature` can be specified as two-element iterables to set the start and end boundary conditions separately
25+
1526
- 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)
1627
- Added `unfold` method for point arrays to help construct polygons with mirror symmetry
1728
- Added FAQ entry about MeshSized/OptionalEntity styling on Paths

src/schematics/solidmodels.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
levelwise_layers::Vector{Symbol}
66
indexed_layers::Vector{Symbol}
77
substrate_layers::Vector{Symbol}
8+
ignored_layers::Vector{Symbol}
89
rendering_options::NamedTuple
910
postrenderer
1011
end
@@ -14,6 +15,18 @@ Contains information about how to render a `Schematic` to a 3D `SolidModel`.
1415
The `technology` contains parameters like layer heights and thicknesses that are used
1516
to position and extrude 2D geometry elements.
1617
18+
# Metadata Mapping
19+
20+
When rendering entities, metadata is mapped to physical group names as follows:
21+
22+
1. If `layer(m) == layer(DeviceLayout.NORENDER_META)` (i.e., `:norender`), the entity is skipped and not rendered to the solid model.
23+
2. If `layer(m)` is in `ignored_layers`, the entity is skipped and not rendered to the solid model.
24+
3. The base name is taken from `layername(m)`.
25+
4. If `layer(m)` is in `levelwise_layers`, `"_L\$(level(m))"` is appended.
26+
5. If `layer(m)` is in `indexed_layers` and `layerindex(m) != 0`, `"_\$(layerindex(m))"` is appended.
27+
28+
# Rendering Options
29+
1730
The `rendering_options` include any keyword arguments to be passed down to the lower-level
1831
`render!(::SolidModel, ::CoordinateSystem; kwargs...)`. The target also includes some
1932
3D-specific options:
@@ -27,6 +40,7 @@ The `rendering_options` include any keyword arguments to be passed down to the l
2740
- `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`.
2841
- `substrate_layers`: A list of layer `Symbol`s for layers that are extruded by their
2942
`technology` into the substrate, rather than away from it.
43+
- `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.
3044
3145
The `postrenderer` is a list of geometry kernel commands that create new named groups of
3246
entities from other groups, for example by geometric Boolean operations like intersection.
@@ -39,6 +53,7 @@ struct SolidModelTarget <: Target
3953
levelwise_layers::Vector{Symbol}
4054
indexed_layers::Vector{Symbol}
4155
substrate_layers::Vector{Symbol}
56+
ignored_layers::Vector{Symbol}
4257
preserved_groups::Vector{Tuple{String, Int}}
4358
rendering_options
4459
postrenderer
@@ -50,6 +65,7 @@ SolidModelTarget(
5065
levelwise_layers=[],
5166
indexed_layers=[],
5267
substrate_layers=[],
68+
ignored_layers=[],
5369
postrender_ops=[],
5470
preserved_groups=[],
5571
kwargs...
@@ -59,6 +75,7 @@ SolidModelTarget(
5975
levelwise_layers,
6076
indexed_layers,
6177
substrate_layers,
78+
ignored_layers,
6279
preserved_groups,
6380
(; solidmodel=true, kwargs...),
6481
postrender_ops
@@ -128,6 +145,12 @@ function _map_meta_fn(target::SolidModelTarget)
128145
# By default, target maps a layer to layername (string)
129146
# Append -L$(level) and/or _$(index) if appropriate
130147
return m -> begin
148+
# Skip rendering if this is the NORENDER_META layer
149+
(layer(m) == layer(DeviceLayout.NORENDER_META)) && return nothing
150+
151+
# Skip rendering if layer is in ignored_layers
152+
(layer(m) in target.ignored_layers) && return nothing
153+
131154
name = layername(m)
132155
if layer(m) in levelwise_layers(target)
133156
name = name * "_L$(level(m))"

src/schematics/targets.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,11 @@ function map_layer(target::Target, meta::DeviceLayout.Meta)
136136
target.map_meta_dict[meta] = res
137137
return res
138138
end
139-
map_layer(::Target, m::GDSMeta) = m # No need to warn
140139

141-
function _map_layer(target, meta)
140+
# For GDSMeta, return as-is (pass through)
141+
_map_layer(::Target, meta::GDSMeta) = meta
142+
143+
function _map_layer(target::Target, meta)
142144
(layer(meta) == layer(DeviceLayout.NORENDER_META)) && return nothing
143145
!(level(meta) in target.levels) && return nothing
144146
if !haskey(layer_record(target.technology), layer(meta))
@@ -213,8 +215,8 @@ that determine how or whether entities with an `OptionalStyle` with the correspo
213215
214216
When rendering `ent::GeometryEntity` with `target::LayoutTarget`, its metadata `m` is handled as follows:
215217
216-
0. If `m` is already a `GDSMeta`, use as is.
217-
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]`.
218+
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]`.
219+
1. If `m` is already a `GDSMeta` and not in `map_meta_dict`, use as is.
218220
2. If `layer(m) == layer(DeviceLayout.NORENDER_META)` (that is, `:norender`), use `nothing`.
219221
3. If `!(level(m) in target.levels)`, use `nothing`.
220222
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.

src/solidmodels/postrender.jl

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -357,15 +357,16 @@ function union_geom!(
357357

358358
# Actual entities were deleted as part of the operation, just empty the groups.
359359
if remove_object
360-
remove_group!.(getindex.(sm, valid_object, d1))
360+
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
361361
end
362362
if remove_tool
363363
remove_group!.(
364364
getindex.(
365365
sm,
366366
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
367367
d2
368-
)
368+
),
369+
remove_entities=false
369370
)
370371
end
371372
return dt
@@ -485,15 +486,16 @@ function intersect_geom!(
485486

486487
# Actual entities were deleted as part of the operation, just empty the groups.
487488
if remove_object
488-
remove_group!.(getindex.(sm, valid_object, d1))
489+
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
489490
end
490491
if remove_tool
491492
remove_group!.(
492493
getindex.(
493494
sm,
494495
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
495496
d2
496-
)
497+
),
498+
remove_entities=false
497499
)
498500
end
499501
return dt
@@ -650,15 +652,16 @@ function difference_geom!(
650652

651653
# Actual entities were deleted as part of the operation, just empty the groups.
652654
if remove_object
653-
remove_group!.(getindex.(sm, valid_object, d1))
655+
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
654656
end
655657
if remove_tool
656658
remove_group!.(
657659
getindex.(
658660
sm,
659661
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
660662
d2
661-
)
663+
),
664+
remove_entities=false
662665
)
663666
end
664667
return dt
@@ -794,15 +797,16 @@ function fragment_geom!(
794797

795798
# Actual entities were deleted as part of the operation, just empty the groups.
796799
if remove_object
797-
remove_group!.(getindex.(sm, valid_object, d1))
800+
remove_group!.(getindex.(sm, valid_object, d1), remove_entities=false)
798801
end
799802
if remove_tool
800803
remove_group!.(
801804
getindex.(
802805
sm,
803806
remove_object ? setdiff(valid_tool, valid_object) : valid_tool,
804807
d2
805-
)
808+
),
809+
remove_entities=false
806810
)
807811
end
808812
return dt
@@ -953,21 +957,23 @@ function set_periodic!(group1::AbstractPhysicalGroup, group2::AbstractPhysicalGr
953957
end
954958

955959
"""
956-
remove_group!(sm::SolidModel, group::Union{String, Symbol}, dim; recursive=true, remove_entities=false)
957-
remove_group!(group::AbstractPhysicalGroup; recursive=true, remove_entities=false)
960+
remove_group!(sm::SolidModel, group::Union{String, Symbol}, dim; recursive=true, remove_entities=true)
961+
remove_group!(group::AbstractPhysicalGroup; recursive=true, remove_entities=true)
958962
959-
Remove entities in `group` from the model, unless they are boundaries of higher-dimensional entities.
963+
Remove entities in `group` from the model, unless they are boundaries of higher-dimensional entities or part of another physical group.
960964
961965
If `recursive` is true, remove all entities on their boundaries, down to dimension zero (points).
962966
963-
Also removes the (now-empty) physical group.
967+
Also removes the record of the (now-empty) physical group.
968+
969+
If `remove_entities` is false, only removes the record of the group from the model.
964970
"""
965971
function remove_group!(
966972
sm::SolidModel,
967973
group::Union{String, Symbol},
968974
dim;
969975
recursive=true,
970-
remove_entities=false
976+
remove_entities=true
971977
)
972978
if !hasgroup(sm, group, dim)
973979
@info "remove_group!(sm, $group, $dim; recursive=$recursive, remove_entities=$remove_entities): ($group, $dim) is not a physical group."
@@ -983,7 +989,7 @@ end
983989
remove_group!(sm::SolidModel, group, dim; kwargs...) =
984990
remove_group!.(sm, group, dim; kwargs...)
985991

986-
function remove_group!(group::PhysicalGroup; recursive=true, remove_entities=false)
992+
function remove_group!(group::PhysicalGroup; recursive=true, remove_entities=true)
987993
if remove_entities
988994
kernel(group).remove(dimtags(group), recursive)
989995
end

test/test_schematic_solidmodel.jl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ function test_component(component, clip_area, mesh=false, gui=false)
136136
SemanticMeta(:circle)
137137
)
138138

139+
# Add a NORENDER_META element that should be skipped in solid model rendering
140+
render!(
141+
floorplan.coordinate_system,
142+
Rectangle(2μm, 2μm) + Point(50μm, 50μm),
143+
DeviceLayout.NORENDER_META
144+
)
145+
# Also an element in a layer that will be designated as ignored by the target
146+
render!(
147+
floorplan.coordinate_system,
148+
Rectangle(2μm, 2μm) + Point(50μm, 50μm),
149+
SemanticMeta(:ignored)
150+
)
151+
139152
check!(floorplan)
140153
build!(floorplan)
141154

@@ -160,6 +173,7 @@ function test_component(component, clip_area, mesh=false, gui=false)
160173
substrate_layers=[:chip_outline],
161174
levelwise_layers=[:chip_outline],
162175
indexed_layers=[:port],
176+
ignored_layers=[:ignored],
163177
postrender_ops=[
164178
(
165179
"substrates",
@@ -181,7 +195,9 @@ function test_component(component, clip_area, mesh=false, gui=false)
181195
("substrates", 3),
182196
("vacuum", 3),
183197
("base_metal", 2),
184-
("circle", 2)
198+
("circle", 2),
199+
("norender", 2),
200+
("ignored", 2)
185201
],
186202
solidmodel=true,
187203
simulation=true
@@ -202,6 +218,10 @@ function test_component(component, clip_area, mesh=false, gui=false)
202218
@test !SolidModels.hasgroup(sm, "base_negative", 2)
203219
@test !SolidModels.hasgroup(sm, "circle", 2)
204220

221+
# Verify that NORENDER_META element is not present (should be skipped even though it's retained)
222+
@test !SolidModels.hasgroup(sm, "norender", 2)
223+
@test !SolidModels.hasgroup(sm, "ignored", 2) # Same for `ignored_layers=[:ignored]`
224+
205225
# The physical groups should be reindexed in decreasing order of dimension, and then
206226
# alphabetically
207227
@test sm["substrates", 3].grouptag == 1

test/test_schematicdriven.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,14 @@ end
532532
[GDSMeta(), GDSMeta(300), GDSMeta(302, 4), GDSMeta(), GDSMeta(2, 2)]
533533
@test SchematicDrivenLayout.map_layer(ArtworkTarget(tech), SemanticMeta(:GDS2)) ==
534534
GDSMeta(2)
535+
536+
# Manual map_meta_dict override
537+
target = ArtworkTarget(tech; levels=[1])
538+
target.map_meta_dict[meta] = nothing
539+
target.map_meta_dict[GDSMeta(2, 2)] = GDSMeta(3, 3)
540+
cell = Cell("test", nm)
541+
render!(cell, cs, target) # undef_meta, GDSMeta(2,2)
542+
@test cell.element_metadata == [GDSMeta(), GDSMeta(3, 3)]
535543
end
536544

537545
@variant TestCompVariant TestComponent new_defaults =

test/test_solidmodel.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ import DeviceLayout.SolidModels.STP_UNIT
260260
) SolidModels.get_boundary(sm["test", 2]; direction="X", position="no")
261261
)
262262

263-
SolidModels.remove_group!(sm, "test", 2; recursive=false)
263+
SolidModels.remove_group!(sm, "test", 2; recursive=false, remove_entities=false)
264264
@test !SolidModels.hasgroup(sm, "test", 2)
265265
@test !isempty(SolidModels.dimtags(sm["test_bdy", 1]))
266266
@test !isempty(SolidModels.dimtags(sm["test_bdy_xmin", 1]))
@@ -276,7 +276,7 @@ import DeviceLayout.SolidModels.STP_UNIT
276276
@test_logs (
277277
:info,
278278
"remove_group!(sm, foo, 3; recursive=true, remove_entities=false): (foo, 3) is not a physical group."
279-
) SolidModels.remove_group!(sm, "foo", 3)
279+
) SolidModels.remove_group!(sm, "foo", 3; remove_entities=false)
280280
)
281281
@test isempty(
282282
@test_logs (

0 commit comments

Comments
 (0)