From 573efe5b653c59c190f29343f2e0b2ba242fb0f3 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Tue, 22 Aug 2023 18:31:03 +0530 Subject: [PATCH 01/15] Accept vectors in PiecewiseSpace --- src/ApproxFunBase.jl | 9 +++++++ src/LinearAlgebra/helper.jl | 14 +++++++---- src/Spaces/SumSpace.jl | 50 +++++++++++++++++++------------------ 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/ApproxFunBase.jl b/src/ApproxFunBase.jl index c4cbc10d..4db590b6 100644 --- a/src/ApproxFunBase.jl +++ b/src/ApproxFunBase.jl @@ -147,6 +147,15 @@ function _IteratorSize(::Type{T}) where {T<:Tuple} any(x -> x isa Base.IsInfinite, s) ? Base.IsInfinite() : Base.HasLength() end +_IteratorSize(::Type{<:AbstractVector{<:InfRanges}}) = Base.IsInfinite() +_IteratorSize(::Type{<:AbstractVector{<:Vector{<:Integer}}}) = Base.HasLength() +_IteratorSize(::Type{<:AbstractVector{<:SVector{<:Integer}}}) = Base.HasLength() +_IteratorSize(::Type{<:AbstractVector}) = Base.SizeUnknown() + +tuple_to_SVector(t::Tuple) = SVector(t) +tuple_to_SVector(x) = x +_vcat_toabsvec(args...) = mapreduce(tuple_to_SVector, vcat, args) + include("LinearAlgebra/LinearAlgebra.jl") include("Fun.jl") include("onehotvector.jl") diff --git a/src/LinearAlgebra/helper.jl b/src/LinearAlgebra/helper.jl index 72d24601..b2ea6bc6 100644 --- a/src/LinearAlgebra/helper.jl +++ b/src/LinearAlgebra/helper.jl @@ -689,15 +689,13 @@ conv(x::AbstractFill, y::AbstractFill) = DSP.conv(x, y) # TODO: cache sums -struct BlockInterlacer{DMS<:Tuple{Vararg{AbstractVector{Int}}}} +struct BlockInterlacer{DMS} blocks::DMS end const TrivialInterlacer{d,Ax} = BlockInterlacer{<:NTuple{d,Ones{Int,1,Tuple{Ax}}}} -BlockInterlacer(v::AbstractVector) = BlockInterlacer(Tuple(v)) - eltype(::Type{<:BlockInterlacer}) = Tuple{Int,Int} dimensions(b::BlockInterlacer) = map(sum,b.blocks) @@ -724,10 +722,16 @@ function done(it::BlockInterlacer,st) return true end -iterate(it::BlockInterlacer) = +iterate(it::BlockInterlacer{<:Tuple}) = iterate(it, (1,1,ntuple(_ -> tuple(), length(it.blocks)), ntuple(zero,length(it.blocks)))) +iterate(it::BlockInterlacer{<:AbstractVector}) = + iterate(it, (1,1, [tuple() for _ in 1:length(it.blocks)], + [zero(i) for i in 1:length(it.blocks)])) + +_setindex(coll, v, ind) = (coll[ind] = v; coll) +_setindex(coll::Tuple, v, ind) = Base.setindex(coll, v, ind) function iterate(it::BlockInterlacer, (N,k,blkst,lngs)) done(it, (N,k,blkst,lngs)) && return nothing @@ -747,7 +751,7 @@ function iterate(it::BlockInterlacer, (N,k,blkst,lngs)) return iterate(it,(N+1,1,blkst,lngs)) end - lngs = Base.setindex(lngs, lngs[N]+1, N) + lngs = _setindex(lngs, lngs[N]+1, N) return (N,lngs[N]),(N,k+1,blkst,lngs) end diff --git a/src/Spaces/SumSpace.jl b/src/Spaces/SumSpace.jl index 37fe1c81..1b35e641 100644 --- a/src/Spaces/SumSpace.jl +++ b/src/Spaces/SumSpace.jl @@ -25,7 +25,7 @@ ncomponents(f::Fun) = ncomponents(space(f)) BlockInterlacer(sp::DirectSumSpace) = BlockInterlacer(map(blocklengths,sp.spaces)) interlacer(sp::DirectSumSpace) = BlockInterlacer(sp) -interlacer(sp::Space) = BlockInterlacer(tuple(blocklengths(sp))) +interlacer(sp::Space) = BlockInterlacer(blocklengths(sp)) function blocklengths(sp::DirectSumSpace) bl=map(blocklengths,components(sp)) @@ -50,6 +50,21 @@ end SumSpace(sp::Tuple) = SumSpace{typeof(sp),domaintype(first(sp)), mapreduce(rangetype,promote_type,sp)}(sp) +SumSpace(A::SumSpace,B::SumSpace) = SumSpace(tuple(A.spaces...,B.spaces...)) + +SumSpace(A::Space,B::SumSpace) = SumSpace(tuple(A,B.spaces...)) +SumSpace(A::SumSpace,B::Space) = SumSpace(tuple(A.spaces...,B)) +SumSpace(A::Space...) = SumSpace(A) +SumSpace(sp::AbstractArray) = SumSpace(tuple(sp...)) + +canonicalspace(A::SumSpace) = SumSpace(sort(collect(A.spaces))) + +# TODO: Fix this Hack +SumSpace(A::ConstantSpace{AnyDomain}, B::ConstantSpace{AnyDomain}) = error("Should not happen") +SumSpace(A::SumSpace, B::ConstantSpace{AnyDomain}) = SumSpace(A, setdomain(B, domain(A))) +SumSpace(B::ConstantSpace{AnyDomain}, A::SumSpace) = SumSpace(setdomain(B, domain(A)), A) +SumSpace(A::Space, B::ConstantSpace{AnyDomain}) = SumSpace(A, setdomain(B, domain(A))) +SumSpace(B::ConstantSpace{AnyDomain}, A::Space) = SumSpace(setdomain(B, domain(A)), A) struct PiecewiseSpace{SV,D<:UnionDomain,R} <: DirectSumSpace{SV,D,R} spaces::SV @@ -57,7 +72,7 @@ struct PiecewiseSpace{SV,D<:UnionDomain,R} <: DirectSumSpace{SV,D,R} new{SV,D,R}(tuple(map(typ->typ(dom),SV.parameters)...)) PiecewiseSpace{SV,D,R}(dom::UnionDomain) where {SV,D,R} = new{SV,D,R}(tuple(map((typ,dom)->typ(dom),SV.parameters,dom.domains)...)) - PiecewiseSpace{SV,D,R}(sp::Tuple) where {SV,D,R} = + PiecewiseSpace{SV,D,R}(sp::SV) where {SV,D,R} = new{SV,D,R}(sp) end @@ -65,34 +80,21 @@ function _PiecewiseSpace(sp) PiecewiseSpace{typeof(sp),typeof(UnionDomain(map(domain,sp))), mapreduce(rangetype,promote_type,sp)}(sp) end -function PiecewiseSpace(spin::Tuple) - sp=tuple(union(spin)...) # remove duplicates +function PiecewiseSpace(spacesin::Union{Tuple{Vararg{Space}}, AbstractVector{<:Space}}) + sp = union(spacesin) # remove duplicates _PiecewiseSpace(sp) end -PiecewiseSpace(spin::Set) = PiecewiseSpace(collect(spin)) - - +PiecewiseSpace(spacesin::Set) = PiecewiseSpace(collect(spacesin)) -for TYP in (:SumSpace,:PiecewiseSpace) - @eval begin - $TYP(A::$TYP,B::$TYP) = $TYP(tuple(A.spaces...,B.spaces...)) - - $TYP(A::Space,B::$TYP) = $TYP(tuple(A,B.spaces...)) - $TYP(A::$TYP,B::Space) = $TYP(tuple(A.spaces...,B)) - $TYP(A::Space...) = $TYP(A) - $TYP(sp::AbstractArray) = $TYP(tuple(sp...)) +PiecewiseSpace(A::PiecewiseSpace, B::PiecewiseSpace) = + PiecewiseSpace(_vcat_toabsvec(A.spaces, B.spaces)) - canonicalspace(A::$TYP) = $TYP(sort(collect(A.spaces))) - end -end +PiecewiseSpace(A::Space,B::PiecewiseSpace) = PiecewiseSpace(_vcat_toabsvec(A, B.spaces)) +PiecewiseSpace(A::PiecewiseSpace,B::Space) = PiecewiseSpace(_vcat_toabsvec(A.spaces, B)) +PiecewiseSpace(A::Space...) = PiecewiseSpace(A) -# TODO: Fix this Hack -SumSpace(A::ConstantSpace{AnyDomain}, B::ConstantSpace{AnyDomain}) = error("Should not happen") -SumSpace(A::SumSpace, B::ConstantSpace{AnyDomain}) = SumSpace(A, setdomain(B, domain(A))) -SumSpace(B::ConstantSpace{AnyDomain}, A::SumSpace) = SumSpace(setdomain(B, domain(A)), A) -SumSpace(A::Space, B::ConstantSpace{AnyDomain}) = SumSpace(A, setdomain(B, domain(A))) -SumSpace(B::ConstantSpace{AnyDomain}, A::Space) = SumSpace(setdomain(B, domain(A)), A) +canonicalspace(A::PiecewiseSpace) = PiecewiseSpace(sort(convert_vector_or_svector(A.spaces))) pieces(sp::PiecewiseSpace) = sp.spaces piece(s::Space,k) = pieces(s)[k] From 85cc9beac44f616cf98d172622a4633b45892593 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 23 Aug 2023 11:46:55 +0530 Subject: [PATCH 02/15] Bump version to v0.9.12 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 97fe7d9d..d2ca1fba 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ApproxFunBase" uuid = "fbd15aa5-315a-5a7d-a8a4-24992e37be05" -version = "0.9.11" +version = "0.9.12" [deps] AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" From 8d21de2b7c81fb849bf41185f120744c51acf4d8 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 23 Aug 2023 11:54:53 +0530 Subject: [PATCH 03/15] fix SVector type signature --- src/ApproxFunBase.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ApproxFunBase.jl b/src/ApproxFunBase.jl index 4db590b6..677ba1a5 100644 --- a/src/ApproxFunBase.jl +++ b/src/ApproxFunBase.jl @@ -148,8 +148,8 @@ function _IteratorSize(::Type{T}) where {T<:Tuple} end _IteratorSize(::Type{<:AbstractVector{<:InfRanges}}) = Base.IsInfinite() -_IteratorSize(::Type{<:AbstractVector{<:Vector{<:Integer}}}) = Base.HasLength() -_IteratorSize(::Type{<:AbstractVector{<:SVector{<:Integer}}}) = Base.HasLength() +_IteratorSize(::Type{<:AbstractVector{<:Union{Vector{<:Integer}, + SVector{<:Any,<:Integer}}}}) = Base.HasLength() _IteratorSize(::Type{<:AbstractVector}) = Base.SizeUnknown() tuple_to_SVector(t::Tuple) = SVector(t) From 7558d9c6a04e5400d6bfc71c38d49fa38890c5b1 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 23 Aug 2023 12:10:58 +0530 Subject: [PATCH 04/15] Tests for piecewisespace --- src/ApproxFunBase.jl | 2 ++ test/SpacesTest.jl | 12 ++++++++++++ test/runtests.jl | 15 +++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/ApproxFunBase.jl b/src/ApproxFunBase.jl index 677ba1a5..e329fa1e 100644 --- a/src/ApproxFunBase.jl +++ b/src/ApproxFunBase.jl @@ -147,6 +147,8 @@ function _IteratorSize(::Type{T}) where {T<:Tuple} any(x -> x isa Base.IsInfinite, s) ? Base.IsInfinite() : Base.HasLength() end +_IteratorSize(::Type{<:AbstractVector{<:AbstractFill{<:Any,<:Any, + <:Tuple{Vararg{InfRanges}}}}}) = Base.IsInfinite() _IteratorSize(::Type{<:AbstractVector{<:InfRanges}}) = Base.IsInfinite() _IteratorSize(::Type{<:AbstractVector{<:Union{Vector{<:Integer}, SVector{<:Any,<:Integer}}}}) = Base.HasLength() diff --git a/test/SpacesTest.jl b/test/SpacesTest.jl index 39690aba..0a66cb66 100644 --- a/test/SpacesTest.jl +++ b/test/SpacesTest.jl @@ -361,4 +361,16 @@ using LinearAlgebra @test length(A) == 0 @test ApproxFunBase.dimension(A) == 0 end + + @testset "PiecewiseSpace" begin + ps = PiecewiseSpace([PointSpace(1:2), PointSpace(3:4)]) + @test PiecewiseSpace(ps, ps) == ps + @test_broken PiecewiseSpace(ps, PointSpace(1:2)) == ps + @test_broken PiecewiseSpace(PointSpace(1:2), ps) == ps + d = domain(ps) + @test all(x -> x in d, 1:4) + @test all(x -> !(x in d), 5:6) + d2 = domain(PiecewiseSpace(ps, PointSpace(5:6))) + @test all(x -> x in d2, 5:6) + end end diff --git a/test/runtests.jl b/test/runtests.jl index 24d5911e..30a44039 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -87,6 +87,21 @@ end @test first(C, 10) == C[1:10] == B[1:10] == first(B, 10) @test C[2:10][1:2:end] == B[2:10][1:2:end] end + + a = [1,2] + sa = SVector(1,2) + f = Fill(2,∞) + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer((f, f))) == Base.IsInfinite() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer([f, f])) == Base.IsInfinite() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer((1:∞, 1:∞))) == Base.IsInfinite() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer([1:∞, 1:∞])) == Base.IsInfinite() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer([sa, sa])) == Base.HasLength() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer([a, a])) == Base.HasLength() + @test Base.IteratorSize(ApproxFunBase.BlockInterlacer((a, a))) == Base.HasLength() + + b1 = ApproxFunBase.BlockInterlacer([Fill(2,2), 1:2]) + b2 = ApproxFunBase.BlockInterlacer((Fill(2,2), 1:2)) + @test collect(b1) == collect(b2) end @testset "issue #94" begin From f4e0eff3a458ccd956dc851ade09e1077d73a560 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 23 Aug 2023 12:34:22 +0530 Subject: [PATCH 05/15] import StaticArrays in tests --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 30a44039..6e9da25b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,6 +12,7 @@ using Infinities using LinearAlgebra using Random using SpecialFunctions +using StaticArrays using Test @testset "Project quality" begin From e80cec5a08dfe60c82b293c7b1880c609419ec80 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 23 Aug 2023 12:39:42 +0530 Subject: [PATCH 06/15] Test Piecewise component add --- test/SpacesTest.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/SpacesTest.jl b/test/SpacesTest.jl index 0a66cb66..519773a0 100644 --- a/test/SpacesTest.jl +++ b/test/SpacesTest.jl @@ -365,12 +365,16 @@ using LinearAlgebra @testset "PiecewiseSpace" begin ps = PiecewiseSpace([PointSpace(1:2), PointSpace(3:4)]) @test PiecewiseSpace(ps, ps) == ps - @test_broken PiecewiseSpace(ps, PointSpace(1:2)) == ps - @test_broken PiecewiseSpace(PointSpace(1:2), ps) == ps + @test ApproxFunBase.canonicalspace(ps) == ps d = domain(ps) @test all(x -> x in d, 1:4) @test all(x -> !(x in d), 5:6) d2 = domain(PiecewiseSpace(ps, PointSpace(5:6))) @test all(x -> x in d2, 5:6) + d2 = domain(PiecewiseSpace(PointSpace(5:6), ps)) + @test all(x -> x in d2, 5:6) + + ps2 = PiecewiseSpace([PointSpace(3:4), PointSpace(1:2)]) + @test ApproxFunBase.canonicalspace(ps2) == ps end end From 8f2abbb25b3aa11b773453b5e6ea4f0ae7860c36 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 31 Aug 2026 23:03:17 +0400 Subject: [PATCH 07/15] Fix stray '=' in Project.toml uuid The merge commit 263f875 left a trailing '=' after the uuid value, making Project.toml invalid TOML. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DZeBMyvW7qYiDfC7Tkb1vT --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0ed530ee..2a4a4f68 100644 --- a/Project.toml +++ b/Project.toml @@ -1,5 +1,5 @@ name = "ApproxFunBase" -uuid = "fbd15aa5-315a-5a7d-a8a4-24992e37be05"= +uuid = "fbd15aa5-315a-5a7d-a8a4-24992e37be05" version = "0.9.37" [deps] From f7301416ee3956f066fbe88b03d054aa0ed941e5 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 31 Aug 2026 23:17:48 +0400 Subject: [PATCH 08/15] Restore tuple wrapper in interlacer(sp::Space) BlockInterlacer.blocks holds one blocklength sequence per component, as BlockInterlacer(sp::DirectSumSpace) shows. A bare Space is a single component, so it needs a 1-element outer collection; without the tuple, each individual block length was mistaken for a whole component. For an infinite-dimensional space this gave IteratorSize == SizeUnknown() instead of IsInfinite(), and iteration threw CanonicalIndexError from _setindex, since [zero(i) for i in 1:length(blocks)] over an infinite range is a lazy immutable vector. The tuple form also keeps the TrivialInterlacer fast path matching. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DZeBMyvW7qYiDfC7Tkb1vT --- src/Spaces/SumSpace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spaces/SumSpace.jl b/src/Spaces/SumSpace.jl index 7901314d..282c9742 100644 --- a/src/Spaces/SumSpace.jl +++ b/src/Spaces/SumSpace.jl @@ -25,7 +25,7 @@ ncomponents(f::Fun) = ncomponents(space(f)) BlockInterlacer(sp::DirectSumSpace) = BlockInterlacer(map(blocklengths,sp.spaces)) interlacer(sp::DirectSumSpace) = BlockInterlacer(sp) -interlacer(sp::Space) = BlockInterlacer(blocklengths(sp)) +interlacer(sp::Space) = BlockInterlacer(tuple(blocklengths(sp))) function blocklengths(sp::DirectSumSpace) bl=map(blocklengths,components(sp)) From 024ef09fdcb8e9a05e4f3b310c0c5286d038f461 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 31 Aug 2026 23:22:54 +0400 Subject: [PATCH 09/15] Test interlacer on a bare Space Regression test for interlacer(sp::Space) wrapping blocklengths in a 1-element collection. Covers both a finite space and an infinite- dimensional one, where the unwrapped version gave SizeUnknown() instead of IsInfinite() and threw CanonicalIndexError on iteration. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DZeBMyvW7qYiDfC7Tkb1vT --- test/runtests.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 7a2c49e3..92ade5cc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -102,6 +102,16 @@ end b1 = ApproxFunBase.BlockInterlacer([Fill(2,2), 1:2]) b2 = ApproxFunBase.BlockInterlacer((Fill(2,2), 1:2)) @test collect(b1) == collect(b2) + + # a bare Space is one component, so interlacer must wrap its + # blocklengths in a 1-element collection + ps = ApproxFunBase.PointSpace(1:3) + @test ApproxFunBase.interlacer(ps).blocks == (blocklengths(ps),) + cs = ApproxFunBase.ContinuousSpace(ApproxFunBase.PiecewiseSegment([1.0,2.0,3.0])) + itc = ApproxFunBase.interlacer(cs) + @test itc.blocks == (blocklengths(cs),) + @test Base.IteratorSize(itc) == Base.IsInfinite() + @test collect(Iterators.take(itc, 4)) == [(1,k) for k in 1:4] end @testset "issue #94" begin From 727c7574a0a9df9a55d7d05a27b45e4dedef26a3 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Tue, 1 Sep 2026 00:05:18 +0400 Subject: [PATCH 10/15] Test the SumSpace constructors and PiecewiseSpace(::Set) Splitting the `for TYP in (:SumSpace,:PiecewiseSpace)` @eval loop into separate definitions revealed that the SumSpace variants were never tested: they had previously shared source lines with the PiecewiseSpace ones, so the PiecewiseSpace tests made them look covered. Covers the two-argument constructors, the AbstractArray constructor, canonicalspace, and the ConstantSpace{AnyDomain} domain-inheriting hack, plus PiecewiseSpace(::Set). PointSpace has no hash matching its ==, so equal-valued Sets of spaces compare unequal; the Set test canonicalizes rather than comparing sets. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DZeBMyvW7qYiDfC7Tkb1vT --- test/SpacesTest.jl | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/SpacesTest.jl b/test/SpacesTest.jl index 82d29c2e..6102ec32 100644 --- a/test/SpacesTest.jl +++ b/test/SpacesTest.jl @@ -1,5 +1,5 @@ using ApproxFunBase -using ApproxFunBase: PointSpace, HeavisideSpace, PiecewiseSegment, dimension, SVector, checkpoints, AnyDomain +using ApproxFunBase: PointSpace, HeavisideSpace, PiecewiseSegment, dimension, SVector, checkpoints, AnyDomain, SumSpace using BandedMatrices: rowrange, colrange, BandedMatrix using DomainSets: Point using LinearAlgebra @@ -460,5 +460,29 @@ using Test ps2 = PiecewiseSpace([PointSpace(3:4), PointSpace(1:2)]) @test ApproxFunBase.canonicalspace(ps2) == ps + + # a Set has no order, so canonicalize before comparing + @test ApproxFunBase.canonicalspace( + PiecewiseSpace(Set([PointSpace(1:2), PointSpace(3:4)]))) == ps + end + + @testset "SumSpace" begin + a, b, c = PointSpace(1:2), PointSpace(3:4), PointSpace(5:6) + ss = SumSpace(a, b) + @test components(ss) == (a, b) + + @test components(SumSpace(ss, c)) == (a, b, c) + @test components(SumSpace(c, ss)) == (c, a, b) + @test components(SumSpace(ss, SumSpace(c, a))) == (a, b, c, a) + @test SumSpace([a, b]) == ss + + @test ApproxFunBase.canonicalspace(SumSpace(b, a)) == ApproxFunBase.canonicalspace(ss) + + # a ConstantSpace{AnyDomain} argument inherits the domain of the other space + @test domain(components(SumSpace(ss, ConstantSpace()))[end]) == domain(ss) + @test domain(components(SumSpace(a, ConstantSpace()))[end]) == domain(a) + @test domain(components(SumSpace(ConstantSpace(), ss))[1]) == domain(ss) + @test domain(components(SumSpace(ConstantSpace(), a))[1]) == domain(a) + @test_throws ErrorException SumSpace(ConstantSpace(), ConstantSpace()) end end From dae80bc1414c86638a34dc717918c24d8858054d Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Tue, 1 Sep 2026 23:01:45 +0400 Subject: [PATCH 11/15] Fix InterlaceOperator and perm for vector-backed spaces Once the pieces of a `PiecewiseSpace` may be stored in a `Vector`, the operator side has to cope with a `Diagonal` whose diagonal is a `Vector` rather than an `SVector`. Two places silently assumed the latter: - `convert(::Type{Operator{T}}, ::InterlaceOperator)` mapped over the `Diagonal` itself. `map` over a `Diagonal` also applies the function to the structural zeros, and the result of converting a `ZeroOperator{T,UnsetSpace,UnsetSpace}` cannot be stored back in a `Diagonal` of concrete operators. Map over the diagonal instead. - `blockbanded_interlace_convert!` relied on `map` densifying the `Diagonal`, which it does for an `SVector` diagonal (giving an `SMatrix`) but not for a `Vector` one. The preserved `Diagonal` then handed back plain zero `Matrix`es off the diagonal, whose `blocksize` is `(1,1)`, so the guard on the block index passed only for the first block and the rest were misplaced. Fill in the structural zeros explicitly. `perm` also had methods for two tuples and for two vectors but not for a mix, which `maxspace_rule` needs as soon as a tuple-backed and a vector-backed space meet. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M8nc53g1jq2WxsH6S4BS7Q --- src/Operators/banded/PermutationOperator.jl | 5 +++-- src/Operators/general/InterlaceOperator.jl | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Operators/banded/PermutationOperator.jl b/src/Operators/banded/PermutationOperator.jl index a4d35927..1899d42d 100644 --- a/src/Operators/banded/PermutationOperator.jl +++ b/src/Operators/banded/PermutationOperator.jl @@ -35,8 +35,9 @@ end # the permutation that rearranges a to be b multiplyperm(b,a) = Int[a[bk] for bk in b] -perm(a::Vector,b::Vector) = multiplyperm(invperm(sortperm(b)),sortperm(a)) -perm(a::Tuple,b::Tuple) = perm(collect(a),collect(b)) +# a and b may be stored either as tuples or as vectors, and the two may be mixed +perm(a::Union{Tuple,AbstractVector}, b::Union{Tuple,AbstractVector}) = + multiplyperm(invperm(sortperm(convert_vector(b))), sortperm(convert_vector(a))) struct NegateEven{T,DS,RS} <: Operator{T} diff --git a/src/Operators/general/InterlaceOperator.jl b/src/Operators/general/InterlaceOperator.jl index add6c514..b7c3fcfd 100644 --- a/src/Operators/general/InterlaceOperator.jl +++ b/src/Operators/general/InterlaceOperator.jl @@ -229,11 +229,21 @@ InterlaceOperator(ops::AbstractArray, ds=NoSpace, rs=ds) = InterlaceOperator(Array{Operator{promote_eltypeof(ops)}, ndims(ops)}(ops), ds, rs) +# `map` over a `Diagonal` runs the function on the structural zeros as well, which +# for operator eltypes either errors or silently drops the diagonal structure, +# depending on how the diagonal is stored. Map over the diagonal itself instead. +_mapops(f, ops) = map(f, ops) +_mapops(f, ops::Diagonal) = Diagonal(map(f, parent(ops))) + +# a dense array of operators, with the structural zeros of a `Diagonal` filled in +_denseops(ops) = ops +_denseops(ops::Diagonal) = [ops[k,j] for k in axes(ops,1), j in axes(ops,2)] + function convert(::Type{Operator{T}},S::InterlaceOperator) where T if T == eltype(S) S else - ops = map(x -> convert(Operator{T},x), S.ops) + ops = _mapops(x -> convert(Operator{T},x), S.ops) InterlaceOperator(ops,domainspace(S),rangespace(S), S.domaininterlacer,S.rangeinterlacer,S.bandwidths, S.blockbandwidths, S.israggedbelow) @@ -428,7 +438,7 @@ function blockbanded_interlace_convert!(S,ret) KR_size = Block.(Int(first(KR)):min(Int(last(KR)),blocksize(op,1))) JR_size = Block.(Int(first(JR)):min(Int(last(JR)),blocksize(op,2))) BlockBandedMatrix(view(op, KR_size, JR_size)) - end, parent(S).ops) + end, _denseops(parent(S).ops)) for J=blockaxes(ret,2),K=blockcolrange(ret,J) Bs=view(ret,K,J) From f9b72af942ed2cef1fe3410ab9bf746c05768996 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Tue, 1 Sep 2026 23:02:01 +0400 Subject: [PATCH 12/15] Preserve the container of the pieces in PiecewiseSpace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constructing a `PiecewiseSpace` funnelled every input through `union(spacesin)`, which always returns a `Vector`, so a tuple of spaces became vector-backed. That flipped every construction path, not just the ones that are genuinely of runtime length: - `PiecewiseSpace(Chebyshev.(components(d)))` for `d = Segment ∪ Segment`, `A ∪ B`, `PiecewiseSpace(A, B)` and `Space(UnionDomain(a, b))` all start from a tuple. - only `PiecewiseSegment`, and hence `ContinuousSpace`, has a genuinely runtime number of pieces. Collapsing a tuple into a vector costs more inference than it gains. `PiecewiseSpace` exists to hold a different space per piece, and a vector of mixed pieces has an abstract eltype, so `component(f, k)` inferred as `Fun` rather than as a small union of concrete `Fun` types. Keep the container the caller supplied instead: a tuple stays a tuple, so the number of pieces and the type of each stay in the type, and a vector stays a vector, so `canonicalspace(::ContinuousSpace)` is concretely inferred. Duplicates are rare -- `union(::Space, ::Space)` already short-circuits equal spaces before a `PiecewiseSpace` is built -- so a tuple falls back to the vector `union` returns only when a piece was actually removed. This also keeps `SV` unchanged for the tuple-backed spaces that downstream packages dispatch on. Two consequences of the two storages now coexisting: - `spacescompatible` for direct sums required both arguments to have the same type, so a tuple-backed and a vector-backed `PiecewiseSpace` would silently compare as incompatible. Compare the pieces instead, after checking the two are the same kind of sum. - `promote_rule` for a `Fun` over a `PiecewiseSpace` read the per-piece types out of `SV.parameters`, which for a `Vector` yields its eltype and its dimension. Give the vector case its own method. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M8nc53g1jq2WxsH6S4BS7Q --- src/ApproxFunBase.jl | 11 ++++++ src/Spaces/ProductSpaceOperators.jl | 6 +++- src/Spaces/SumSpace.jl | 53 ++++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/ApproxFunBase.jl b/src/ApproxFunBase.jl index f1ef48a3..55eaaae8 100644 --- a/src/ApproxFunBase.jl +++ b/src/ApproxFunBase.jl @@ -173,6 +173,17 @@ tuple_to_SVector(t::Tuple) = SVector(t) tuple_to_SVector(x) = x _vcat_toabsvec(args...) = mapreduce(tuple_to_SVector, vcat, args) +# Concatenation that preserves how the pieces are stored. Each argument is either a +# container of spaces or a single space. Tuples stay tuples, so the number of pieces and +# the type of each one remain in the type; as soon as a runtime-length vector is involved +# the result is a vector. +_ascontainer(x::Tuple) = x +_ascontainer(x::AbstractVector) = x +_ascontainer(x) = (x,) +_vcat_containers(a::Tuple, b::Tuple) = (a..., b...) +_vcat_containers(a, b) = _vcat_toabsvec(a, b) +_vcat_preservecontainer(a, b) = _vcat_containers(_ascontainer(a), _ascontainer(b)) + include("LinearAlgebra/LinearAlgebra.jl") include("Fun.jl") include("Domains/Domains.jl") diff --git a/src/Spaces/ProductSpaceOperators.jl b/src/Spaces/ProductSpaceOperators.jl index 4226a01b..5489a76a 100644 --- a/src/Spaces/ProductSpaceOperators.jl +++ b/src/Spaces/ProductSpaceOperators.jl @@ -187,6 +187,10 @@ end +# combine the pieces of two direct sums one by one, keeping tuples as tuples +_mapspaces(f,S1::Tuple,S2::Tuple) = map(f,S1,S2) +_mapspaces(f,S1,S2) = [f(S1[k],S2[k]) for k=1:length(S1)] + for (OPrule,OP) in ((:conversion_rule,:conversion_type),(:maxspace_rule,:maxspace), (:union_rule,:union)) for TYP in (:SumSpace,:PiecewiseSpace) @@ -202,7 +206,7 @@ for (OPrule,OP) in ((:conversion_rule,:conversion_type),(:maxspace_rule,:maxspac # we can just map down # $TYP(map($OP,S1.spaces,S2.spaces)) # this is commented out due to Issue #13261 - newspaces = [$OP(S1[k],S2[k]) for k=1:length(S1)] + newspaces = _mapspaces($OP,S1,S2) if any(b->b==NoSpace(),newspaces) NoSpace() else diff --git a/src/Spaces/SumSpace.jl b/src/Spaces/SumSpace.jl index 282c9742..b5d9268e 100644 --- a/src/Spaces/SumSpace.jl +++ b/src/Spaces/SumSpace.jl @@ -80,21 +80,37 @@ function _PiecewiseSpace(sp) PiecewiseSpace{typeof(sp),typeof(UnionDomain(map(domain,sp))), mapreduce(rangetype,promote_type,sp)}(sp) end +# `union` removes duplicates, but it always returns a `Vector`, which would drop the +# number of pieces and their individual types from the type of a tuple of spaces. +# Duplicates are rare here -- `union(::Space, ::Space)` already short-circuits equal +# spaces before a `PiecewiseSpace` is built -- so keep the tuple when nothing was +# removed, and only fall back to the vector that `union` returned when it was. +_uniquespaces(sp::AbstractVector) = union(sp) +function _uniquespaces(sp::Tuple) + u = union(sp) + length(u) == length(sp) ? sp : u +end + function PiecewiseSpace(spacesin::Union{Tuple{Vararg{Space}}, AbstractVector{<:Space}}) - sp = union(spacesin) # remove duplicates - _PiecewiseSpace(sp) + _PiecewiseSpace(_uniquespaces(spacesin)) end PiecewiseSpace(spacesin::Set) = PiecewiseSpace(collect(spacesin)) PiecewiseSpace(A::PiecewiseSpace, B::PiecewiseSpace) = - PiecewiseSpace(_vcat_toabsvec(A.spaces, B.spaces)) + PiecewiseSpace(_vcat_preservecontainer(A.spaces, B.spaces)) -PiecewiseSpace(A::Space,B::PiecewiseSpace) = PiecewiseSpace(_vcat_toabsvec(A, B.spaces)) -PiecewiseSpace(A::PiecewiseSpace,B::Space) = PiecewiseSpace(_vcat_toabsvec(A.spaces, B)) +PiecewiseSpace(A::Space,B::PiecewiseSpace) = PiecewiseSpace(_vcat_preservecontainer(A, B.spaces)) +PiecewiseSpace(A::PiecewiseSpace,B::Space) = PiecewiseSpace(_vcat_preservecontainer(A.spaces, B)) PiecewiseSpace(A::Space...) = PiecewiseSpace(A) -canonicalspace(A::PiecewiseSpace) = PiecewiseSpace(sort(convert_vector_or_svector(A.spaces))) +# `sort` of a tuple is only defined if every piece has the same type, so fall back to +# permuting by a sorted vector for a mix of spaces. Either way a tuple stays a tuple. +_sortspaces(sp::AbstractVector) = sort(sp) +_sortspaces(sp::Tuple{T,Vararg{T}}) where {T} = sort(sp) +_sortspaces(sp::Tuple) = sp[sortperm(convert_vector(sp))] + +canonicalspace(A::PiecewiseSpace) = PiecewiseSpace(_sortspaces(A.spaces)) pieces(sp::PiecewiseSpace) = sp.spaces piece(s::Space,k) = pieces(s)[k] @@ -114,8 +130,16 @@ setdomain(A::PiecewiseSpace,d::UnionDomain) = -function spacescompatible(A::S,B::S) where S<:DirectSumSpace - if ncomponents(A) != ncomponents(B) +# Two direct sums are compatible if they are the same kind of sum and their pieces +# match up. The pieces may be stored as a tuple in one and as a vector in the other, +# so this must not require the two spaces to have the same type. +_directsumkind(::SumSpace) = SumSpace +_directsumkind(::PiecewiseSpace) = PiecewiseSpace + +function spacescompatible(A::DirectSumSpace,B::DirectSumSpace) + if _directsumkind(A) !== _directsumkind(B) + false + elseif ncomponents(A) != ncomponents(B) false else ret=true @@ -142,7 +166,18 @@ end Base.promote_rule(::Type{Fun{SumSpace{SV,D,R}}},::Type{T}) where {SV,D,R,T<:Number} = promote_rule(VFun{SumSpace{SV,D,R},Float64},T) -function Base.promote_rule(::Type{Fun{PiecewiseSpace{SV,D,R},V,VV}},::Type{T}) where {SV,D,R,V,VV,T<:Number} +# pieces stored in a vector: there is no per-piece type to promote, only the element type +function Base.promote_rule(::Type{Fun{PiecewiseSpace{SV,D,R},V,VV}}, + ::Type{T}) where {SV<:AbstractVector,D,R,V,VV,T<:Number} + newf = promote_type(VFun{eltype(SV),V},T) + if newf == Fun + Fun + else + VFun{PiecewiseSpace{Vector{newf.parameters[1]},D,R},promote_type(V,T)} + end +end + +function Base.promote_rule(::Type{Fun{PiecewiseSpace{SV,D,R},V,VV}},::Type{T}) where {SV<:Tuple,D,R,V,VV,T<:Number} # if any doesn't support promoting, just leave unpromoted newfsp=map(s->promote_type(VFun{s,V},T),SV.parameters) From f6b8124d1ea7a13c8df5f1dd161b88da9f9cad81 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 2 Sep 2026 01:27:05 +0400 Subject: [PATCH 13/15] Compare the pieces, not the containers, in the direct sum rules `conversion_rule`, `maxspace_rule` and `union_rule` compared the canonicalised pieces of the two spaces with `cs1 == cs2`. Now that the pieces of a `PiecewiseSpace` may be stored either in a tuple or in a vector, the two sides may use different containers, and a tuple never compares equal to a vector however its elements compare. The comparison was therefore false for two spaces with matching pieces, the rule fell through to the branch that sorts the first space, and `perm` returned the identity permutation, so the rule called itself with its arguments unchanged and recursed until the stack overflowed. The mixed pair is ordinary rather than exotic: `Space(UnionDomain(a, b, c))` is tuple-backed while a range space built with `PiecewiseSpace(map(rangespace, B))` is vector-backed, and solving an ODE on three pieces brings the two together. Two pieces were spared only because the `length(S1) == length(S2) == 2` branch above caught them first. Compare the pieces one by one instead, and refuse to recurse on a permutation that reorders nothing so that a similar mistake cannot hang again. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BHnCGhPiCzGdqz98Z6pNVZ --- src/Spaces/ProductSpaceOperators.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Spaces/ProductSpaceOperators.jl b/src/Spaces/ProductSpaceOperators.jl index 5489a76a..a0895f2e 100644 --- a/src/Spaces/ProductSpaceOperators.jl +++ b/src/Spaces/ProductSpaceOperators.jl @@ -191,6 +191,10 @@ end _mapspaces(f,S1::Tuple,S2::Tuple) = map(f,S1,S2) _mapspaces(f,S1,S2) = [f(S1[k],S2[k]) for k=1:length(S1)] +# The pieces of the two spaces may be stored in different containers, and a tuple never +# compares equal to a vector however its elements compare, so compare the pieces. +_samespaces(S1,S2) = length(S1) == length(S2) && all(((a,b),) -> a == b, zip(S1,S2)) + for (OPrule,OP) in ((:conversion_rule,:conversion_type),(:maxspace_rule,:maxspace), (:union_rule,:union)) for TYP in (:SumSpace,:PiecewiseSpace) @@ -202,7 +206,7 @@ for (OPrule,OP) in ((:conversion_rule,:conversion_type),(:maxspace_rule,:maxspac NoSpace() elseif canonicalspace(S1sp) == canonicalspace(S2sp) # this sorts S1 and S2 S1sp ≤ S2sp ? S1sp : S2sp # choose smallest space by sorting - elseif cs1 == cs2 + elseif _samespaces(cs1,cs2) # we can just map down # $TYP(map($OP,S1.spaces,S2.spaces)) # this is commented out due to Issue #13261 @@ -215,7 +219,8 @@ for (OPrule,OP) in ((:conversion_rule,:conversion_type),(:maxspace_rule,:maxspac elseif sort(collect(cs1)) == sort(collect(cs2)) # sort S1 p=perm(cs1,cs2) - $OP($TYP(S1[p]),S2sp) + # a permutation that reorders nothing would recurse forever + p == 1:length(S1) ? NoSpace() : $OP($TYP(S1[p]),S2sp) elseif length(S1) == length(S2) == 2 && $OP(S1[1],S2[1]) != NoSpace() && $OP(S1[2],S2[2]) != NoSpace() From fdec32969eb1ac17e182573e27da67a77d10fafd Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 2 Sep 2026 11:01:30 +0400 Subject: [PATCH 14/15] Test how the pieces of a PiecewiseSpace are stored The existing tests built every piecewise space from a vector, so none of the code that keeps a tuple of pieces a tuple was reached, and neither was the code that has to cope with one space holding its pieces in a tuple while the other holds them in a vector. The infinite recursion in the direct sum rules was found by a downstream ODE solve rather than here. Cover both: that each constructor keeps the container it was given, that concatenation keeps a tuple only while no vector is involved, that duplicates are still removed, that a tuple of pieces of differing types can still be sorted, and that pieces held in a vector promote. For the mixed case, `SubSpace` canonicalises to the space it is taken from, which gives two spaces that differ while their pieces canonicalise to the same thing -- the shape that recursed. Three pieces, because two are caught by a special case before the offending branch. Reverting either half of the fix turns `maxspace(pt, pv)` into a `StackOverflowError`, and the permuted variant reaches `perm` with a tuple on one side and a vector on the other, which nothing here reached before. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BHnCGhPiCzGdqz98Z6pNVZ --- test/SpacesTest.jl | 71 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/test/SpacesTest.jl b/test/SpacesTest.jl index 6102ec32..1a717e75 100644 --- a/test/SpacesTest.jl +++ b/test/SpacesTest.jl @@ -1,5 +1,5 @@ using ApproxFunBase -using ApproxFunBase: PointSpace, HeavisideSpace, PiecewiseSegment, dimension, SVector, checkpoints, AnyDomain, SumSpace +using ApproxFunBase: PointSpace, HeavisideSpace, PiecewiseSegment, dimension, SVector, checkpoints, AnyDomain, SumSpace, SubSpace using BandedMatrices: rowrange, colrange, BandedMatrix using DomainSets: Point using LinearAlgebra @@ -466,6 +466,75 @@ using Test PiecewiseSpace(Set([PointSpace(1:2), PointSpace(3:4)]))) == ps end + @testset "PiecewiseSpace pieces container" begin + a, b, c, d = PointSpace(1:2), PointSpace(3:4), PointSpace(5:6), PointSpace(7:8) + + # the pieces are kept in the container they were given in, so that a tuple keeps + # the number of pieces and the type of each one in the type of the space + @test PiecewiseSpace(a, b).spaces isa Tuple + @test PiecewiseSpace((a, b)).spaces isa Tuple + @test PiecewiseSpace([a, b]).spaces isa Vector + @test PiecewiseSpace(Set([a, b])).spaces isa Vector + + # concatenation keeps a tuple only as long as no vector is involved + ab, dc = PiecewiseSpace(a, b), PiecewiseSpace(d, c) + @test PiecewiseSpace(ab, dc).spaces isa Tuple + @test components(PiecewiseSpace(ab, dc)) == (a, b, d, c) + @test PiecewiseSpace(c, ab).spaces isa Tuple + @test PiecewiseSpace(ab, c).spaces isa Tuple + @test PiecewiseSpace(PiecewiseSpace([a, b]), dc).spaces isa Vector + @test PiecewiseSpace(ab, PiecewiseSpace([c, d])).spaces isa Vector + + # duplicates are still removed, at the cost of the tuple + @test ApproxFunBase.ncomponents(PiecewiseSpace(a, a)) == 1 + + # `sort` of a tuple is only defined if every piece has the same type + sb = SubSpace(b, 1:1) + het = ApproxFunBase._PiecewiseSpace((sb, a)) + @test ApproxFunBase.canonicalspace(het).spaces isa Tuple + @test components(ApproxFunBase.canonicalspace(het)) == (a, sb) + + # pieces held in a vector have no per-piece type to promote + @test promote_type(Fun{typeof(PiecewiseSpace([a, b])),Float64,Vector{Float64}}, Int) == Fun + end + + @testset "PiecewiseSpace pieces in different containers" begin + # A tuple never compares equal to a vector however its elements compare, so the + # rules below have to compare the pieces and not the containers holding them. + # Comparing the containers made `maxspace` fall through to the branch that sorts + # the first space, where the permutation reorders nothing, so the rule called + # itself unchanged and recursed until the stack overflowed. + a, b, c = PointSpace(1:2), PointSpace(3:4), PointSpace(5:6) + # `SubSpace` canonicalises to the space it is taken from, so the two spaces below + # differ but their pieces canonicalise to the same thing. Three pieces, because + # two are caught by a special case before the offending branch is reached. + pt = PiecewiseSpace(SubSpace(a, 1:1), SubSpace(b, 1:1), SubSpace(c, 1:1)) + pv = PiecewiseSpace([a, b, c]) + @test pt.spaces isa Tuple + @test pv.spaces isa Vector + @test ApproxFunBase.canonicalspace(pt) != ApproxFunBase.canonicalspace(pv) + @test collect(map(ApproxFunBase.canonicalspace, components(pt))) == + collect(map(ApproxFunBase.canonicalspace, components(pv))) + + @test ApproxFunBase.maxspace(pt, pv) == pv + @test union(pt, pv) == pv + @test ApproxFunBase.conversion_type(pt, pv) == pt + + # with the pieces in a different order the rule has to permute the first space, + # so `perm` is handed a tuple and a vector + po = PiecewiseSpace(SubSpace(b, 1:1), SubSpace(a, 1:1), SubSpace(c, 1:1)) + @test ApproxFunBase.perm(map(ApproxFunBase.canonicalspace, components(po)), + map(ApproxFunBase.canonicalspace, components(pv))) == [2, 1, 3] + @test ApproxFunBase.maxspace(po, pv) == pv + @test union(po, pv) == pv + + # the same pieces held in different containers describe the same space + tup, vec = PiecewiseSpace(a, b), PiecewiseSpace([a, b]) + @test ApproxFunBase.spacescompatible(tup, vec) + @test tup == vec + @test ApproxFunBase.canonicalspace(tup) == ApproxFunBase.canonicalspace(vec) + end + @testset "SumSpace" begin a, b, c = PointSpace(1:2), PointSpace(3:4), PointSpace(5:6) ss = SumSpace(a, b) From 7127500d05cf007e92a8ffa5a0cc7bcaf1d53e10 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 2 Sep 2026 12:11:36 +0400 Subject: [PATCH 15/15] Sort the pieces of a tuple without `sort(::Tuple)` `sort` of a tuple was only added to Base after the oldest Julia this package supports, so `canonicalspace` of a `PiecewiseSpace` holding its pieces in a tuple raised a `MethodError` on lts and on the minimum supported version. Sort a vector of the pieces and permute the tuple by the result instead. This replaces both previous methods rather than just the one guarded for a common piece type, and infers better than the method it replaces: the pieces keep their own types, so a tuple of differing pieces now sorts to a tuple of a concrete union rather than to `Tuple{Vararg{...}}`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BHnCGhPiCzGdqz98Z6pNVZ --- src/Spaces/SumSpace.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Spaces/SumSpace.jl b/src/Spaces/SumSpace.jl index b5d9268e..dd228311 100644 --- a/src/Spaces/SumSpace.jl +++ b/src/Spaces/SumSpace.jl @@ -104,11 +104,14 @@ PiecewiseSpace(A::Space,B::PiecewiseSpace) = PiecewiseSpace(_vcat_preservecontai PiecewiseSpace(A::PiecewiseSpace,B::Space) = PiecewiseSpace(_vcat_preservecontainer(A.spaces, B)) PiecewiseSpace(A::Space...) = PiecewiseSpace(A) -# `sort` of a tuple is only defined if every piece has the same type, so fall back to -# permuting by a sorted vector for a mix of spaces. Either way a tuple stays a tuple. +# `sort` of a tuple needs a newer Julia than this package supports, and even there it is +# only defined if every piece has the same type. Sort a vector of the pieces and permute +# the tuple by the result instead, so that a tuple stays a tuple of the same length. _sortspaces(sp::AbstractVector) = sort(sp) -_sortspaces(sp::Tuple{T,Vararg{T}}) where {T} = sort(sp) -_sortspaces(sp::Tuple) = sp[sortperm(convert_vector(sp))] +function _sortspaces(sp::Tuple) + p = sortperm(convert_vector(sp)) + ntuple(i -> sp[p[i]], Val(length(sp))) +end canonicalspace(A::PiecewiseSpace) = PiecewiseSpace(_sortspaces(A.spaces))