Repository: QuantumKitHub/MatrixAlgebraKit.jl
Pull request #18: Truncation composition
Author: login: mtfishman
Created: 2025-04-09T16:24:38Z | Merged: 2025-04-17T15:26:26Z

Same as #15 with a cleaner git history.


Diff:
diff --git a/Project.toml b/Project.toml
index 78702565d..254377c9c 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "MatrixAlgebraKit"
 uuid = "6c742aac-3347-4629-af66-fc926824e5e4"
 authors = ["Jutho <jutho.haegeman@ugent.be> and contributors"]
-version = "0.1.1"
+version = "0.1.2"
 
 [deps]
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
diff --git a/src/implementations/truncation.jl b/src/implementations/truncation.jl
index f135df106..6088ed7ec 100644
--- a/src/implementations/truncation.jl
+++ b/src/implementations/truncation.jl
@@ -11,10 +11,17 @@ function TruncationStrategy(; atol=nothing, rtol=nothing, maxrank=nothing)
     if isnothing(maxrank) && isnothing(atol) && isnothing(rtol)
         return NoTruncation()
     elseif isnothing(maxrank)
-        @assert isnothing(rtol) "TODO: rtol"
-        return trunctol(atol)
+        atol = @something atol 0
+        rtol = @something rtol 0
+        return TruncationKeepAbove(atol, rtol)
     else
-        return truncrank(maxrank)
+        if isnothing(atol) && isnothing(rtol)
+            return truncrank(maxrank)
+        else
+            atol = @something atol 0
+            rtol = @something rtol 0
+            return truncrank(maxrank) & TruncationKeepAbove(atol, rtol)
+        end
     end
 end
 
@@ -82,6 +89,28 @@ Truncation strategy to discard the values that are larger than `atol` in absolut
 """
 truncabove(atol) = TruncationKeepFiltered(≤(atol) ∘ abs)
 
+"""
+    TruncationIntersection(trunc1::TruncationStrategy, trunc2::TruncationStrategy)
+
+Compose two truncation strategies, keeping values common between the two strategies.
+"""
+struct TruncationIntersection{T<:Tuple{Vararg{TruncationStrategy}}} <:
+       TruncationStrategy
+    components::T
+end
+function Base.:&(trunc1::TruncationStrategy, trunc2::TruncationStrategy)
+    return TruncationIntersection((trunc1, trunc2))
+end
+function Base.:&(trunc1::TruncationIntersection, trunc2::TruncationIntersection)
+    return TruncationIntersection((trunc1.components..., trunc2.components...))
+end
+function Base.:&(trunc1::TruncationIntersection, trunc2::TruncationStrategy)
+    return TruncationIntersection((trunc1.components..., trunc2))
+end
+function Base.:&(trunc1::TruncationStrategy, trunc2::TruncationIntersection)
+    return TruncationIntersection((trunc1, trunc2.components...))
+end
+
 # truncate!
 # ---------
 # Generic implementation: `findtruncated` followed by indexing
@@ -147,6 +176,11 @@ function findtruncated(values::AbstractVector, strategy::TruncationKeepAbove)
     return 1:i
 end
 
+function findtruncated(values::AbstractVector, strategy::TruncationIntersection)
+    inds = map(Base.Fix1(findtruncated, values), strategy.components)
+    return intersect(inds...)
+end
+
 """
     TruncatedAlgorithm(alg::AbstractAlgorithm, trunc::TruncationAlgorithm)
 
diff --git a/test/runtests.jl b/test/runtests.jl
index 541c7da88..7ad7c2167 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,5 +1,8 @@
 using SafeTestsets
 
+@safetestset "Truncate" begin
+    include("truncate.jl")
+end
 @safetestset "QR / LQ Decomposition" begin
     include("qr.jl")
     include("lq.jl")
diff --git a/test/svd.jl b/test/svd.jl
index b92c2e45c..e40a69e92 100644
--- a/test/svd.jl
+++ b/test/svd.jl
@@ -3,7 +3,7 @@ using Test
 using TestExtras
 using StableRNGs
 using LinearAlgebra: LinearAlgebra, Diagonal, I, isposdef
-using MatrixAlgebraKit: diagview
+using MatrixAlgebraKit: TruncationKeepAbove, diagview
 
 @testset "svd_compact! for T = $T" for T in (Float32, Float64, ComplexF32, ComplexF64)
     rng = StableRNG(123)
