diff --git a/ext/ParameterSetYAMLExt.jl b/ext/ParameterSetYAMLExt.jl index f7b50f69d..9aa8c02f2 100644 --- a/ext/ParameterSetYAMLExt.jl +++ b/ext/ParameterSetYAMLExt.jl @@ -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. @@ -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 diff --git a/src/units.jl b/src/units.jl index 729fc7801..fde9468c0 100644 --- a/src/units.jl +++ b/src/units.jl @@ -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}) = diff --git a/test/test_parameter_set.jl b/test/test_parameter_set.jl index b18b504bf..ae477ee0e 100644 --- a/test/test_parameter_set.jl +++ b/test/test_parameter_set.jl @@ -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