Skip to content

Commit efb4ce3

Browse files
committed
Many more working things
1 parent 7727080 commit efb4ce3

8 files changed

Lines changed: 139 additions & 82 deletions

File tree

Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,6 @@ cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1"
8989

9090
[targets]
9191
test = ["ArgParse", "Adapt", "Aqua", "AllocCheck", "Combinatorics", "CUDA", "cuTENSOR", "GPUArrays", "JET", "LinearAlgebra", "SafeTestsets", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote", "Mooncake"]
92+
93+
[sources]
94+
Strided = {url = "https://github.com/QuantumKitHub/Strided.jl/", rev = "ksh/copyto"}

ext/TensorKitAdaptExt.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ function Adapt.adapt_structure(to, x::DiagonalTensorMap)
1515
data′ = adapt(to, x.data)
1616
return DiagonalTensorMap(data′, x.domain)
1717
end
18-
function Adapt.adapt_structure(::Type{TorA}, x::BraidingTensor) where {TorA <: Union{Number, DenseArray{<:Number}}}
19-
return BraidingTensor{scalartype(TorA)}(space(x), x.adjoint)
18+
function Adapt.adapt_structure(::Type{T}, x::BraidingTensor{T′, S, A}) where {T <: Number, T′, S, A}
19+
return BraidingTensor(space(x), TensorKit.similarstoragetype(A, T), x.adjoint)
20+
end
21+
function Adapt.adapt_structure(::Type{TA}, x::BraidingTensor{T, S, A}) where {TA <: DenseArray{<:Number}, T, S, A}
22+
return BraidingTensor(space(x), TA, x.adjoint)
2023
end
2124

2225
end

ext/TensorKitCUDAExt/TensorKitCUDAExt.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ module TensorKitCUDAExt
33
using CUDA, CUDA.CUBLAS, CUDA.CUSOLVER, LinearAlgebra
44
using CUDA: @allowscalar
55
using cuTENSOR: cuTENSOR
6+
using Strided: StridedViews
67
import CUDA: rand as curand, rand! as curand!, randn as curandn, randn! as curandn!
78

9+
using CUDA: KernelAbstractions
10+
using CUDA.KernelAbstractions: @kernel, @index
11+
812
using TensorKit
913
using TensorKit.Factorizations
1014
using TensorKit.Strided
1115
using TensorKit.Factorizations: AbstractAlgorithm
1216
using TensorKit: SectorDict, tensormaptype, scalar, similarstoragetype, AdjointTensorMap, scalartype, project_symmetric_and_check
13-
import TensorKit: randisometry, rand, randn, similarmatrixtype
17+
import TensorKit: randisometry, rand, randn, similarmatrixtype, _set_subblock!
1418

1519
using TensorKit: MatrixAlgebraKit
1620

@@ -21,4 +25,16 @@ include("truncation.jl")
2125

2226
TensorKit.similarmatrixtype(::Type{A}) where {T <: Number, M, A <: CuVector{T, M}} = CuMatrix{T, M}
2327

28+
function TensorKit._set_subblock!(data::TD, val) where {T, TD <: Union{<:CuMatrix{T}, <:StridedViews.StridedView{T, 4, <:CuArray{T}}}}
29+
@kernel function fill_subblock_kernel!(subblock, val)
30+
idx = @index(Global, Cartesian)
31+
@inbounds subblock[idx[1], idx[2], idx[2], idx[1]] = val
32+
end
33+
kernel = fill_subblock_kernel!(KernelAbstractions.get_backend(data))
34+
d1 = size(data, 1)
35+
d2 = size(data, 2)
36+
kernel(data, val; ndrange = (d1, d2))
37+
return data
38+
end
39+
2440
end

ext/TensorKitCUDAExt/cutensormap.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,23 @@ for f in (:sqrt, :log, :asin, :acos, :acosh, :atanh, :acoth)
168168
return tf
169169
end
170170
end
171+
172+
173+
function TensorKit.add_kernel_nonthreaded!(
174+
::TensorKit.FusionStyle,
175+
tdst::CuTensorMap, tsrc::CuTensorMap, p, transformer::TensorKit.GenericTreeTransformer, α, β, backend...
176+
)
177+
# preallocate buffers
178+
buffers = TensorKit.allocate_buffers(tdst, tsrc, transformer)
179+
180+
for subtransformer in transformer.data
181+
# Special case without intermediate buffers whenever there is only a single block
182+
if length(subtransformer[1]) == 1
183+
TensorKit._add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...)
184+
else
185+
cu_subtransformer = tuple(CUDA.adapt(CuArray, subtransformer[1]), subtransformer[2:end]...)
186+
TensorKit._add_transform_multi!(tdst, tsrc, p, cu_subtransformer, buffers, α, β, backend...)
187+
end
188+
end
189+
return nothing
190+
end