@@ -115,3 +115,33 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        for trunc_fun in ((rtol, maxrank) -> (; rtol, maxrank),
+                          (rtol, maxrank) -> truncrank(maxrank) & TruncationKeepAbove(0, rtol))
+            U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=trunc_fun(0.2, 1))
+            @test length(S1.diag) == 1
+            @test S1.diag ≈ S.diag[1:1] rtol = sqrt(eps(real(T)))
+
+            U2, S2, V2ᴴ = svd_trunc(A; alg, trunc=trunc_fun(0.2, 3))
+            @test length(S2.diag) == 2
+            @test S2.diag ≈ S.diag[1:2] rtol = sqrt(eps(real(T)))
+        end
+    end
+end
diff --git a/test/truncate.jl b/test/truncate.jl
new file mode 100644
index 000000000..5004a39d1
--- /dev/null
+++ b/test/truncate.jl
@@ -0,0 +1,29 @@
+using MatrixAlgebraKit
+using Test
+using TestExtras
+using MatrixAlgebraKit: NoTruncation, TruncationIntersection, TruncationKeepAbove,
+                        TruncationStrategy
+
+@testset "truncate" begin
+    trunc = @constinferred TruncationStrategy()
+    @test trunc isa NoTruncation
+
+    trunc = @constinferred TruncationStrategy(; atol=1e-2, rtol=1e-3)
+    @test trunc isa TruncationKeepAbove
+    @test trunc == TruncationKeepAbove(1e-2, 1e-3)
+    @test trunc.atol == 1e-2
+    @test trunc.rtol == 1e-3
+
+    trunc = @constinferred TruncationStrategy(; maxrank=10)
+    @test trunc isa TruncationKeepSorted
+    @test trunc == truncrank(10)
+    @test trunc.howmany == 10
+    @test trunc.sortby == abs
+    @test trunc.rev == true
+
+    trunc = @constinferred TruncationStrategy(; atol=1e-2, rtol=1e-3, maxrank=10)
+    @test trunc isa TruncationIntersection
+    @test trunc == truncrank(10) & TruncationKeepAbove(1e-2, 1e-3)
+    @test trunc.components[1] == truncrank(10)
+    @test trunc.components[2] == TruncationKeepAbove(1e-2, 1e-3)
+end


Reviews:
user: login: lkdvos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:45:46Z
user: login: mtfishman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T17:52:54Z
user: login: lkdvos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T18:12:47Z
user: login: mtfishman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T18:13:52Z
user: login: mtfishman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T18:16:17Z
user: login: lkdvos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T18:52:41Z
user: login: mtfishman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T20:29:33Z
user: login: mtfishman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:14:56Z
user: login: lkdvos
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T13:20:16Z
user: login: Jutho
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T23:16:06Z

Review comments:
diff_hunk: @@ -115,3 +115,30 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
path: test/svd.jl
user: login: lkdvos
body: Could you also include a test for the `&` syntax here or below?
created_at: 2025-04-09T17:40:27Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,7 @@
 name = "MatrixAlgebraKit"
 uuid = "6c742aac-3347-4629-af66-fc926824e5e4"
 authors = ["Jutho <jutho.haegeman@ugent.be> and contributors"]
-version = "0.1.1"
+version = "0.1.2"
path: Project.toml
user: login: lkdvos
body: I think this is correct, but just keep in mind that we typically only bump the version when we actually register, so the assumption that this PR doesn't introduce breaking changes therefore this update is not breaking doesn't always hold.

In principle we've added the pullback support since v0.1.1, which is a rather substantial amount of code, which might warrant increasing the version.
Definitely curious to hear your opinion on this, the v0.x semver rules are always a bit tricky to me.
created_at: 2025-04-09T17:45:40Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,7 @@
 name = "MatrixAlgebraKit"
 uuid = "6c742aac-3347-4629-af66-fc926824e5e4"
 authors = ["Jutho <jutho.haegeman@ugent.be> and contributors"]
-version = "0.1.1"
+version = "0.1.2"
path: Project.toml
user: login: mtfishman
body: Got it, thanks for the heads up. Would you prefer that I don't bump the version for now? This kind of thing is why I like to bias towards making releases after every PR, so there isn't a question about when to make a release and features become available right away.

I usually just follow the rule that I bump `0.x.y` to `0.x.y+1` if it isn't breaking (so that includes adding new features), and then `0.x.y` to `0.x+1.0` if it is breaking. If a package doesn't have many dependents I don't think it really matters but once there are a decent number of dependents, minimizing the number of breaking releases is obviously friendlier.
created_at: 2025-04-09T17:52:53Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +1,7 @@
 name = "MatrixAlgebraKit"
 uuid = "6c742aac-3347-4629-af66-fc926824e5e4"
 authors = ["Jutho <jutho.haegeman@ugent.be> and contributors"]
-version = "0.1.1"
+version = "0.1.2"
path: Project.toml
user: login: lkdvos
body: I'm definitely okay with bumping it here, just wanted to give you the heads up because I know the workflow is a bit different :)
created_at: 2025-04-09T18:12:47Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,7 @@
 name = "MatrixAlgebraKit"
 uuid = "6c742aac-3347-4629-af66-fc926824e5e4"
 authors = ["Jutho <jutho.haegeman@ugent.be> and contributors"]
