Description
A common code pattern is:
A = ITensor(1)
A *= B
A *= C
which is useful when initializing a loop, for example. However, it has bothered me that it makes an unnecessary copy of B
. We decided, I think correctly, that ITensor(1) * B
should make a copy of B
, since it could lead to very subtle bugs if ITensor(1) * B
didn't copy but ITensor(1.000001) * B
did copy (say you were doing floating point operations to compute a constant a
, and then did ITensor(a) * B
, and then one time a
happened to be equal to 1
but you were relying on the copying behavior).
My proposal for this would be to introduce yet another storage type as well as a special number type called One
. The proposal would be the following:
using ITensors
using ITensors.NDTensors
# Multiplicative identity
struct One <: Number
end
struct Scalar{ElT} <: TensorStorage{ElT}
data::Base.RefValue{ElT}
end
Scalar(n::Number) = Scalar(Ref(n))
oneITensor() = itensor(Scalar(One()), IndexSet())
The Base.RefValue
storage allows the value to be modified in-place, i.e. would allow T[] = 2.0
. That's not totally necessary but I think is helpful for generic code, since a lot of code in NDTensors
assumes the tensors are mutable.
Then, we can define contraction such that (oneITensor() * B) = B
(without copying). Scalar(One())
storage could be interpreted as a universal multiplicative identity. Note that if someone did 1.0 * oneITensor()
, it would create a tensor with storage Scalar(1.0)
, which would then act as ITensor(1.0)
with copying behavior with multiplication. This allows the nice property that (1.0 * oneITensor()) * B
and oneITensor() * (1.0 * B)
both return copies of B
.
Another note that the Scalar
storage isn't strictly necessary here, and we could use a length 1 Dense
storage with element type One
, but I've been meaning to add a Scalar
type anyway since I think it will help clean up some code logic with scalar ITensor contractions.
A bonus from this is getting a behavior that I've wanted for delta
tensors, which is that A * delta(i, j)
would not copy A
if the contraction is equivalent to an index replacement. This faced a similar issue to trying to make ITensor(1) * A
non-copying, which is that A * (1 * delta(i, j))
would not copy but (A * 1) * delta(i, j)
would copy (and similarly, if 1
was replaced by a general floating point constant a
, the behavior would subtly change of a
happened to be equal to 1
).
With the new One
type, a delta
tensor could have storage with a value of One
instead of 1
, which would be clearly documented. Then, if you did 1 * delta(i, j)
, it would return a tensor with storage of 1.0
and have the regular copying behavior, which would have the property that A * (1 * delta(i, j))
and (A * 1) * delta(i, j)
always return a copy of A
.