Repository: Jutho/TensorKit.jl
Pull request #232: Rewrite special-purpose constructors to use in-place operations
Author: login: lkdvos
Created: 2025-03-19T14:39:14Z | Merged: 2025-03-21T13:06:58Z

Refactor special-purpose constructors to utilize in-place implementations instead:
```julia
t = zeros(ComplexSpace(2), ComplexSpace(2))
unitary!(t)
```


Diff:
diff --git a/src/TensorKit.jl b/src/TensorKit.jl
index 045f83c05..06b074ba4 100644
--- a/src/TensorKit.jl
+++ b/src/TensorKit.jl
@@ -61,7 +61,8 @@ export blocksectors, blockdim, block, blocks
 export randisometry, randisometry!, rand, rand!, randn, randn!
 
 # special purpose constructors
-export zero, one, one!, id, isomorphism, unitary, isometry
+export zero, one, one!, id, id!, isomorphism, isomorphism!, unitary, unitary!, isometry,
+       isometry!
 
 # reexport most of VectorInterface and some more tensor algebra
 export zerovector, zerovector!, zerovector!!, scale, scale!, scale!!, add, add!, add!!
diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl
index 395c49f27..460e5543a 100644
--- a/src/tensors/linalg.jl
+++ b/src/tensors/linalg.jl
@@ -64,24 +64,31 @@ function one!(t::AbstractTensorMap)
     return t
 end
 
-"""
+@doc """
     id([T::Type=Float64,] V::TensorSpace) -> TensorMap
+    id!(t::AbstractTensorMap) -> AbstractTensorMap
 
 Construct the identity endomorphism on space `V`, i.e. return a `t::TensorMap` with
 `domain(t) == codomain(t) == V`, where either `scalartype(t) = T` if `T` is a `Number` type
 or `storagetype(t) = T` if `T` is a `DenseVector` type.
-"""
+
+See also [`one!`](@ref).
+""" id, id!
+
 id(V::TensorSpace) = id(Float64, V)
 function id(A::Type, V::TensorSpace{S}) where {S}
     W = V ← V
     N = length(codomain(W))
-    return one!(tensormaptype(S, N, N, A)(undef, W))
+    dst = tensormaptype(S, N, N, A)(undef, W)
+    return id!(dst)
 end
+const id! = one!
 