-version = "0.1.1"
+version = "0.1.2"
path: Project.toml
user: login: mtfishman
body: Yes, definitely good to get on the same page about that!
created_at: 2025-04-09T18:13:51Z
author_association: COLLABORATOR
diff_hunk: @@ -115,3 +115,30 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
path: test/svd.jl
user: login: mtfishman
body: I've added tests explicitly using and directly testing some of the truncation objects, I realize some of that may be internal and may get changed so let me know what you think.
created_at: 2025-04-09T18:16:16Z
author_association: COLLABORATOR
diff_hunk: @@ -115,3 +115,35 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        for (rtol, maxrank) in ((0.2, 1), (0.2, 3))
+            for trunc in ((; rtol, maxrank),
+                          truncrank(maxrank) & TruncationKeepAbove(0, rtol))
+                U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
path: test/svd.jl
user: login: lkdvos
body: ```suggestion
                U1, S1, V1ᴴ = svd_trunc(A; alg, trunc)
```
created_at: 2025-04-09T18:51:44Z
author_association: MEMBER
diff_hunk: @@ -115,3 +115,35 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        for (rtol, maxrank) in ((0.2, 1), (0.2, 3))
+            for trunc in ((; rtol, maxrank),
+                          truncrank(maxrank) & TruncationKeepAbove(0, rtol))
+                U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
+                @test length(S1.diag) == 1
+                @test S1.diag ≈ S.diag[1:1] rtol = sqrt(eps(real(T)))
+
+                U2, S2, V2ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=3))
path: test/svd.jl
user: login: lkdvos
body: ```suggestion
                U2, S2, V2ᴴ = svd_trunc(A; alg, trunc)
```
created_at: 2025-04-09T18:51:54Z
author_association: MEMBER
diff_hunk: @@ -115,3 +115,35 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        for (rtol, maxrank) in ((0.2, 1), (0.2, 3))
+            for trunc in ((; rtol, maxrank),
+                          truncrank(maxrank) & TruncationKeepAbove(0, rtol))
+                U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
+                @test length(S1.diag) == 1
+                @test S1.diag ≈ S.diag[1:1] rtol = sqrt(eps(real(T)))
+
+                U2, S2, V2ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=3))
path: test/svd.jl
user: login: mtfishman
body: Whoops, good catch. Also that logic was totally off. I should have written some tests for my tests.
created_at: 2025-04-09T20:29:33Z
author_association: COLLABORATOR
diff_hunk: @@ -115,3 +115,35 @@ end
         end
     end
 end
+
+@testset "svd_trunc! mix maxrank and tol for T = $T" for T in
+                                                         (Float32, Float64, ComplexF32,
+                                                          ComplexF64)
+    rng = StableRNG(123)
+    if LinearAlgebra.LAPACK.version() < v"3.12.0"
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection())
+    else
+        algs = (LAPACK_DivideAndConquer(), LAPACK_QRIteration(), LAPACK_Bisection(),
+                LAPACK_Jacobi())
+    end
+    m = 4
+    @testset "algorithm $alg" for alg in algs
+        U = qr_compact(randn(rng, T, m, m))[1]
+        S = Diagonal([0.9, 0.3, 0.1, 0.01])
+        Vᴴ = qr_compact(randn(rng, T, m, m))[1]
+        A = U * S * Vᴴ
+
+        for (rtol, maxrank) in ((0.2, 1), (0.2, 3))
+            for trunc in ((; rtol, maxrank),
+                          truncrank(maxrank) & TruncationKeepAbove(0, rtol))
+                U1, S1, V1ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=1))
+                @test length(S1.diag) == 1
+                @test S1.diag ≈ S.diag[1:1] rtol = sqrt(eps(real(T)))
+
+                U2, S2, V2ᴴ = svd_trunc(A; alg, trunc=(; rtol=0.2, maxrank=3))
path: test/svd.jl
user: login: mtfishman
body: Should be fixed now.
created_at: 2025-04-09T21:14:55Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-09T16:33:52Z
body: ## [Codecov](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub) Report
Attention: Patch coverage is `70.00000%` with `6 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub) | Patch % | Lines |
|---|---|---|
| [src/implementations/truncation.jl](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?src=pr&el=tree&filepath=src%2Fimplementations%2Ftruncation.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub#diff-c3JjL2ltcGxlbWVudGF0aW9ucy90cnVuY2F0aW9uLmps) | 70.00% | [6 Missing :warning: ](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub) |

| [Files with missing lines](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub) | Coverage Δ | |
|---|---|---|
| [src/implementations/truncation.jl](https://app.codecov.io/gh/QuantumKitHub/MatrixAlgebraKit.jl/pull/18?src=pr&el=tree&filepath=src%2Fimplementations%2Ftruncation.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QuantumKitHub#diff-c3JjL2ltcGxlbWVudGF0aW9ucy90cnVuY2F0aW9uLmps) | `88.40% <70.00%> (+5.71%)` | :arrow_up: |

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: Jutho
created_at: 2025-04-16T23:17:23Z
body: Very clean PR and nice tests. Only comment/question is that `TruncationComposition` is a generic name that does not necessarily communicate the `&` (`intersect`) nature of the composition. Would there ever be a need for an `|` (`union`) form of truncation? 
author_association: MEMBER
user: login: mtfishman
created_at: 2025-04-17T01:19:24Z
body: > Very clean PR and nice tests. Only comment/question is that `TruncationComposition` is a generic name that does not necessarily communicate the `&` (`intersect`) nature of the composition. Would there ever be a need for an `|` (`union`) form of truncation?

Good question. I actually chose that name because at first I had `TruncationComposition` store a function that would be used to combine the spectra, where `instersect` was just a special case, but then I couldn't come up with a better name for the special case of intersection.

A use case I could think of for `union` would be filtering both large and small singular values/eigenvalues (it sounds a bit contrived but it comes up when diagonalizing correlation matrices if you want to filter the most occupied/unoccupied orbitals).

Any suggestions for other names? Maybe `TruncationIntersection`? Otherwise I could change the design to:
```julia
struct TruncationComposition{F,T<:Tuple{Vararg{TruncationStrategy}}} <: TruncationStrategy
    f::F
    components::T