src/tensors/abstracttensor.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,10 +620,10 @@ Base.similar(t::AbstractTensorMap, ::Type{T}, codomain::TensorSpace) where {T} =
620620
# 2 arguments
621621
Base.similar(t::AbstractTensorMap, codomain::TensorSpace) =
622622
similar(t, codomain one(codomain))
623-
Base.similar(t::AbstractTensorMap, V::TensorMapSpace) = similar(t, scalartype(t), V)
623+
Base.similar(t::AbstractTensorMap, V::TensorMapSpace) = similar(t, storagetype(t), V)
624624
Base.similar(t::AbstractTensorMap, ::Type{T}) where {T} = similar(t, T, space(t))
625625
# 1 argument
626-
Base.similar(t::AbstractTensorMap) = similar(t, scalartype(t), space(t))
626+
Base.similar(t::AbstractTensorMap) = similar(t, storagetype(t), space(t))
627627

628628
# generic implementation for AbstractTensorMap -> returns `TensorMap`
629629
function Base.similar(t::AbstractTensorMap, ::Type{TorA}, V::TensorMapSpace) where {TorA}

src/tensors/braidingtensor.jl

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct BraidingTensor{T, S, A} <: AbstractTensorMap{T, S, 2, 2}
1717
V1::S
1818
V2::S
1919
adjoint::Bool
20-
function BraidingTensor{T, S}(V1::S, V2::S, ::Type{A}, adjoint::Bool = false) where {T, S <: IndexSpace, A <: DenseVector{T}}
20+
function BraidingTensor{T, S, A}(V1::S, V2::S, ::Type{A}, adjoint::Bool = false) where {T, S <: IndexSpace, A <: DenseVector{T}}
2121
for a in sectors(V1), b in sectors(V2), c in (a b)
2222
Nsymbol(a, b, c) == Nsymbol(b, a, c) ||
2323
throw(ArgumentError("Cannot define a braiding between $a and $b"))
@@ -26,6 +26,9 @@ struct BraidingTensor{T, S, A} <: AbstractTensorMap{T, S, 2, 2}
2626
# partial construction: only construct rowr and colr when needed
2727
end
2828
end
29+
function BraidingTensor{T, S}(V1::S, V2::S, ::Type{A}, adjoint::Bool = false) where {T, S <: IndexSpace, A}
30+
return BraidingTensor{T, S, A}(V1, V2, A, adjoint)
31+
end
2932
function BraidingTensor{T}(V1::S, V2::S, A, adjoint::Bool = false) where {T, S <: IndexSpace}
3033
return BraidingTensor{T, S}(V1, V2, A, adjoint)
3134
end
@@ -72,23 +75,16 @@ function BraidingTensor{T}(V::HomSpace, adjoint::Bool = false) where {T}
7275
return BraidingTensor{T}(V[2], V[1], adjoint)
7376
end
7477
function Base.adjoint(b::BraidingTensor{T, S, A}) where {T, S, A}
75-
return BraidingTensor{T, S, A}(b.V1, b.V2, !b.adjoint)
78+
return BraidingTensor{T, S, A}(b.V1, b.V2, A, !b.adjoint)
7679
end
7780

78-
storagetype(b::BraidingTensor{T, S, A}) where {T, S, A} = A
81+
storagetype(::Type{BraidingTensor{T, S, A}}) where {T, S, A} = A
7982
space(b::BraidingTensor) = b.adjoint ? b.V1 b.V2 b.V2 b.V1 : b.V2 b.V1 b.V1 b.V2
8083