-"""
+@doc """
     isomorphism([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
     isomorphism([T::Type=Float64,] codomain ← domain) -> TensorMap
     isomorphism([T::Type=Float64,] domain → codomain) -> TensorMap
+    isomorphism!(t::AbstractTensorMap) -> AbstractTensorMap
 
 Construct a specific isomorphism between the codomain and the domain, i.e. return a
 `t::TensorMap` where either `scalartype(t) = T` if `T` is a `Number` type or
@@ -93,21 +100,22 @@ error will be thrown.
     that `isomorphism(cod, dom) == inv(isomorphism(dom, cod))`.
 
 See also [`unitary`](@ref) when `InnerProductStyle(cod) === EuclideanInnerProduct()`.
-"""
-function isomorphism(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
-    codomain(V) ≅ domain(V) ||
-        throw(SpaceMismatch("codomain and domain are not isomorphic: $V"))
-    t = tensormaptype(S, N₁, N₂, A)(undef, V)
+""" isomorphism, isomorphism!
+
+function isomorphism!(t::AbstractTensorMap)
+    domain(t) ≅ codomain(t) ||
+        throw(SpaceMismatch(lazy"domain and codomain are not isomorphic: $(space(t))"))
     for (_, b) in blocks(t)
         MatrixAlgebra.one!(b)
     end
     return t
 end
 
-"""
+@doc """
     unitary([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
     unitary([T::Type=Float64,] codomain ← domain) -> TensorMap
     unitary([T::Type=Float64,] domain → codomain) -> TensorMap
+    unitary!(t::AbstractTensorMap) -> AbstractTensorMap
 
 Construct a specific unitary morphism between the codomain and the domain, i.e. return a
 `t::TensorMap` where either `scalartype(t) = T` if `T` is a `Number` type or
@@ -119,16 +127,18 @@ the spacetype does not have a Euclidean inner product, an error will be thrown.
     `unitary(cod, dom) == inv(unitary(dom, cod)) = adjoint(unitary(dom, cod))`.
 
 See also [`isomorphism`](@ref) and [`isometry`](@ref).
-"""
-function unitary(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
-    InnerProductStyle(S) === EuclideanInnerProduct() || throw_invalid_innerproduct(:unitary)
-    return isomorphism(A, V)
+""" unitary, unitary!
+
+function unitary!(t::AbstractTensorMap)
+    InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:unitary)
+    return isomorphism!(t)
 end
 
-"""
+@doc """
     isometry([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
     isometry([T::Type=Float64,] codomain ← domain) -> TensorMap
     isometry([T::Type=Float64,] domain → codomain) -> TensorMap
+    isometry!(t::AbstractTensorMap) -> AbstractTensorMap
 
 Construct a specific isometry between the codomain and the domain, i.e. return a
 `t::TensorMap` where either `scalartype(t) = T` if `T` is a `Number` type or
@@ -137,13 +147,13 @@ Construct a specific isometry between the codomain and the domain, i.e. return a
 isometric inclusion, an error will be thrown.
 
 See also [`isomorphism`](@ref) and [`unitary`](@ref).
-"""
-function isometry(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
-    InnerProductStyle(S) === EuclideanInnerProduct() ||
+""" isometry, isometry!
+
+function isometry!(t::AbstractTensorMap)
+    InnerProductStyle(t) === EuclideanInnerProduct() ||
         throw_invalid_innerproduct(:isometry)
-    domain(V) ≾ codomain(V) ||
-        throw(SpaceMismatch("$V does not allow for an isometric inclusion"))
-    t = tensormaptype(S, N₁, N₂, A)(undef, V)
+    domain(t) ≾ codomain(t) ||
+        throw(SpaceMismatch(lazy"domain and codomain are not isometrically embeddable: $(space(t))"))
     for (_, b) in blocks(t)
         MatrixAlgebra.one!(b)
     end
@@ -152,13 +162,18 @@ end
 
 # expand methods with default arguments
 for morphism in (:isomorphism, :unitary, :isometry)
+    morphism! = Symbol(morphism, :!)
     @eval begin
         $morphism(V::TensorMapSpace) = $morphism(Float64, V)
         $morphism(codomain::TensorSpace, domain::TensorSpace) = $morphism(codomain ← domain)
         function $morphism(A::Type, codomain::TensorSpace, domain::TensorSpace)
             return $morphism(A, codomain ← domain)
         end
-        $morphism(t::AbstractTensorMap) = $morphism(storagetype(t), space(t))
+        function $morphism(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
+            t = tensormaptype(S, N₁, N₂, A)(undef, V)
+            return $morphism!(t)
+        end
+        $morphism(t::AbstractTensorMap) = $morphism!(similar(t))
     end
 end
 


Reviews:
user: login: Jutho
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:20:10Z
user: login: lkdvos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:38:18Z

Review comments:
diff_hunk: @@ -137,13 +147,13 @@ Construct a specific isometry between the codomain and the domain, i.e. return a
 isometric inclusion, an error will be thrown.
 
 See also [`isomorphism`](@ref) and [`unitary`](@ref).
-"""
-function isometry(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
-    InnerProductStyle(S) === EuclideanInnerProduct() ||
+""" isometry, isometry!
+
+function isometry!(t::AbstractTensorMap)
+    InnerProductStyle(t) === EuclideanInnerProduct() ||
         throw_invalid_innerproduct(:isometry)
-    domain(V) ≾ codomain(V) ||
-        throw(SpaceMismatch("$V does not allow for an isometric inclusion"))
-    t = tensormaptype(S, N₁, N₂, A)(undef, V)
+    domain(t) ≾ codomain(t) ||
+        throw(SpaceMismatch("domain and codomain are not isometrically embeddable: $t"))
path: src/tensors/linalg.jl
user: login: Jutho
body: Is this including the whole tensor in the error message?
created_at: 2025-03-20T17:20:10Z
author_association: MEMBER
diff_hunk: @@ -137,13 +147,13 @@ Construct a specific isometry between the codomain and the domain, i.e. return a
 isometric inclusion, an error will be thrown.
 
 See also [`isomorphism`](@ref) and [`unitary`](@ref).
-"""
-function isometry(A::Type, V::TensorMapSpace{S,N₁,N₂}) where {S,N₁,N₂}
-    InnerProductStyle(S) === EuclideanInnerProduct() ||
+""" isometry, isometry!
+
+function isometry!(t::AbstractTensorMap)
+    InnerProductStyle(t) === EuclideanInnerProduct() ||
         throw_invalid_innerproduct(:isometry)
-    domain(V) ≾ codomain(V) ||
-        throw(SpaceMismatch("$V does not allow for an isometric inclusion"))
-    t = tensormaptype(S, N₁, N₂, A)(undef, V)
+    domain(t) ≾ codomain(t) ||
+        throw(SpaceMismatch("domain and codomain are not isometrically embeddable: $t"))
path: src/tensors/linalg.jl
user: login: lkdvos
body: Woops, definitely did not mean to do that. Good catch, thanks!
created_at: 2025-03-20T17:38:18Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T15:05:57Z
body: ## [Codecov](https://app.codecov.io/gh/Jutho/TensorKit.jl/pull/232?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman) Report
Attention: Patch coverage is `92.85714%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 82.64%. Comparing base [(`9bd4c40`)](https://app.codecov.io/gh/Jutho/TensorKit.jl/commit/9bd4c40cd6f436b0c19d187210c83cc5b04cdd68?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman) to head [(`67f6dc5`)](https://app.codecov.io/gh/Jutho/TensorKit.jl/commit/67f6dc56cb8a142a8a5519cd985c65036c88fbb4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman).
> Report is 2 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/Jutho/TensorKit.jl/pull/232?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman) | Patch % | Lines |
|---|---|---|
| [src/tensors/linalg.jl](https://app.codecov.io/gh/Jutho/TensorKit.jl/pull/232?src=pr&el=tree&filepath=src%2Ftensors%2Flinalg.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman#diff-c3JjL3RlbnNvcnMvbGluYWxnLmps) | 92.85% | [1 Missing :warning: ](https://app.codecov.io/gh/Jutho/TensorKit.jl/pull/232?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #232   +/-   ##
=======================================
  Coverage   82.63%   82.64%           
=======================================
  Files          43       43           
  Lines        5557     5559    +2     
=======================================
+ Hits         4592     4594    +2     
  Misses        965      965           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Jutho/TensorKit.jl/pull/232?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jutho+Haegeman).

<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-03-20T17:22:09Z
body: Looks good except for the comment/question.
author_association: MEMBER
user: login: lkdvos
created_at: 2025-03-20T17:40:32Z
body: Good catch, thanks!
At some point we should maybe also normalize our error messages here as well.
I'm still not sure if we want to use `lazy` everywhere, include information, or not, and it seems like we still have `SectorMismatch` and `SpaceMismatch` both being used for similar things in certain locations.
author_association: MEMBER