end
```
with a definition:
```julia
function Base.:&(trunc1::TruncationStrategy, trunc2::TruncationStrategy)
    return TruncationComposition(intersect, (trunc1, trunc2))
end

function findtruncated(values::AbstractVector, strategy::TruncationComposition)
    inds = map(Base.Fix1(findtruncated, values), strategy.components)
    return strategy.f(inds...)
end
```
author_association: COLLABORATOR
user: login: lkdvos
created_at: 2025-04-17T13:14:44Z
body: TensorKit had `MultipleTruncation`, but I'm not sure if that's any better. I think I like `TruncationIntersection`, seems to indicate well what it is doing, and from the user side you probably don't have to interact too much with that name either, because `&` is more easily accessible?

The proposed general `TruncationComposition` also looks fine to me, it might be "overengineered" in the sense that I can only come up with `union` or `intersect`, but probably that's also just my lack of imagination, so I don't see why not
author_association: MEMBER
user: login: mtfishman
created_at: 2025-04-17T13:26:39Z
body: > TensorKit had `MultipleTruncation`, but I'm not sure if that's any better. I think I like `TruncationIntersection`, seems to indicate well what it is doing, and from the user side you probably don't have to interact too much with that name either, because `&` is more easily accessible?

Yes, I would picture these types would be internal (maybe we would use them internally in ITensor to implement some fancier truncation schemes, though even in those cases I imagine `&` will suffice).

> The proposed general `TruncationComposition` also looks fine to me, it might be "overengineered" in the sense that I can only come up with `union` or `intersect`, but probably that's also just my lack of imagination, so I don't see why not

I guess I could imagine other set operations (`setdiff`, `symdiff`) could be useful.

For now I'll go with `TruncationIntersection`, and we can consider the more general `TruncationComposition` design if we start needing other compositions and don't want to define types for each one. `TruncationIntersection` could later be defined as `TruncationComposition{typeof(intersect)}`.
author_association: COLLABORATOR
user: login: Jutho
created_at: 2025-04-17T14:26:44Z
body: Looks good to me. I also liked the `TruncationComposition` proposal with the `f` field. You then only loose that you cannot join more than 2 components as you might have intersections and unions. So in that case I would just have 2 component fields explicitly, instead of a tuple field, and also nest `TruncationComposition` objects (as in reality there will not be that many of them I believe).

But the current `TruncationIntersection` is good. Shall I merge?
author_association: MEMBER
user: login: mtfishman
created_at: 2025-04-17T15:00:41Z
body: > Looks good to me. I also liked the `TruncationComposition` proposal with the `f` field. You then only loose that you cannot join more than 2 components as you might have intersections and unions. So in that case I would just have 2 component fields explicitly, instead of a tuple field, and also nest `TruncationComposition` objects (as in reality there will not be that many of them I believe).

Yes, that would be something we'd need to work out. I was thinking we could still have a tuple field, but if the `f`s are equal we could splat them together, and otherwise it would become nested. (But as you say just making them nested in general would probably be fine.)

> But the current `TruncationIntersection` is good. Shall I merge?

I'm good with this being merged. I'd be happy to implement the more general `TruncationComposition` in a future PR when use cases arise.
author_association: COLLABORATOR