81-
# specializations to ignore the storagetype of BraidingTensor
82-
promote_storagetype(::Type{A}, ::Type{B}) where {A <: BraidingTensor, B <: AbstractTensorMap} = storagetype(B)
83-
promote_storagetype(::Type{A}, ::Type{B}) where {A <: AbstractTensorMap, B <: BraidingTensor} = storagetype(A)
84-
promote_storagetype(::Type{A}, ::Type{B}) where {A <: BraidingTensor, B <: BraidingTensor} = storagetype(A)
85-
86-
promote_storagetype(::Type{T}, ::Type{A}, ::Type{B}) where {T <: Number, A <: BraidingTensor, B <: AbstractTensorMap} =
87-
similarstoragetype(B, T)
88-
promote_storagetype(::Type{T}, ::Type{A}, ::Type{B}) where {T <: Number, A <: AbstractTensorMap, B <: BraidingTensor} =
89-
similarstoragetype(A, T)
90-
promote_storagetype(::Type{T}, ::Type{A}, ::Type{B}) where {T <: Number, A <: BraidingTensor, B <: BraidingTensor} =
91-
similarstoragetype(A, T)
84+
promote_storagetype(::Type{B}, ::Type{T}) where {B <: BraidingTensor, T <: AbstractTensorMap} =
85+
promote_storagetype(storagetype(B), storagetype(T))
86+
promote_storagetype(::Type{T}, ::Type{B}) where {B <: BraidingTensor, T <: AbstractTensorMap} =
87+
promote_storagetype(storagetype(B), storagetype(T))
9288

9389
function Base.getindex(b::BraidingTensor)
9490
sectortype(b) === Trivial || throw(SectorMismatch())
@@ -120,6 +116,14 @@ function _braiding_factor(f₁, f₂, inv::Bool = false)
120116
return r
121117
end
122118

119+
function _set_subblock!(data, val)
120+
@inbounds for i in axes(data, 1), j in axes(data, 2)
121+
data[i, j, j, i] = val
122+
end
123+
return data
124+
end
125+
126+
123127
@inline function subblock(
124128
b::BraidingTensor, (f₁, f₂)::Tuple{FusionTree{I, 2}, FusionTree{I, 2}}
125129
) where {I <: Sector}
@@ -141,11 +145,7 @@ end
141145
fill!(data, zero(eltype(b)))
142146

143147
r = _braiding_factor(f₁, f₂, b.adjoint)
144-
if !isnothing(r)
145-
@inbounds for i in axes(data, 1), j in axes(data, 2)
146-
data[i, j, j, i] = r
147-
end
148-
end
148+
!isnothing(r) && _set_subblock!(data, r)
149149
return data
150150
end
151151

@@ -157,9 +157,30 @@ Base.convert(::Type{TensorMap}, b::BraidingTensor) = TensorMap(b)
157157

158158
Base.complex(b::BraidingTensor{<:Complex}) = b
159159
function Base.complex(b::BraidingTensor{T, S, A}) where {T, S, A}
160-
Tc = complex(T)
161-
Ac = similarstoragetype(Tc, A)
162-
return BraidingTensor{Tc, S, Ac}(space(b), b.adjoint)
160+
Ac = similarstoragetype(A, complex(T))
161+
return BraidingTensor(space(b), Ac, b.adjoint)
162+
end
163+
164+
function _trivial_subblock!(data, b::BraidingTensor)
165+
V1, V2 = codomain(b)
166+
d1, d2 = dim(V1), dim(V2)
167+
subblock = sreshape(StridedView(data), (d1, d2, d2, d1))
168+
_set_subblock!(subblock, one(eltype(b)))
169+
return data
170+
end
171+
172+
function _nontrivial_subblock!(data, b::BraidingTensor, s::Sector)
173+
base_offset = first(blockstructure(b)[s][2]) - 1
174+
175+
for ((f₁, f₂), (sz, str, off)) in pairs(subblockstructure(space(b)))
176+
(f₁.coupled == f₂.coupled == s) || continue
177+
r = _braiding_factor(f₁, f₂, b.adjoint)
178+
isnothing(r) && continue
179+
# change offset to account for single block
180+
subblock = StridedView(data, sz, str, off - base_offset)
181+
_set_subblock!(subblock, r)
182+
end
183+
return data
163184
end
164185

