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
1 change: 0 additions & 1 deletion .gitattributes

This file was deleted.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/src/examples/singletransmon.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```

Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
DeviceLayout.OptionalStyle
DeviceLayout.optional_entity
DeviceLayout.ToTolerance
DeviceLayout.WithDirection
```

### [GeometryStructure](@id api-geometrystructure)
Expand Down
24 changes: 14 additions & 10 deletions examples/SingleTransmon/SingleTransmon.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -192,30 +192,34 @@ 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.

- `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
high-order spaces.
- `amr = 0`: Maximum number of adaptive mesh refinement (AMR) iterations.
"""
function configfile(
sch::Schematic,
sm::SolidModel;
palace_build=nothing,
solver_order=2,
amr=0,
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(
Expand Down Expand Up @@ -275,13 +279,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]
)
)
)...,
Expand All @@ -290,7 +294,7 @@ function configfile(
"Attributes" => [attributes["lumped_element"]],
"L" => 14.860e-9,
"C" => 5.5e-15,
"Direction" => "+Y"
"Direction" => lumped_dirs[1]
)
]
),
Expand Down Expand Up @@ -388,14 +392,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
Expand Down
3 changes: 2 additions & 1 deletion src/DeviceLayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions src/schematics/ExamplePDK/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,66 @@ 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 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`,
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 = DeviceLayout._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

# 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
49 changes: 49 additions & 0 deletions src/styles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,52 @@ 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` 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 describes the global
orientation.

If an angle is given without units, it is assumed to be in radians.
The stored angle is **not** automatically normalized to `[0°, 360°)`.
"""
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))

# 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
Loading
Loading