From b3d1e8b648bd9e44ed3c367187b7c645a4156199 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Wed, 13 May 2026 05:44:02 +0200 Subject: [PATCH 01/10] Add WithDirection style + port_directions query (Issue #52 Phase 2) Adds a `WithDirection <: GeometryEntityStyle` style that annotates any `GeometryEntity` with a direction angle (CCW from +X in local frame), and a `port_directions(sch::Schematic, ly::Symbol) -> Dict{Int, String}` query that extracts Palace-compatible `Direction` strings for indexed entities on layer `ly` that carry the style. Minor fixes, simplify tests --- src/DeviceLayout.jl | 3 +- src/schematics/SchematicDrivenLayout.jl | 1 + src/schematics/solidmodels.jl | 78 ++++++++++++++++++++ src/styles.jl | 39 ++++++++++ test/test_entity.jl | 97 +++++++++++++++++++++++++ 5 files changed, 217 insertions(+), 1 deletion(-) diff --git a/src/DeviceLayout.jl b/src/DeviceLayout.jl index 2fef9d2f7..3939202fc 100644 --- a/src/DeviceLayout.jl +++ b/src/DeviceLayout.jl @@ -218,7 +218,8 @@ export transform, # Entity styles include("styles.jl") -export OptionalStyle, ToTolerance, optional_entity, MeshSized, meshsized_entity, styled +export OptionalStyle, + ToTolerance, WithDirection, optional_entity, MeshSized, meshsized_entity, styled """ abstract type GeometryStructure{S} <: AbstractGeometry{S} diff --git a/src/schematics/SchematicDrivenLayout.jl b/src/schematics/SchematicDrivenLayout.jl index 36d6a7111..fc8446092 100644 --- a/src/schematics/SchematicDrivenLayout.jl +++ b/src/schematics/SchematicDrivenLayout.jl @@ -120,6 +120,7 @@ export ParameterSet, MissingNamespace, ParameterKeyError, resolve, leaf_params, save_parameter_set export ProcessTechnology, SimulationTarget, ArtworkTarget, SolidModelTarget export base_variant, flipchip!, map_metadata!, @composite_variant, @variant +export port_directions """ const Component = AbstractComponent{typeof(1.0UPREFERRED)} diff --git a/src/schematics/solidmodels.jl b/src/schematics/solidmodels.jl index a76c8ce6b..ede9f7fff 100644 --- a/src/schematics/solidmodels.jl +++ b/src/schematics/solidmodels.jl @@ -215,6 +215,84 @@ function _map_meta_fn(target::SolidModelTarget) end end +""" + port_directions(sch::Schematic, ly::Symbol) -> Dict{Int, String} + +For every entity on layer `ly` in `sch.coordinate_system` that has been indexed +(i.e., `layerindex(metadata) != 0`) AND carries a [`WithDirection`](@ref) style in +its wrapper chain, return a dictionary mapping `layerindex(metadata) -> direction string` suitable for Palace's `LumpedPort`/`WavePort` `Direction` field. + +Direction strings are `"+X"`, `"-X"`, `"+Y"`, `"-Y"` for axis-aligned orientations +(within `atol=1e-3` degrees of the nearest axis), or `"[dx, dy, 0.0]"` for arbitrary +orientations. + +Must be called AFTER indexing has run. Typical usage is after `render!(sm, sch, target)` or `Cell(sch, target)` for a target whose `indexed_layers(target)` +includes `ly`. If no entities on `ly` are indexed or none carry `WithDirection`, +returns an empty `Dict`. This function does NOT call `index_layer!` itself. + +# Example + +```julia +render!(sm, sch, target) +dirs = port_directions(sch, :lumped_element) +# Dict(1 => "+Y", 2 => "-X") +``` + +See also: [`WithDirection`](@ref). +""" +function port_directions(sch::Schematic, ly::Symbol) + dirs = Dict{Int, String}() + # Traverse all reachable coordinate systems in the schematic (the schematic's + # own `coordinate_system` plus every reference descendant). `index_layer!` + # places indexed entities onto per-node coordsyses, recording the node for + # each index in `sch.index_dict[ly]` and setting in-component indices to 0. + # Each `(cs, trans)` pair includes the accumulated reference transform; + # applying it makes the returned direction reflect the entity's global orientation. + for (cs, trans) in DeviceLayout.traversal(sch.coordinate_system) + for (el, m) in zip(elements(cs), element_metadata(cs)) + layer(m) == ly || continue + idx = layerindex(m) + idx == 0 && continue + dir = _extract_direction(el) + dir === nothing && continue + haskey(dirs, idx) && error("Repeated index $idx. Before calling `port_directions`, \ + layer $ly should be indexed by rendering with a target whose `indexed_layers` \ + include $ly (or indexed directly with `index_layer!`)") + dirs[idx] = _direction_string(rotated_direction(dir, trans)) + end + end + return dirs +end + +# Walk through any nesting of StyledEntity wrappers and return the `direction` +# angle of the innermost `WithDirection` style encountered, or `nothing` if no +# `WithDirection` is present. Handles nesting like +# WithDirection(MeshSized(only_simulated(rect))) and the reverse. +_extract_direction(::DeviceLayout.GeometryEntity) = nothing +function _extract_direction(ent::DeviceLayout.StyledEntity) + return _extract_direction(ent.ent) +end +function _extract_direction(ent::DeviceLayout.StyledEntity{T,U,WithDirection}) where {T, U <: GeometryEntity{T}} + return ent.sty.direction +end + +# Format a direction angle (CCW from +X, in degrees) as a Palace-compatible +# `Direction` string. Axis-aligned directions return one of "+X", "-X", "+Y", +# "-Y"; off-axis returns a unit-vector literal "[dx, dy, 0.0]" with 6-digit +# precision. Input is normalized modulo 360°. +function _direction_string(angle; atol=1e-3) + a_deg = mod(ustrip(°, angle), 360.0) + abs(a_deg - 0.0) < atol && return "+X" + abs(a_deg - 90.0) < atol && return "+Y" + abs(a_deg - 180.0) < atol && return "-X" + abs(a_deg - 270.0) < atol && return "-Y" + abs(a_deg - 360.0) < atol && return "+X" + a_rad = a_deg * π / 180.0 + dx = round(cos(a_rad), digits=6) + dy = round(sin(a_rad), digits=6) + return "[$(dx), $(dy), 0.0]" +end + """ render!(sm::SolidModel, sch::Schematic, target::Target; strict=:error, kwargs...) diff --git a/src/styles.jl b/src/styles.jl index e14eac044..974a9fc93 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -350,3 +350,42 @@ struct ToTolerance{T <: Coordinate} <: GeometryEntityStyle end to_polygons(ent::GeometryEntity, sty::ToTolerance; kwargs...) = to_polygons(ent; merge((; kwargs...), (; atol=sty.atol))...) + +""" + struct WithDirection <: GeometryEntityStyle + direction::typeof(1.0°) + end + WithDirection(direction=0°) + +Style that annotates a `GeometryEntity` with a direction angle (CCW from the +X axis +in the entity's local frame) for use in simulation configuration. For example, a +lumped-port rectangle can carry its electrical orientation so that Palace's +`LumpedPort`/`WavePort` `Direction` field can be populated after rendering. + +Rendering is unaffected: `to_polygons` passes through to the underlying entity, and +`to_primitives` is stripped by the generic fallback at `src/solidmodels/render.jl`. +The direction transforms with the entity under rotation or reflection via +`transform(sty::WithDirection, f::Transformation) = WithDirection(rotated_direction(sty.direction, f))`, +so after `plan!`/`build!`/`index_layer!` the carried angle reflects the global +orientation. + +The stored angle is NOT automatically normalized to `[0°, 360°)`. Use +[`port_directions`](@ref) to extract Palace-compatible strings (which normalize and +map to `"+X"`/`"-X"`/`"+Y"`/`"-Y"` or `"[dx, dy, 0]"`). + +See also: [`port_directions`](@ref), [`MeshSized`](@ref), [`OptionalStyle`](@ref). +""" +struct WithDirection <: GeometryEntityStyle + direction::typeof(1.0°) + # Constrain the inner constructor to a Number so WithDirection(::GeometryEntity) + # routes via the generic `(T::Type{<:GeometryEntityStyle})(x::GeometryEntity, args...)` + # fallback instead of colliding with this constructor (silences Aqua). + WithDirection(direction::Number) = new(uconvert(°, direction)) +end +# Default constructor — no-arg form is unambiguous. +WithDirection() = WithDirection(0°) + +to_polygons(ent::GeometryEntity, ::WithDirection; kwargs...) = to_polygons(ent; kwargs...) + +transform(sty::WithDirection, f::Transformation) = + WithDirection(rotated_direction(sty.direction, f)) diff --git a/test/test_entity.jl b/test/test_entity.jl index c9220d77e..e5f20d2c0 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -156,3 +156,100 @@ Paths.TaperCPW{typeof(1.0μm)}(30μm, 10μm, 40μm, 10μm, 10μm) end end + +@testitem "WithDirection" setup = [CommonTestSetup] begin + ## Construction + @test WithDirection().direction == 0° + # Numeric argument converts to degrees + @test WithDirection(pi/4).direction ≈ 45° + ## Transformation + sty = WithDirection(90°) + @test transform(sty, Rotation(90°)).direction == 180° + @test transform(sty, Rotation(270°)).direction % 360° == 0° + @test isapprox_angle(transform(sty, XReflection()).direction, 270°) + @test transform(WithDirection(), XReflection()).direction == 0° + @test transform(WithDirection(), ScaledIsometry(nothing, 90°, true, 1.0)).direction == 90° + @test isapprox_angle(transform(sty, ScaledIsometry(nothing, 90°, true, 1.0)).direction, 0°) + @test transform(sty, Transformations.IdentityTransformation()).direction == 90° + ## Rendering + rect = Rectangle(2μm, 3μm) + @test to_polygons(sty(rect)) == to_polygons(rect) + ## Direction extraction + opt = optional_entity(rect, :foo; default=true) + msz = meshsized_entity(opt, 0.5μm) + wd_outer = WithDirection(90°)(msz) + wd_inner = WithDirection(45°)(rect) + msz2 = meshsized_entity(wd_inner, 0.5μm) + opt2 = optional_entity(msz2, :foo; default=true) + @test SchematicDrivenLayout._extract_direction(wd_outer) == 90° + @test SchematicDrivenLayout._extract_direction(wd_inner) == 45° + @test SchematicDrivenLayout._extract_direction(opt2) == 45° + @test SchematicDrivenLayout._extract_direction(opt) === nothing + @test SchematicDrivenLayout._extract_direction(rect) === nothing + # If multiple WithDirection layers exist, outer wins (expected behavior, not a contract) + double = WithDirection(0°)(WithDirection(90°)(rect)) + @test SchematicDrivenLayout._extract_direction(double) == 0° + ## _direction_string + using DeviceLayout.SchematicDrivenLayout: _direction_string + @test _direction_string(0°) == "+X" + @test _direction_string(90°) == "+Y" + @test _direction_string(180°) == "-X" + @test _direction_string(270°) == "-Y" + + # Normalization: 360° → +X, -90° → -Y, 450° → +Y + @test _direction_string(360°) == "+X" + @test _direction_string(-90°) == "-Y" + @test _direction_string(450°) == "+Y" + + # Off-axis: "[dx, dy, 0.0]" format + s45 = _direction_string(45°) + @test startswith(s45, "[") + @test occursin("0.707107", s45) + @test endswith(s45, ", 0.0]") + + # Within atol tolerance → still +X + @test _direction_string(0.0005°) == "+X" + @test _direction_string(-0.0005°) == "+X" + @test _direction_string(359.9995°) == "+X" + + @testset "port_directions" setup = [CommonTestSetup] begin + using DeviceLayout.SchematicDrivenLayout + # Place three rectangles directly on the schematic's top-level coordsys. + # Two of them carry WithDirection; one is bare. + g = SchematicGraph("test-g") + sch = plan(g) + rect1 = centered(Rectangle(1μm, 1μm)) + rect2 = centered(Rectangle(1μm, 1μm)) + rect3 = centered(Rectangle(1μm, 1μm)) + place!(sch, WithDirection(0°)(rect1), SemanticMeta(:myport)) + place!(sch, WithDirection(90°)(rect2), SemanticMeta(:myport)) + place!(sch, rect3, SemanticMeta(:myport)) # bare, no direction + # Needs to be indexed first + @test_throws "Repeated index" port_directions(sch, :myport) + + SchematicDrivenLayout.index_layer!(sch, :myport) + dirs = port_directions(sch, :myport) + # Indexed entities 1 and 2 have directions; entity 3 is bare so no entry. + @test length(dirs) == 2 + @test dirs[1] == "+X" + @test dirs[2] == "+Y" + @test !haskey(dirs, 3) + + g = SchematicGraph("test-g-empty") + sch = plan(g) + rect = centered(Rectangle(1μm, 1μm)) + place!(sch, rect, SemanticMeta(:bareport)) # no WithDirection + SchematicDrivenLayout.index_layer!(sch, :bareport) + dirs = port_directions(sch, :bareport) + @test isempty(dirs) + # Entities with explicit index=0 (e.g. inner component entities after a + # Phase-2 `index_layer!` flatten-and-clear pass) must be skipped by + # port_directions. + g = SchematicGraph("test-g-zeroidx") + sch = plan(g) + rect = centered(Rectangle(1μm, 1μm)) + place!(sch, WithDirection(0°)(rect), SemanticMeta(:zly, index=0)) + dirs = port_directions(sch, :zly) + @test isempty(dirs) + end +end \ No newline at end of file From 1548721d5f3ccee3327bc1340f8e7f25d4e28c8c Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 22 May 2026 17:26:31 +0200 Subject: [PATCH 02/10] Run formatter --- src/schematics/solidmodels.jl | 11 +++++++---- test/test_entity.jl | 12 ++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/schematics/solidmodels.jl b/src/schematics/solidmodels.jl index ede9f7fff..a4217c004 100644 --- a/src/schematics/solidmodels.jl +++ b/src/schematics/solidmodels.jl @@ -255,9 +255,10 @@ function port_directions(sch::Schematic, ly::Symbol) idx == 0 && continue dir = _extract_direction(el) dir === nothing && continue - haskey(dirs, idx) && error("Repeated index $idx. Before calling `port_directions`, \ - layer $ly should be indexed by rendering with a target whose `indexed_layers` \ - include $ly (or indexed directly with `index_layer!`)") + haskey(dirs, idx) && + error("Repeated index $idx. Before calling `port_directions`, \ +layer $ly should be indexed by rendering with a target whose `indexed_layers` \ +include $ly (or indexed directly with `index_layer!`)") dirs[idx] = _direction_string(rotated_direction(dir, trans)) end end @@ -272,7 +273,9 @@ _extract_direction(::DeviceLayout.GeometryEntity) = nothing function _extract_direction(ent::DeviceLayout.StyledEntity) return _extract_direction(ent.ent) end -function _extract_direction(ent::DeviceLayout.StyledEntity{T,U,WithDirection}) where {T, U <: GeometryEntity{T}} +function _extract_direction( + ent::DeviceLayout.StyledEntity{T, U, WithDirection} +) where {T, U <: GeometryEntity{T}} return ent.sty.direction end diff --git a/test/test_entity.jl b/test/test_entity.jl index e5f20d2c0..41a9adee5 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -161,15 +161,19 @@ end ## Construction @test WithDirection().direction == 0° # Numeric argument converts to degrees - @test WithDirection(pi/4).direction ≈ 45° + @test WithDirection(pi / 4).direction ≈ 45° ## Transformation sty = WithDirection(90°) @test transform(sty, Rotation(90°)).direction == 180° @test transform(sty, Rotation(270°)).direction % 360° == 0° @test isapprox_angle(transform(sty, XReflection()).direction, 270°) @test transform(WithDirection(), XReflection()).direction == 0° - @test transform(WithDirection(), ScaledIsometry(nothing, 90°, true, 1.0)).direction == 90° - @test isapprox_angle(transform(sty, ScaledIsometry(nothing, 90°, true, 1.0)).direction, 0°) + @test transform(WithDirection(), ScaledIsometry(nothing, 90°, true, 1.0)).direction == + 90° + @test isapprox_angle( + transform(sty, ScaledIsometry(nothing, 90°, true, 1.0)).direction, + 0° + ) @test transform(sty, Transformations.IdentityTransformation()).direction == 90° ## Rendering rect = Rectangle(2μm, 3μm) @@ -252,4 +256,4 @@ end dirs = port_directions(sch, :zly) @test isempty(dirs) end -end \ No newline at end of file +end From ad0b59a7cf55313f7294c7064501c11c30213160 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Fri, 22 May 2026 18:06:27 +0200 Subject: [PATCH 03/10] Update test_entity.jl --- test/test_entity.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_entity.jl b/test/test_entity.jl index 41a9adee5..c0a522bbd 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -216,7 +216,7 @@ end @test _direction_string(-0.0005°) == "+X" @test _direction_string(359.9995°) == "+X" - @testset "port_directions" setup = [CommonTestSetup] begin + @testset "port_directions" begin using DeviceLayout.SchematicDrivenLayout # Place three rectangles directly on the schematic's top-level coordsys. # Two of them carry WithDirection; one is bare. From 99ad7dcd007da97056f560ebe3ed8f2211253ce1 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 27 May 2026 10:43:19 +0200 Subject: [PATCH 04/10] Doc fixes --- src/schematics/solidmodels.jl | 2 +- src/styles.jl | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/schematics/solidmodels.jl b/src/schematics/solidmodels.jl index a4217c004..a676c1c55 100644 --- a/src/schematics/solidmodels.jl +++ b/src/schematics/solidmodels.jl @@ -266,7 +266,7 @@ include $ly (or indexed directly with `index_layer!`)") end # Walk through any nesting of StyledEntity wrappers and return the `direction` -# angle of the innermost `WithDirection` style encountered, or `nothing` if no +# angle of the outermost `WithDirection` style encountered, or `nothing` if no # `WithDirection` is present. Handles nesting like # WithDirection(MeshSized(only_simulated(rect))) and the reverse. _extract_direction(::DeviceLayout.GeometryEntity) = nothing diff --git a/src/styles.jl b/src/styles.jl index 974a9fc93..b24f72a03 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -362,17 +362,18 @@ in the entity's local frame) for use in simulation configuration. For example, a lumped-port rectangle can carry its electrical orientation so that Palace's `LumpedPort`/`WavePort` `Direction` field can be populated after rendering. -Rendering is unaffected: `to_polygons` passes through to the underlying entity, and -`to_primitives` is stripped by the generic fallback at `src/solidmodels/render.jl`. +Rendering is unaffected: `to_polygons` and `to_primitives` pass through to the underlying entity. The direction transforms with the entity under rotation or reflection via `transform(sty::WithDirection, f::Transformation) = WithDirection(rotated_direction(sty.direction, f))`, -so after `plan!`/`build!`/`index_layer!` the carried angle reflects the global +so after `plan!`/`build!`/`index_layer!` the carried angle describes the global orientation. -The stored angle is NOT automatically normalized to `[0°, 360°)`. Use +The stored angle is not automatically normalized to `[0°, 360°)`. Use [`port_directions`](@ref) to extract Palace-compatible strings (which normalize and map to `"+X"`/`"-X"`/`"+Y"`/`"-Y"` or `"[dx, dy, 0]"`). +If an angle is given without units, it is assumed to be in radians. + See also: [`port_directions`](@ref), [`MeshSized`](@ref), [`OptionalStyle`](@ref). """ struct WithDirection <: GeometryEntityStyle From 6be1311d679b6a3ea767122d013acd37dd7e02eb Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 27 May 2026 13:17:32 +0200 Subject: [PATCH 05/10] Move port_directions to ExamplePDK, use WithDirection for SingleTransmon config --- examples/SingleTransmon/SingleTransmon.jl | 21 ++--- .../SimpleJunctions/SimpleJunctions.jl | 2 +- src/schematics/ExamplePDK/utils.jl | 77 ++++++++++++++++++ src/schematics/SchematicDrivenLayout.jl | 1 - src/schematics/solidmodels.jl | 81 ------------------- src/styles.jl | 7 +- test/test_entity.jl | 76 ++++++++--------- 7 files changed, 131 insertions(+), 134 deletions(-) diff --git a/examples/SingleTransmon/SingleTransmon.jl b/examples/SingleTransmon/SingleTransmon.jl index 1906d90de..db32766dc 100644 --- a/examples/SingleTransmon/SingleTransmon.jl +++ b/examples/SingleTransmon/SingleTransmon.jl @@ -108,12 +108,12 @@ function single_transmon(; csport = CoordinateSystem(uniquename("port"), nm) render!( csport, - only_simulated(centered(Rectangle(cpw_width, cpw_width))), + only_simulated(WithDirection(centered(Rectangle(cpw_width, cpw_width)))), LayerVocabulary.PORT ) # Attach with port center `cpw_width` from the end (instead of `cpw_width/2`) to avoid corner effects attach!(p_readout, sref(csport), cpw_width, i=1) # @ start - attach!(p_readout, sref(csport), readout_length / 2 - cpw_width, i=2) # @ end + attach!(p_readout, sref(csport, rot=180°), readout_length / 2 - cpw_width, i=2) # @ end end #### Build schematic graph @@ -192,11 +192,11 @@ function single_transmon(; flatten!(c) save(joinpath(@__DIR__, "single_transmon.gds"), c) end - return sm + return floorplan, sm end """ - configfile(sm::SolidModel; palace_build=nothing, solver_order=2, amr=0, wave_ports=false) + configfile(sch::Schematic, sm::SolidModel; palace_build=nothing, solver_order=2, amr=0, wave_ports=false) Given a `SolidModel`, assemble a dictionary defining a configuration file for use within Palace. @@ -209,6 +209,7 @@ Palace. - `amr = 0`: Maximum number of adaptive mesh refinement (AMR) iterations. """ function configfile( + sch::Schematic, sm::SolidModel; palace_build=nothing, solver_order=2, @@ -216,6 +217,8 @@ function configfile( wave_ports=false ) attributes = SolidModels.attributes(sm) + port_dirs = ExamplePDK.port_directions(sch, layer(LayerVocabulary.PORT)) + lumped_dirs = ExamplePDK.port_directions(sch, layer(LayerVocabulary.LUMPED_ELEMENT)) config = Dict( "Problem" => Dict( @@ -275,13 +278,13 @@ function configfile( "Index" => 1, "Attributes" => [attributes["port_1"]], "R" => 50, - "Direction" => "+X" + "Direction" => port_dirs[1] ), Dict( "Index" => 2, "Attributes" => [attributes["port_2"]], "R" => 50, - "Direction" => "+X" + "Direction" => port_dirs[2] ) ) )..., @@ -290,7 +293,7 @@ function configfile( "Attributes" => [attributes["lumped_element"]], "L" => 14.860e-9, "C" => 5.5e-15, - "Direction" => "+Y" + "Direction" => lumped_dirs[1] ) ] ), @@ -388,14 +391,14 @@ function compute_eigenfrequencies( total_length=5000μm ) # Construct the SolidModel - @time "SolidModel + Meshing" sm = single_transmon( + @time "SolidModel + Meshing" floorplan, sm = single_transmon( save_mesh=true; cap_length=cap_length, total_length=total_length, mesh_order=mesh_order ) # Assemble the configuration - @time "Configuration" config = configfile(sm; palace_build, solver_order=solver_order) + @time "Configuration" config = configfile(floorplan, sm; palace_build, solver_order=solver_order) # Call Palace @time "Palace" freqs = palace_job(config; palace_build, np) return freqs diff --git a/src/schematics/ExamplePDK/components/SimpleJunctions/SimpleJunctions.jl b/src/schematics/ExamplePDK/components/SimpleJunctions/SimpleJunctions.jl index afdcfc558..cceda07b8 100644 --- a/src/schematics/ExamplePDK/components/SimpleJunctions/SimpleJunctions.jl +++ b/src/schematics/ExamplePDK/components/SimpleJunctions/SimpleJunctions.jl @@ -73,7 +73,7 @@ function SchematicDrivenLayout._geometry!(cs::CoordinateSystem, jj::ExampleSimpl top_lead = Align.above(Rectangle(w_jj, (h_ground_island - h_jj) / 2), jj_rect; centered=true) bot_lead = Align.below(top_lead, jj_rect) - place!(cs, only_simulated(jj_rect), LUMPED_ELEMENT) + place!(cs, only_simulated(WithDirection(jj_rect, 90°)), LUMPED_ELEMENT) place!(cs, MeshSized(2 * w_jj)(only_simulated(top_lead)), METAL_POSITIVE) place!(cs, MeshSized(2 * w_jj)(only_simulated(bot_lead)), METAL_POSITIVE) # artwork geometry diff --git a/src/schematics/ExamplePDK/utils.jl b/src/schematics/ExamplePDK/utils.jl index 2698b2c30..2c1951f2f 100644 --- a/src/schematics/ExamplePDK/utils.jl +++ b/src/schematics/ExamplePDK/utils.jl @@ -231,3 +231,80 @@ end @deprecate filter_params filter_parameters # For backward compatibility # (No one should be using methods from ExamplePDK but just in case) + +""" + port_directions(sch::Schematic, ly::Symbol) -> Dict{Int, Union{String, Vector{Float64}}} + +For every entity on layer `ly` in `sch.coordinate_system` that has been indexed +(i.e., `layerindex(metadata) != 0`) AND carries a [`WithDirection`](@ref) style in +its wrapper chain, return a dictionary mapping `layerindex(metadata) -> direction config value` suitable for Palace's `LumpedPort`/`WavePort` `Direction` field. + +Direction strings are `"+X"`, `"-X"`, `"+Y"`, `"-Y"` for axis-aligned orientations +(within `atol=1e-3` degrees of the nearest axis), or `[dx, dy, 0.0]` for arbitrary +orientations. + +Must be called AFTER indexing has run. Typical usage is after `render!(sm, sch, target)` or `Cell(sch, target)` for a target whose `indexed_layers(target)` +includes `ly`. If no entities on `ly` are indexed or none carry `WithDirection`, +returns an empty `Dict`. This function does NOT call `index_layer!` itself. + +# Example + +```julia +render!(sm, sch, target) +dirs = port_directions(sch, :lumped_element) +# Dict(1 => "+Y", 2 => "-X") +``` + +See also: [`WithDirection`](@ref). +""" +function port_directions(sch::Schematic, ly::Symbol) + dirs = Dict{Int, Union{String, Vector{Float64}}}() + # Traverse all reachable coordinate systems in the schematic (the schematic's + # own `coordinate_system` plus every reference descendant). `index_layer!` + # places indexed entities onto per-node coordsyses, recording the node for + # each index in `sch.index_dict[ly]` and setting in-component indices to 0. + # Each `(cs, trans)` pair includes the accumulated reference transform; + # applying it makes the returned direction reflect the entity's global orientation. + for (cs, trans) in DeviceLayout.traversal(sch.coordinate_system) + for (el, m) in zip(elements(cs), element_metadata(cs)) + layer(m) == ly || continue + idx = layerindex(m) + idx == 0 && continue + dir = _extract_direction(el) + dir === nothing && continue + haskey(dirs, idx) && + error("Repeated index $idx. Before calling `port_directions`, \ +layer $ly should be indexed by rendering with a target whose `indexed_layers` \ +include $ly (or indexed directly with `index_layer!`)") + dirs[idx] = _direction_config(rotated_direction(dir, trans)) + end + end + return dirs +end + +# Walk through any nesting of StyledEntity wrappers and return the `direction` +# angle of the outermost `WithDirection` style encountered, or `nothing` if no +# `WithDirection` is present. Handles nesting like +# WithDirection(MeshSized(only_simulated(rect))) and the reverse. +_extract_direction(::DeviceLayout.GeometryEntity) = nothing +function _extract_direction(ent::DeviceLayout.StyledEntity) + return _extract_direction(ent.ent) +end +function _extract_direction( + ent::DeviceLayout.StyledEntity{T, U, WithDirection} +) where {T, U <: GeometryEntity{T}} + return ent.sty.direction +end + +# Format a direction angle (CCW from +X, in degrees) as a Palace-compatible +# `Direction` config value. Axis-aligned directions return one of "+X", "-X", "+Y", +# "-Y"; off-axis returns a unit-vector [dx, dy, 0.0]. Input is normalized modulo 360°. +function _direction_config(angle; atol=1e-3) + a_deg = mod(DeviceLayout.ustrip(°, angle), 360.0) + abs(a_deg - 0.0) < atol && return "+X" + abs(a_deg - 90.0) < atol && return "+Y" + abs(a_deg - 180.0) < atol && return "-X" + abs(a_deg - 270.0) < atol && return "-Y" + abs(a_deg - 360.0) < atol && return "+X" + return [cos(angle), sin(angle), 0.0] +end diff --git a/src/schematics/SchematicDrivenLayout.jl b/src/schematics/SchematicDrivenLayout.jl index fc8446092..36d6a7111 100644 --- a/src/schematics/SchematicDrivenLayout.jl +++ b/src/schematics/SchematicDrivenLayout.jl @@ -120,7 +120,6 @@ export ParameterSet, MissingNamespace, ParameterKeyError, resolve, leaf_params, save_parameter_set export ProcessTechnology, SimulationTarget, ArtworkTarget, SolidModelTarget export base_variant, flipchip!, map_metadata!, @composite_variant, @variant -export port_directions """ const Component = AbstractComponent{typeof(1.0UPREFERRED)} diff --git a/src/schematics/solidmodels.jl b/src/schematics/solidmodels.jl index a676c1c55..a76c8ce6b 100644 --- a/src/schematics/solidmodels.jl +++ b/src/schematics/solidmodels.jl @@ -215,87 +215,6 @@ function _map_meta_fn(target::SolidModelTarget) end end -""" - port_directions(sch::Schematic, ly::Symbol) -> Dict{Int, String} - -For every entity on layer `ly` in `sch.coordinate_system` that has been indexed -(i.e., `layerindex(metadata) != 0`) AND carries a [`WithDirection`](@ref) style in -its wrapper chain, return a dictionary mapping `layerindex(metadata) -> direction string` suitable for Palace's `LumpedPort`/`WavePort` `Direction` field. - -Direction strings are `"+X"`, `"-X"`, `"+Y"`, `"-Y"` for axis-aligned orientations -(within `atol=1e-3` degrees of the nearest axis), or `"[dx, dy, 0.0]"` for arbitrary -orientations. - -Must be called AFTER indexing has run. Typical usage is after `render!(sm, sch, target)` or `Cell(sch, target)` for a target whose `indexed_layers(target)` -includes `ly`. If no entities on `ly` are indexed or none carry `WithDirection`, -returns an empty `Dict`. This function does NOT call `index_layer!` itself. - -# Example - -```julia -render!(sm, sch, target) -dirs = port_directions(sch, :lumped_element) -# Dict(1 => "+Y", 2 => "-X") -``` - -See also: [`WithDirection`](@ref). -""" -function port_directions(sch::Schematic, ly::Symbol) - dirs = Dict{Int, String}() - # Traverse all reachable coordinate systems in the schematic (the schematic's - # own `coordinate_system` plus every reference descendant). `index_layer!` - # places indexed entities onto per-node coordsyses, recording the node for - # each index in `sch.index_dict[ly]` and setting in-component indices to 0. - # Each `(cs, trans)` pair includes the accumulated reference transform; - # applying it makes the returned direction reflect the entity's global orientation. - for (cs, trans) in DeviceLayout.traversal(sch.coordinate_system) - for (el, m) in zip(elements(cs), element_metadata(cs)) - layer(m) == ly || continue - idx = layerindex(m) - idx == 0 && continue - dir = _extract_direction(el) - dir === nothing && continue - haskey(dirs, idx) && - error("Repeated index $idx. Before calling `port_directions`, \ -layer $ly should be indexed by rendering with a target whose `indexed_layers` \ -include $ly (or indexed directly with `index_layer!`)") - dirs[idx] = _direction_string(rotated_direction(dir, trans)) - end - end - return dirs -end - -# Walk through any nesting of StyledEntity wrappers and return the `direction` -# angle of the outermost `WithDirection` style encountered, or `nothing` if no -# `WithDirection` is present. Handles nesting like -# WithDirection(MeshSized(only_simulated(rect))) and the reverse. -_extract_direction(::DeviceLayout.GeometryEntity) = nothing -function _extract_direction(ent::DeviceLayout.StyledEntity) - return _extract_direction(ent.ent) -end -function _extract_direction( - ent::DeviceLayout.StyledEntity{T, U, WithDirection} -) where {T, U <: GeometryEntity{T}} - return ent.sty.direction -end - -# Format a direction angle (CCW from +X, in degrees) as a Palace-compatible -# `Direction` string. Axis-aligned directions return one of "+X", "-X", "+Y", -# "-Y"; off-axis returns a unit-vector literal "[dx, dy, 0.0]" with 6-digit -# precision. Input is normalized modulo 360°. -function _direction_string(angle; atol=1e-3) - a_deg = mod(ustrip(°, angle), 360.0) - abs(a_deg - 0.0) < atol && return "+X" - abs(a_deg - 90.0) < atol && return "+Y" - abs(a_deg - 180.0) < atol && return "-X" - abs(a_deg - 270.0) < atol && return "-Y" - abs(a_deg - 360.0) < atol && return "+X" - a_rad = a_deg * π / 180.0 - dx = round(cos(a_rad), digits=6) - dy = round(sin(a_rad), digits=6) - return "[$(dx), $(dy), 0.0]" -end - """ render!(sm::SolidModel, sch::Schematic, target::Target; strict=:error, kwargs...) diff --git a/src/styles.jl b/src/styles.jl index b24f72a03..32b4bf6f0 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -368,13 +368,8 @@ The direction transforms with the entity under rotation or reflection via so after `plan!`/`build!`/`index_layer!` the carried angle describes the global orientation. -The stored angle is not automatically normalized to `[0°, 360°)`. Use -[`port_directions`](@ref) to extract Palace-compatible strings (which normalize and -map to `"+X"`/`"-X"`/`"+Y"`/`"-Y"` or `"[dx, dy, 0]"`). - If an angle is given without units, it is assumed to be in radians. - -See also: [`port_directions`](@ref), [`MeshSized`](@ref), [`OptionalStyle`](@ref). +The stored angle is **not** automatically normalized to `[0°, 360°)`. """ struct WithDirection <: GeometryEntityStyle direction::typeof(1.0°) diff --git a/test/test_entity.jl b/test/test_entity.jl index c0a522bbd..2cb8e3fc8 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -178,46 +178,50 @@ end ## Rendering rect = Rectangle(2μm, 3μm) @test to_polygons(sty(rect)) == to_polygons(rect) - ## Direction extraction - opt = optional_entity(rect, :foo; default=true) - msz = meshsized_entity(opt, 0.5μm) - wd_outer = WithDirection(90°)(msz) - wd_inner = WithDirection(45°)(rect) - msz2 = meshsized_entity(wd_inner, 0.5μm) - opt2 = optional_entity(msz2, :foo; default=true) - @test SchematicDrivenLayout._extract_direction(wd_outer) == 90° - @test SchematicDrivenLayout._extract_direction(wd_inner) == 45° - @test SchematicDrivenLayout._extract_direction(opt2) == 45° - @test SchematicDrivenLayout._extract_direction(opt) === nothing - @test SchematicDrivenLayout._extract_direction(rect) === nothing - # If multiple WithDirection layers exist, outer wins (expected behavior, not a contract) - double = WithDirection(0°)(WithDirection(90°)(rect)) - @test SchematicDrivenLayout._extract_direction(double) == 0° - ## _direction_string - using DeviceLayout.SchematicDrivenLayout: _direction_string - @test _direction_string(0°) == "+X" - @test _direction_string(90°) == "+Y" - @test _direction_string(180°) == "-X" - @test _direction_string(270°) == "-Y" - # Normalization: 360° → +X, -90° → -Y, 450° → +Y - @test _direction_string(360°) == "+X" - @test _direction_string(-90°) == "-Y" - @test _direction_string(450°) == "+Y" + @testset "port_directions" begin + # Not currently API functionality but worth testing alongside WithDirection + using DeviceLayout.SchematicDrivenLayout + import .SchematicDrivenLayout.ExamplePDK: + port_directions, _extract_direction, _direction_config + ## Direction extraction + rect = Rectangle(2μm, 3μm) + @test to_polygons(sty(rect)) == to_polygons(rect) + opt = optional_entity(rect, :foo; default=true) + msz = meshsized_entity(opt, 0.5μm) + wd_outer = WithDirection(90°)(msz) + wd_inner = WithDirection(45°)(rect) + msz2 = meshsized_entity(wd_inner, 0.5μm) + opt2 = optional_entity(msz2, :foo; default=true) + # Extract direction + @test _extract_direction(wd_outer) == 90° + @test _extract_direction(wd_inner) == 45° + @test _extract_direction(opt2) == 45° + @test _extract_direction(opt) === nothing + @test _extract_direction(rect) === nothing + # If multiple WithDirection layers exist, outer wins (expected behavior, not a contract) + double = WithDirection(0°)(WithDirection(90°)(rect)) + @test _extract_direction(double) == 0° + ## _direction_config + @test _direction_config(0°) == "+X" + @test _direction_config(90°) == "+Y" + @test _direction_config(180°) == "-X" + @test _direction_config(270°) == "-Y" - # Off-axis: "[dx, dy, 0.0]" format - s45 = _direction_string(45°) - @test startswith(s45, "[") - @test occursin("0.707107", s45) - @test endswith(s45, ", 0.0]") + # Normalization: 360° → +X, -90° → -Y, 450° → +Y + @test _direction_config(360°) == "+X" + @test _direction_config(-90°) == "-Y" + @test _direction_config(450°) == "+Y" - # Within atol tolerance → still +X - @test _direction_string(0.0005°) == "+X" - @test _direction_string(-0.0005°) == "+X" - @test _direction_string(359.9995°) == "+X" + # Off-axis: [dx, dy, 0.0] format + s45 = _direction_config(45°) + @test s45 ≈ [cos(45°), sin(45°), 0.0] - @testset "port_directions" begin - using DeviceLayout.SchematicDrivenLayout + ## Port directions + # Within atol tolerance → still +X + @test _direction_config(0.0005°) == "+X" + @test _direction_config(-0.0005°) == "+X" + @test _direction_config(359.9995°) == "+X" # Place three rectangles directly on the schematic's top-level coordsys. # Two of them carry WithDirection; one is bare. g = SchematicGraph("test-g") From 9e72e132a0b90a9869bb183f48c55a5e5c2abcee Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 28 May 2026 11:29:37 +0200 Subject: [PATCH 06/10] Tweak port_directions docstring and add WithDirection to changelog --- CHANGELOG.md | 4 ++++ src/schematics/ExamplePDK/utils.jl | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 547b3edd6..57327d453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ The format of this changelog is based on through `_build_subcomponents` via `parameter_set(graph)` and `create_component(T, ps, address)` - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form + - Added `SchematicDrivenLayout.footprint_halo` for implementing fast custom halos with less boilerplate + - Added `WithDirection <: GeometryEntityStyle` to annotate geometry entities with a direction (CCW from +x in local frame). The direction transforms with the entity under rotations and reflections, allowing extraction of the final global direction for use in simulation configuration. + - Fixed incorrect loading of GDS array references with nonzero origin + - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form `set_parameters(c, sub::ParameterSet)` for the templates-aliasing pattern: overlay `ParameterSet` leaves on top of a template instance, with optional composite-level kwargs winning over the overlay. Unknown leaves under the diff --git a/src/schematics/ExamplePDK/utils.jl b/src/schematics/ExamplePDK/utils.jl index 2c1951f2f..0496a1f1a 100644 --- a/src/schematics/ExamplePDK/utils.jl +++ b/src/schematics/ExamplePDK/utils.jl @@ -239,9 +239,9 @@ For every entity on layer `ly` in `sch.coordinate_system` that has been indexed (i.e., `layerindex(metadata) != 0`) AND carries a [`WithDirection`](@ref) style in its wrapper chain, return a dictionary mapping `layerindex(metadata) -> direction config value` suitable for Palace's `LumpedPort`/`WavePort` `Direction` field. -Direction strings are `"+X"`, `"-X"`, `"+Y"`, `"-Y"` for axis-aligned orientations -(within `atol=1e-3` degrees of the nearest axis), or `[dx, dy, 0.0]` for arbitrary -orientations. +Direction config value is a string `"+X"`, `"-X"`, `"+Y"`, or `"-Y"` for axis-aligned orientations +(within `atol=1e-3` degrees of the nearest axis), or a unit vector `[dx, dy, 0.0]::Vector{Float64}` +for arbitrary orientations. Must be called AFTER indexing has run. Typical usage is after `render!(sm, sch, target)` or `Cell(sch, target)` for a target whose `indexed_layers(target)` includes `ly`. If no entities on `ly` are indexed or none carry `WithDirection`, From 0211db6894b778bc4deaf793646a08bcd56907b1 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 1 Jun 2026 11:54:06 +0200 Subject: [PATCH 07/10] Move changelog entry to unreleased --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57327d453..c5d19450b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## Unreleased + + - Added `WithDirection <: GeometryEntityStyle` to annotate geometry entities with a direction (CCW from +x in local frame). The direction transforms with the entity under rotations and reflections, allowing extraction of the final global direction for use in simulation configuration. + ## 1.14.0 (2026-05-28) - Added `ParameterSet`, a nested dictionary wrapper with dot-access for reading and @@ -16,7 +20,6 @@ The format of this changelog is based on `create_component(T, ps, address)` - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form - Added `SchematicDrivenLayout.footprint_halo` for implementing fast custom halos with less boilerplate - - Added `WithDirection <: GeometryEntityStyle` to annotate geometry entities with a direction (CCW from +x in local frame). The direction transforms with the entity under rotations and reflections, allowing extraction of the final global direction for use in simulation configuration. - Fixed incorrect loading of GDS array references with nonzero origin - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form `set_parameters(c, sub::ParameterSet)` for the templates-aliasing pattern: From b8d1943a1d16a403d592183400247e7066a4b461 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 1 Jun 2026 13:58:51 +0200 Subject: [PATCH 08/10] Fix changelog, revert merge=union gitattribute --- .gitattributes | 1 - CHANGELOG.md | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index a19ade077..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -CHANGELOG.md merge=union diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d19450b..a6bab5d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,6 @@ The format of this changelog is based on through `_build_subcomponents` via `parameter_set(graph)` and `create_component(T, ps, address)` - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form - - Added `SchematicDrivenLayout.footprint_halo` for implementing fast custom halos with less boilerplate - - Fixed incorrect loading of GDS array references with nonzero origin - - Added `set_parameters(c, ps, address; kwargs...)` and the scoped form `set_parameters(c, sub::ParameterSet)` for the templates-aliasing pattern: overlay `ParameterSet` leaves on top of a template instance, with optional composite-level kwargs winning over the overlay. Unknown leaves under the From 80d994035ed3ccaf1ab163cb950d2ab3b84c6098 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 1 Jun 2026 20:19:52 +0200 Subject: [PATCH 09/10] Add rotation test --- docs/src/examples/singletransmon.md | 6 +++--- docs/src/reference/api.md | 1 + examples/SingleTransmon/SingleTransmon.jl | 3 ++- test/test_entity.jl | 14 ++++++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/singletransmon.md b/docs/src/examples/singletransmon.md index 146a47d31..68e90358b 100644 --- a/docs/src/examples/singletransmon.md +++ b/docs/src/examples/singletransmon.md @@ -61,14 +61,14 @@ function compute_eigenfrequencies( total_length=5000μm ) # Construct the SolidModel - @time "SolidModel + Meshing" sm = single_transmon( + @time "SolidModel + Meshing" schematic, sm = single_transmon( save_mesh=true; cap_length=cap_length, total_length=total_length, mesh_order=mesh_order ) # Assemble the configuration - @time "Configuration" config = configfile(sm; palace_build, solver_order=solver_order) + @time "Configuration" config = configfile(schematic, sm; palace_build, solver_order=solver_order) # Call Palace @time "Palace" freqs = palace_job(config; palace_build, np) return freqs @@ -90,7 +90,7 @@ then call `single_transmon()`. This will construct the schematic and render the ```julia using DeviceLayout include("examples/SingleTransmon/SingleTransmon.jl") -sm = SingleTransmon.single_transmon() +schematic, sm = SingleTransmon.single_transmon() SolidModels.gmsh.fltk.run() # Opens Gmsh GUI ``` diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index 3eda018c4..8a6c6afd9 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -60,6 +60,7 @@ DeviceLayout.OptionalStyle DeviceLayout.optional_entity DeviceLayout.ToTolerance + DeviceLayout.WithDirection ``` ### [GeometryStructure](@id api-geometrystructure) diff --git a/examples/SingleTransmon/SingleTransmon.jl b/examples/SingleTransmon/SingleTransmon.jl index db32766dc..c9f407416 100644 --- a/examples/SingleTransmon/SingleTransmon.jl +++ b/examples/SingleTransmon/SingleTransmon.jl @@ -201,7 +201,8 @@ end Given a `SolidModel`, assemble a dictionary defining a configuration file for use within Palace. - - `sm`: The `SolidModel`from which to construct the configuration file + - `sch`: The `Schematic` corresponding to the model, which associates ports with design intent + - `sm`: The `SolidModel` for which to construct the configuration file - `palace_build = nothing`: Path to a Palace build directory, used to perform validation of the configuration file. If not present, no validation is performed. - `solver_order = 2`: Finite element order (degree) for the solver. Palace supports arbitrary diff --git a/test/test_entity.jl b/test/test_entity.jl index 2cb8e3fc8..4379cc960 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -259,5 +259,19 @@ end place!(sch, WithDirection(0°)(rect), SemanticMeta(:zly, index=0)) dirs = port_directions(sch, :zly) @test isempty(dirs) + + # SchematicGraph with component and rotated sub-cs + cs = CoordinateSystem("comp") + cs2 = CoordinateSystem("subcs") + place!(cs2, WithDirection(0°)(rect1), SemanticMeta(:myport)) + addref!(cs, sref(cs2, rot=90°)) + comp = BasicComponent(cs) + g = SchematicGraph("test") + add_node!(g, comp) + sch = plan(g) + SchematicDrivenLayout.index_layer!(sch, :myport) + dirs = port_directions(sch, :myport) + @test length(dirs) == 1 + @test dirs[1] == "+Y" end end From 3926eebb1fb5acadd5077686df011f0a022a5efa Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 2 Jun 2026 11:07:53 +0200 Subject: [PATCH 10/10] Move _extract_direction to DL proper --- src/schematics/ExamplePDK/utils.jl | 16 +--------------- src/styles.jl | 14 ++++++++++++++ test/test_entity.jl | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/schematics/ExamplePDK/utils.jl b/src/schematics/ExamplePDK/utils.jl index 0496a1f1a..cc524a3ec 100644 --- a/src/schematics/ExamplePDK/utils.jl +++ b/src/schematics/ExamplePDK/utils.jl @@ -270,7 +270,7 @@ function port_directions(sch::Schematic, ly::Symbol) layer(m) == ly || continue idx = layerindex(m) idx == 0 && continue - dir = _extract_direction(el) + dir = DeviceLayout._extract_direction(el) dir === nothing && continue haskey(dirs, idx) && error("Repeated index $idx. Before calling `port_directions`, \ @@ -282,20 +282,6 @@ include $ly (or indexed directly with `index_layer!`)") return dirs end -# Walk through any nesting of StyledEntity wrappers and return the `direction` -# angle of the outermost `WithDirection` style encountered, or `nothing` if no -# `WithDirection` is present. Handles nesting like -# WithDirection(MeshSized(only_simulated(rect))) and the reverse. -_extract_direction(::DeviceLayout.GeometryEntity) = nothing -function _extract_direction(ent::DeviceLayout.StyledEntity) - return _extract_direction(ent.ent) -end -function _extract_direction( - ent::DeviceLayout.StyledEntity{T, U, WithDirection} -) where {T, U <: GeometryEntity{T}} - return ent.sty.direction -end - # Format a direction angle (CCW from +X, in degrees) as a Palace-compatible # `Direction` config value. Axis-aligned directions return one of "+X", "-X", "+Y", # "-Y"; off-axis returns a unit-vector [dx, dy, 0.0]. Input is normalized modulo 360°. diff --git a/src/styles.jl b/src/styles.jl index 32b4bf6f0..5af422f05 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -385,3 +385,17 @@ to_polygons(ent::GeometryEntity, ::WithDirection; kwargs...) = to_polygons(ent; transform(sty::WithDirection, f::Transformation) = WithDirection(rotated_direction(sty.direction, f)) + +# Walk through any nesting of StyledEntity wrappers and return the `direction` +# angle of the first `WithDirection` style encountered (from outside in), or `nothing` if no +# `WithDirection` is present. Handles nesting like +# WithDirection(MeshSized(only_simulated(rect))) and the reverse. +_extract_direction(::DeviceLayout.GeometryEntity) = nothing +function _extract_direction(ent::DeviceLayout.StyledEntity) + return _extract_direction(ent.ent) +end +function _extract_direction( + ent::DeviceLayout.StyledEntity{T, U, WithDirection} +) where {T, U <: GeometryEntity{T}} + return ent.sty.direction +end diff --git a/test/test_entity.jl b/test/test_entity.jl index 4379cc960..05d380ff8 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -182,8 +182,8 @@ end @testset "port_directions" begin # Not currently API functionality but worth testing alongside WithDirection using DeviceLayout.SchematicDrivenLayout - import .SchematicDrivenLayout.ExamplePDK: - port_directions, _extract_direction, _direction_config + import DeviceLayout: _extract_direction + import .SchematicDrivenLayout.ExamplePDK: port_directions, _direction_config ## Direction extraction rect = Rectangle(2μm, 3μm) @test to_polygons(sty(rect)) == to_polygons(rect)