165186
function block(b::BraidingTensor, s::Sector)
@@ -169,36 +190,17 @@ function block(b::BraidingTensor, s::Sector)
169190
# TODO: probably always square?
170191
m = blockdim(codomain(b), s)
171192
n = blockdim(domain(b), s)
172-
data = similarmatrixtype(storagetype(b))(undef, (m, n))
173193

174-
length(data) == 0 && return data # s ∉ blocksectors(b)
194+
m * n == 0 && return similarmatrixtype(storagetype(b))(undef, (m, n)) # s ∉ blocksectors(b)
175195

196+
data = similarmatrixtype(storagetype(b))(undef, (m, n))
176197
data = fill!(data, zero(eltype(b)))
177198

178-
V1, V2 = codomain(b)
179199
if sectortype(b) === Trivial
180-
d1, d2 = dim(V1), dim(V2)
181-
subblock = sreshape(StridedView(data), (d1, d2, d2, d1))
182-
@inbounds for i in axes(subblock, 1), j in axes(subblock, 2)
183-
subblock[i, j, j, i] = one(eltype(b))
184-
end
185-
return data
186-
end
187-
188-
base_offset = first(blockstructure(b)[s][2]) - 1
189-
190-
for ((f₁, f₂), (sz, str, off)) in pairs(subblockstructure(space(b)))
191-
(f₁.coupled == f₂.coupled == s) || continue
192-
r = _braiding_factor(f₁, f₂, b.adjoint)
193-
isnothing(r) && continue
194-
# change offset to account for single block
195-
subblock = StridedView(data, sz, str, off - base_offset)
196-
@inbounds for i in axes(subblock, 1), j in axes(subblock, 2)
197-
subblock[i, j, j, i] = r
198-
end
200+
return _trivial_subblock!(data, b)
201+
else
202+
return _nontrivial_subblock!(data, b, s)
199203
end
200-
201-
return data
202204
end
203205

204206
# Index manipulations

test/cuda/planar.jl

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ using .TestSetup
1111
@testset "Braiding tensor" begin
1212
for V in (Vtr, VU₁, VfU₁, VfSU₂, Vfib)
1313
W = V[1] V[2] V[2] V[1]
14-
t1 = @constinferred BraidingTensor(W, CuVector)
14+
T = isreal(sectortype(W)) ? Float64 : ComplexF64
15+
t1 = @constinferred BraidingTensor(W, CuVector{T, CUDA.DeviceMemory})
1516
@test space(t1) == W
1617
@test codomain(t1) == codomain(W)
1718
@test domain(t1) == domain(W)
1819
@test scalartype(t1) == (isreal(sectortype(W)) ? Float64 : ComplexF64)
1920
@test storagetype(t1) == CuVector{scalartype(t1), CUDA.DeviceMemory}
20-
t2 = @constinferred BraidingTensor{ComplexF64, typeof(W), CuVector{ComplexF64, CUDA.DeviceMemory}}(W)
21+
t2 = @constinferred BraidingTensor(W, CuVector{ComplexF64, CUDA.DeviceMemory})
2122
@test scalartype(t2) == ComplexF64
2223
@test storagetype(t2) == CuVector{ComplexF64, CUDA.DeviceMemory}
2324
t3 = @testinferred adapt(storagetype(t2), t1)
2425
@test storagetype(t3) == storagetype(t2)
25-
@test t3 == t2
26+
# allowscalar needed for the StridedView comparison
27+
CUDA.@allowscalar begin
28+
@test t3 == t2
29+
end
2630

2731
W2 = reverse(codomain(W)) domain(W)
2832
@test_throws SpaceMismatch BraidingTensor(W2)
@@ -32,25 +36,33 @@ using .TestSetup
3236
@test scalartype(complex(t1)) <: Complex
3337

