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
9 changes: 7 additions & 2 deletions ext/ParameterSetYAMLExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import Unitful
_parse_units!(data::Dict{String, Any})

Recursively walk a parsed YAML dict. String values that parse into a
`Unitful.Quantity` (e.g. `"150μm"`) are converted in place.
`Unitful.Quantity` (e.g. `"150μm"`) are converted in place via
[`DeviceLayout.uparse`](@ref), so length-dimensioned values share the
package's promotion context (`ContextUnits`) instead of carrying bare
`FreeUnits` (which would later trip the mixed-context promotion bug
fixed in https://github.com/JuliaPhysics/Unitful.jl/pull/845 — surfaced
in DeviceLayout via `layer_z` arithmetic).

Bare unit names like `"s"`, `"m"`, `"cm"` parse into `Unitful.Units`, not
`Quantity` - those are left as strings so that ordinary text values (e.g.
Expand All @@ -21,7 +26,7 @@ function _parse_units!(data::Dict{String, Any})
_parse_units!(v)
elseif v isa AbstractString
try
parsed = Unitful.uparse(v)
parsed = DeviceLayout.uparse(v)
parsed isa Unitful.Quantity && (data[k] = parsed)
catch
# not a valid unit expression, keep as-is
Expand Down
26 changes: 26 additions & 0 deletions src/units.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,32 @@ Mixed unit operations with these imports will be converted based on the unit
preference set by [`DeviceLayout.set_unit_preference!`](@ref) (default `nm`).
""" PreferredUnits

"""
uparse(str)

Parse `str` as a `Unitful.Quantity`, resolving length symbols (`fm`, `pm`, `nm`,
`μm`, `mm`, `cm`, `dm`, `m`) against [`PreferredUnits`](@ref) so the parsed value
carries the package's promotion context (`Unitful.ContextUnits`) instead of bare
`Unitful.FreeUnits`. Non-length symbols (e.g. `s`, `Hz`, `fH`) fall back to
`Unitful` and behave exactly as `Unitful.uparse`.

Use this in place of `Unitful.uparse` when ingesting unit strings from external
sources (YAML, JSON, user input) — bare `FreeUnits` lengths cause the mixed-context
promotion bug in https://github.com/JuliaPhysics/Unitful.jl/pull/845 when later
combined with package-internal `ContextUnits` values (e.g. inside `layer_z`).
"""
function uparse(str::AbstractString)
# Try PreferredUnits first; fall back to Unitful for any non-length symbol
# that PreferredUnits does not redefine. This avoids `Unitful.uparse`'s
# "found in multiple registered unit modules" warning for shared length
# symbols (μm, nm, ...) that exist in both modules with different values.
try
return Unitful.uparse(str; unit_context=PreferredUnits)
catch
return Unitful.uparse(str)
end
end

onemicron(v::T) where {T <: Unitful.Length} =
one(T) * Unitful.ContextUnits(Unitful.μm, Unitful.unit(Unitful.upreferred(v)))
onemicron(T::Type{<:Unitful.Length}) =
Expand Down
39 changes: 39 additions & 0 deletions test/test_parameter_set.jl
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,45 @@ end
@test ps2.components.transmon.island.cap_width == 24μm
@test ps2.components.transmon.junction.w_jj == 1μm
end

@testset "Parsed lengths share DeviceLayout's promotion context" begin
# `Unitful.uparse` returns FreeUnits quantities (promotion target `m`),
# which mismatch DeviceLayout's ContextUnits (target `nm` or `μm`).
# Mixing them through `+` (e.g. inside `layer_z`) used to fail with
# `MethodError: no method matching (::FreeUnits{(m,), 𝐋, nothing})()` -
# see https://github.com/JuliaPhysics/Unitful.jl/pull/845. Lengths
# parsed from YAML must therefore enter the system already wrapped in
# the package's preferred context via `DeviceLayout.uparse`.
yaml_str = """
components:
cap:
finger_length: 150μm
t_chip: 525μm
inv_length: 1/μm
"""
ps = ParameterSet(IOBuffer(yaml_str))
v = ps.components.cap.finger_length

@test v isa Unitful.Quantity
@test Unitful.unit(v) isa Unitful.ContextUnits
# The parsed length must add cleanly to a value carrying the package's
# preferred context - this was the operation that previously threw.
@test (v + 1 * DeviceLayout.PreferredUnits.UPREFERRED) isa Unitful.Quantity

# Compound expressions involving length symbols must also resolve
# against PreferredUnits so the embedded length carries ContextUnits.
@test ps.components.cap.inv_length isa Unitful.Quantity

# Reproduce the original `level_z` failure path: mixing YAML-loaded
# chip thicknesses (parsed as μm) with package-internal nm-targeted
# ContextUnits inside the same arithmetic chain. Pre-fix this raised
# the FreeUnits MethodError on the second `+`/`-`.
t_chips = [v, v]
t_gap = ps.components.cap.t_chip
nm_value = 1 * DeviceLayout.nm
z = sum(t_chips) + t_gap - nm_value
@test (z + nm_value) isa Unitful.Quantity
end
end

@testitem "Composite ParameterSet flow" setup = [CommonTestSetup] begin
Expand Down
Loading