From 615178cf57a7a8bc282b9bb75cae55efefb11515 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Wed, 26 Nov 2025 14:43:24 -0500 Subject: [PATCH 1/6] Merge NamedTuple transforms by merging the underlying NamedTuples --- src/aggregation.jl | 9 +++++++++ test/runtests.jl | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/aggregation.jl b/src/aggregation.jl index f3266d08..d2810686 100644 --- a/src/aggregation.jl +++ b/src/aggregation.jl @@ -286,6 +286,15 @@ Base.getindex(t::TransformTuple, i::Int) = getindex(_inner(t), i) Base.propertynames(t::TransformTuple) = propertynames(_inner(t)) Base.getproperty(t::TransformTuple, i::Int) = getproperty(_inner(t), i) Base.getproperty(t::TransformTuple{<:NamedTuple}, i::Symbol) = getproperty(_inner(t), i) +""" +$(SIGNATURES) + +Merge multiple `TransformTuple{<:NamedTuple}` by merging the underlying `NamedTuple`s. +""" +function Base.merge(t1::TransformTuple{<:NamedTuple}, + ts::Vararg{TransformTuple{<:NamedTuple}}) + TransformTuple(merge(_inner(t1), [_inner(t) for t in ts]...)) +end function _summary_rows(transformation::TransformTuple, mime) inner = _inner(transformation) diff --git a/test/runtests.jl b/test/runtests.jl index 1c84c106..94c786f8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -506,6 +506,23 @@ end @test_throws ArgumentError("Property :b not in (:a, :c).") inverse(t, (a = 1.0, c = 2.0)) end +@testset "merging NamedTuple" begin + t1 = as((a = asℝ, b = as𝕀)) + t2 = as((c = CorrCholeskyFactor(3), d = unit_vector_norm(4))) + t3 = as((e = asℝ₊, f = as𝕀)) + tm = merge(t1) + @test tm == t1 + tm = merge(t1, t2) + @test tm == as((a = asℝ, b = as𝕀, c = CorrCholeskyFactor(3), d = unit_vector_norm(4))) + tm = merge(t1, t2, t3) + @test tm == as((a = asℝ, b = as𝕀, c = CorrCholeskyFactor(3), d = unit_vector_norm(4), + e = asℝ₊, f = as𝕀)) + x = randn(dimension(tm)) + y = transform(tm, x) + x′ = inverse(tm, y) + @test x ≈ x′ +end + #### #### log density correctness checks #### From c1dd0582d00266ee972a87019bdefe305709993d Mon Sep 17 00:00:00 2001 From: Isaac Wheeler <47340776+Ickaser@users.noreply.github.com> Date: Sat, 29 Nov 2025 20:44:16 -0500 Subject: [PATCH 2/6] Check inference of merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 94c786f8..557c6b89 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -510,11 +510,11 @@ end t1 = as((a = asℝ, b = as𝕀)) t2 = as((c = CorrCholeskyFactor(3), d = unit_vector_norm(4))) t3 = as((e = asℝ₊, f = as𝕀)) - tm = merge(t1) + tm = @inferred(merge(t1)) @test tm == t1 - tm = merge(t1, t2) + tm = @inferred(merge(t1, t2)) @test tm == as((a = asℝ, b = as𝕀, c = CorrCholeskyFactor(3), d = unit_vector_norm(4))) - tm = merge(t1, t2, t3) + tm = @inferred(merge(t1, t2, t3)) @test tm == as((a = asℝ, b = as𝕀, c = CorrCholeskyFactor(3), d = unit_vector_norm(4), e = asℝ₊, f = as𝕀)) x = randn(dimension(tm)) From ce54bee1c9c64539e50c81461a71112c01abb7fa Mon Sep 17 00:00:00 2001 From: Isaac Wheeler <47340776+Ickaser@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:01:22 -0500 Subject: [PATCH 3/6] Map and splat, rather than vector comprehension and splat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/aggregation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregation.jl b/src/aggregation.jl index d2810686..e5aca5a6 100644 --- a/src/aggregation.jl +++ b/src/aggregation.jl @@ -293,7 +293,7 @@ Merge multiple `TransformTuple{<:NamedTuple}` by merging the underlying `NamedTu """ function Base.merge(t1::TransformTuple{<:NamedTuple}, ts::Vararg{TransformTuple{<:NamedTuple}}) - TransformTuple(merge(_inner(t1), [_inner(t) for t in ts]...)) + TransformTuple(merge(_inner(t1), map(_inner, ts)...)) end function _summary_rows(transformation::TransformTuple, mime) From e0e7ca0e38460d33094d84ff612225a8c0883940 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Sat, 29 Nov 2025 21:16:27 -0500 Subject: [PATCH 4/6] Add a brief note to docs --- docs/src/index.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/src/index.md b/docs/src/index.md index aa0aecea..3767eb9a 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -85,6 +85,18 @@ See `methods(as)` for all the constructors, `?as` for their documentation. as ``` +Transforms which produce `NamedTuple`s can be `merge`d, which uses the semantics of `Base.merge` (by internally calling `Base.merge`) for handling name collisions. +When using e.g. [ConstructionBase.setproperties](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.setproperties) to map a vector onto a subset of parameters stored in a struct, this functionality allows transforms for different parameter subsets to be constructed for use separately or together: + +```julia +t_a = as((;a = asℝ₊)) +t_b = as((;b = as𝕀)) +t_c = as((;c = TVShift(5) ∘ TVExp())) +t_ab = merge(t_a, t_b) +t_abc = merge(t_ab, t_c) +t_abc = merge(t_a, t_b, t_c) +``` + ## Scalar transforms The symbol `∞` is a placeholder for infinity. It does not correspond to `Inf`, but acts as a placeholder for the correct dispatch. `-∞` is valid. From 39326f674fde42890d69d6a0e3037dc484e132ae Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Sat, 29 Nov 2025 21:17:59 -0500 Subject: [PATCH 5/6] Code format the link --- docs/src/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index 3767eb9a..1fc2cac3 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -86,7 +86,7 @@ as ``` Transforms which produce `NamedTuple`s can be `merge`d, which uses the semantics of `Base.merge` (by internally calling `Base.merge`) for handling name collisions. -When using e.g. [ConstructionBase.setproperties](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.setproperties) to map a vector onto a subset of parameters stored in a struct, this functionality allows transforms for different parameter subsets to be constructed for use separately or together: +When using e.g. [`ConstructionBase.setproperties`](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.setproperties) to map a vector onto a subset of parameters stored in a struct, this functionality allows transforms for different parameter subsets to be constructed for use separately or together: ```julia t_a = as((;a = asℝ₊)) From ab13a6ce5bc6481856918ac08b8acd974f8d7547 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Thu, 4 Dec 2025 12:21:42 -0500 Subject: [PATCH 6/6] Add test and a note to docs demonstrating name collision behavior when merging --- docs/src/index.md | 3 ++- test/runtests.jl | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index 1fc2cac3..5f300b37 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -85,7 +85,7 @@ See `methods(as)` for all the constructors, `?as` for their documentation. as ``` -Transforms which produce `NamedTuple`s can be `merge`d, which uses the semantics of `Base.merge` (by internally calling `Base.merge`) for handling name collisions. +Transforms which produce `NamedTuple`s can be `merge`d, which internally calls `Base.merge`; name collisions will thus follow `Base` behavior, which is that the right-most instance will be kept. When using e.g. [`ConstructionBase.setproperties`](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.setproperties) to map a vector onto a subset of parameters stored in a struct, this functionality allows transforms for different parameter subsets to be constructed for use separately or together: ```julia @@ -95,6 +95,7 @@ t_c = as((;c = TVShift(5) ∘ TVExp())) t_ab = merge(t_a, t_b) t_abc = merge(t_ab, t_c) t_abc = merge(t_a, t_b, t_c) +t_collision = merge(t_a, as((;a = asℝ₋))) # Will have a = asℝ₋, from rightmost ``` ## Scalar transforms diff --git a/test/runtests.jl b/test/runtests.jl index 557c6b89..96c8ffb6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -521,6 +521,14 @@ end y = transform(tm, x) x′ = inverse(tm, y) @test x ≈ x′ + # Check merge collision behavior: rightmost gets kept + t4 = as((b = asℝ₋, c = TVScale(2.0))) + tm = @inferred(merge(t1, t4)) + @test tm == as((a = asℝ, b = asℝ₋, c = TVScale(2.0))) + @test tm != as((a = asℝ, b = as𝕀, c = TVScale(2.0))) + tm = @inferred(merge(t1, t4, t2)) + @test tm == as((a = asℝ, b = asℝ₋, c = CorrCholeskyFactor(3), d = unit_vector_norm(4))) + end ####