3438
t3 = @inferred TensorMap(t2)
35-
@test storagetype(t3) = CuVector{ComplexF64, CUDA.DeviceMemory}
36-
t4 = braid(id(storagetype(t2), domain(t2)), ((2, 1), (3, 4)), (1, 2, 3, 4))
37-
@test t1 t4
39+
@test storagetype(t3) == CuVector{ComplexF64, CUDA.DeviceMemory}
40+
t4 = braid(adapt(CuArray, id(scalartype(t2), domain(t2))), ((2, 1), (3, 4)), (1, 2, 3, 4))
41+
CUDA.@allowscalar begin
42+
@test t1 t4
43+
end
3844
for (c, b) in blocks(t1)
3945
@test block(t1, c) b block(t3, c)
4046
end
41-
for (f1, f2) in fusiontrees(t1)
42-
@test t1[f1, f2] t3[f1, f2]
47+
48+
CUDA.@allowscalar begin
49+
for (f1, f2) in fusiontrees(t1)
50+
@test t1[f1, f2] t3[f1, f2]
51+
end
4352
end
4453

4554
t5 = @inferred TensorMap(t2')
46-
@test storagetype(t5) = CuVector{ComplexF64, CUDA.DeviceMemory}
47-
t6 = braid(id(storagetype(t2), domain(t2')), ((2, 1), (3, 4)), (4, 3, 2, 1))
48-
@test t5 t6
49-
for (c, b) in blocks(t1')
50-
@test block(t1', c) b block(t5, c)
51-
end
52-
for (f1, f2) in fusiontrees(t1')
53-
@test t1'[f1, f2] t5[f1, f2]
55+
@test storagetype(t5) == CuVector{ComplexF64, CUDA.DeviceMemory}
56+
t6 = braid(adapt(CuArray, id(scalartype(t2), domain(t2'))), ((2, 1), (3, 4)), (4, 3, 2, 1))
57+
CUDA.@allowscalar begin
58+
@test t5 t6
59+
for (c, b) in blocks(t1')
60+
@test block(t1', c) b block(t5, c)
61+
end
62+
for (f1, f2) in fusiontrees(t1')
63+
# needed here for broadcasting the - in isapprox
64+
@test t1'[f1, f2] t5[f1, f2]
65+
end
5466
end
5567
end
5668
end
@@ -112,14 +124,15 @@ end
112124
@tensor contractcheck = true C3[i j; k l] := A[i j; m] * B2[k l; m]
113125
end
114126

127+
#= # TODO NEEDS UPDATES TO planar/preprocessors
115128
A = CUDA.rand(T, V ← V ⊗ V)
116129
B = CUDA.rand(T, V ⊗ V ← V)
117130
@planar C1[i; j] := A[i; k l] * τ[k l; m n] * B[m n; j]
118131
@planar contractcheck = true C2[i; j] := A[i; k l] * τ[k l; m n] * B[m n; j]
119132
@test C1 ≈ C2
120133
@test_throws SpaceMismatch("incompatible spaces for m: $V ≠ $(V')") begin
121134
@planar contractcheck = true C3[i; j] := A[i; k l] * τ[k l; m n] * B[n j; m]
122-
end
135+
end=#
123136
end
124137

125138
@testset "MPS networks" begin
@@ -169,9 +182,9 @@ end
169182

170183
@tensor ρ2[-1 -2; -3] := GL[1 -2; 3] * x[3 2; -3] * conj(x[1 2; -1])
171184
@plansor ρ3[-1 -2; -3] := GL[1 2; 4] * x[4 5; -3] * τ[2 3; 5 -2] * conj(x[1 3; -1])
172-
@planar ρ2′[-1 -2; -3] := GL′[1 2; 4] * x′[4 5; -3] * τ[2 3; 5 -2] *
173-
conj(x′[1 3; -1])
174-
@test force_planar(ρ2) ρ2′
185+
#@planar ρ2′[-1 -2; -3] := GL′[1 2; 4] * x′[4 5; -3] * τ[2 3; 5 -2] *
186+
# conj(x′[1 3; -1])
187+
#@test force_planar(ρ2) ≈ ρ2′
175188
@test ρ2 ρ3
176189

177190
# Periodic boundary conditions
@@ -185,11 +198,11 @@ end
185198
@plansor O_periodic2[-1 -2; -3 -4] := O[1 2; -3 6] * f1[-1; 1 3 5] *
186199
conj(f2[-4; 6 7 8]) * τ[2 3; 7 4] *
187200
τ[4 5; 8 -2]
188-
@planar O_periodic′[-1 -2; -3 -4] := O′[1 2; -3 6] * f1′[-1; 1 3 5] *
201+
#=@planar O_periodic′[-1 -2; -3 -4] := O′[1 2; -3 6] * f1′[-1; 1 3 5] *
189202
conj(f2′[-4; 6 7 8]) * τ[2 3; 7 4] *
190-
τ[4 5; 8 -2]
203+
τ[4 5; 8 -2]=#
191204
@test O_periodic1 O_periodic2
192-
@test force_planar(O_periodic1) O_periodic′
205+
#@test force_planar(O_periodic1) ≈ O_periodic′
193206
end
194207

195208
@testset "MERA networks" begin
@@ -253,24 +266,24 @@ end
253266
t2 = CUDA.rand(T, V2 V1)
254267

255268
tr1 = @planar opt = true t1[a; b] * t2[b; a] / 2
256-
tr2 = @planar opt = true t1[d; a] * t2[b; c] * 1 / 2 * τ[c b; a d]
269+
#=tr2 = @planar opt = true t1[d; a] * t2[b; c] * 1 / 2 * τ[c b; a d]
257270
tr3 = @planar opt = true t1[d; a] * t2[b; c] * τ[a c; d b] / 2
258271
tr4 = @planar opt = true t1[f; a] * 1 / 2 * t2[c; d] * τ[d b; c e] * τ[e b; a f]
259272
tr5 = @planar opt = true t1[f; a] * t2[c; d] / 2 * τ[d b; c e] * τ[a e; f b]
260273
tr6 = @planar opt = true t1[f; a] * t2[c; d] * τ[c d; e b] / 2 * τ[e b; a f]
261-
tr7 = @planar opt = true t1[f; a] * t2[c; d] * (τ[c d; e b] * τ[a e; f b] / 2)
274+
tr7 = @planar opt = true t1[f; a] * t2[c; d] * (τ[c d; e b] * τ[a e; f b] / 2)=#
262275

263-
@test tr1 tr2 tr3 tr4 tr5 tr6 tr7
276+
#@test tr1 ≈ tr2 ≈ tr3 ≈ tr4 ≈ tr5 ≈ tr6 ≈ tr7
264277

265278
tr1 = @plansor opt = true t1[a; b] * t2[b; a] / 2
266-
tr2 = @plansor opt = true t1[d; a] * t2[b; c] * 1 / 2 * τ[c b; a d]
279+
#=tr2 = @plansor opt = true t1[d; a] * t2[b; c] * 1 / 2 * τ[c b; a d]
267280
tr3 = @plansor opt = true t1[d; a] * t2[b; c] * τ[a c; d b] / 2
268281
tr4 = @plansor opt = true t1[f; a] * 1 / 2 * t2[c; d] * τ[d b; c e] * τ[e b; a f]
269282
tr5 = @plansor opt = true t1[f; a] * t2[c; d] / 2 * τ[d b; c e] * τ[a e; f b]
270283
tr6 = @plansor opt = true t1[f; a] * t2[c; d] * τ[c d; e b] / 2 * τ[e b; a f]
271-
tr7 = @plansor opt = true t1[f; a] * t2[c; d] * (τ[c d; e b] * τ[a e; f b] / 2)
284+
tr7 = @plansor opt = true t1[f; a] * t2[c; d] * (τ[c d; e b] * τ[a e; f b] / 2)=#
272285

273-
@test tr1 tr2 tr3 tr4 tr5 tr6 tr7
286+
#@test tr1 ≈ tr2 ≈ tr3 ≈ tr4 ≈ tr5 ≈ tr6 ≈ tr7
274287
end
275288
@testset "Issue 262" begin
276289
V =^2

0 commit comments

Comments
 (0)