Repository: google/TensorNetwork

Issue:
Blocksparse not evaluating scalar products properly
Hi @mganahl , I am having issues when trying to contract networks that evaluate to a scalar. For example defining:
```
i0 = Index(U1Charge([-1,1]), flow=False)
i1 = Index(U1Charge([-1,1]), flow=True)
T0 = BlockSparseTensor.random([i0,i1]) 
```
then evaluating `ncon([T0,T0],[[1,2],[2,1]])` produces an error. It this specific instance, I can instead evaluate the network as `c = trace(T0 @ T0)`, however `c` is returned as some kind of weird rank-0 blocksparse tensor rather than a scalar as expected. Evaluating `c.todense()` produces an empty array, but taking `c.data` gives the correct scalar (although the .data field is not something the user should ever have to touch). Can blocksparse `trace` be fixed to return a scalar? And also blocksparse `ncon` fixed to work with closed tensor networks? (i.e networks that contain no dangling edges)

On a related note, I see that the numpy version of `ncon` returns a length-1 numpy array when contracting closed tensor networks. I would strongly advocate that the return from evaluating a closed tensor network (no dangling edges) should always be a plain scalar, regardless of the types of the tensors involved.

 




Discussion:
Hey @gevenbly, regarding your second point, for me`ncon` is returning an unsized object for contraction of a closed network. Could you give an example of when it returns a length-1 array?

tahnks!
In general  I think it's better to have `ncon` return a `Tensor` object rather than a python number, also for closed networks.

Resolving patch:
diff --git a/tensornetwork/backends/symmetric/symmetric_backend.py b/tensornetwork/backends/symmetric/symmetric_backend.py
--- a/tensornetwork/backends/symmetric/symmetric_backend.py
+++ b/tensornetwork/backends/symmetric/symmetric_backend.py
@@ -90,6 +90,10 @@ def diag(self, tensor: Tensor) -> Tensor:
     return self.bs.diag(tensor)
 
   def convert_to_tensor(self, tensor: Tensor) -> Tensor:
+    if numpy.isscalar(tensor):
+      tensor = BlockSparseTensor(
+          data=tensor, charges=[], flows=[], order=[], check_consistency=False)
+
     if not isinstance(tensor, BlockSparseTensor):
       raise TypeError(
           "cannot convert tensor of type `{}` to `BlockSparseTensor`".format(
diff --git a/tensornetwork/block_sparse/blocksparsetensor.py b/tensornetwork/block_sparse/blocksparsetensor.py
--- a/tensornetwork/block_sparse/blocksparsetensor.py
+++ b/tensornetwork/block_sparse/blocksparsetensor.py
@@ -391,6 +391,12 @@ def __rmul__(self, number: np.number) -> "ChargeArray":
   def __truediv__(self, number: np.number) -> "ChargeArray":
     raise NotImplementedError("__truediv__ not implemented for ChargeArray")
 
+  def item(self):
+    if self.ndim == 0:
+      if len(self.data) == 1:
+        return self.data[0]
+    raise ValueError("can only convert an array of size 1 to a Python scalar")
+
 
 class BlockSparseTensor(ChargeArray):
   """
@@ -930,23 +936,15 @@ def tensordot(
 
   #checks finished
 
-  #special case inner product
+  #special case inner product (returns an ndim=0 tensor)
   if (len(axes1) == tensor1.ndim) and (len(axes2) == tensor2.ndim):
     t1 = tensor1.transpose(axes1).transpose_data()
     t2 = tensor2.transpose(axes2).transpose_data()
-    data = np.dot(t1.data, t2.data)
-    charge = tensor1._charges[0]
-    final_charge = charge.__new__(type(charge))
-
-    final_charge.__init__(
-        np.empty((charge.num_symmetries, 0), dtype=np.int16),
-        charge_labels=np.empty(0, dtype=np.int16),
-        charge_types=charge.charge_types)
     return BlockSparseTensor(
-        data=data,
-        charges=[final_charge],
-        flows=[False],
-        order=[[0]],
+        data=np.dot(t1.data, t2.data),
+        charges=[],
+        flows=[],
+        order=[],
         check_consistency=False)
 
   #in all other cases we perform a regular tensordot
diff --git a/tensornetwork/block_sparse/linalg.py b/tensornetwork/block_sparse/linalg.py
--- a/tensornetwork/block_sparse/linalg.py
+++ b/tensornetwork/block_sparse/linalg.py
@@ -602,7 +602,8 @@ def eye(column_index: Index,
 def trace(tensor: BlockSparseTensor,
           axes: Optional[Tuple[int, ...]] = None) -> BlockSparseTensor:
   """
-  Compute the trace of a matrix or tensor.
+  Compute the trace of a matrix or tensor. If input has `ndim>2`, take
+  the trace over the last two dimensions.
   Args:
     tensor: A `BlockSparseTensor`.
     axes: The axes over which the trace should be computed.
@@ -656,6 +657,8 @@ def trace(tensor: BlockSparseTensor,
       a1ar[np.logical_xor(mask_min, mask_max)] -= 1
       a0 = list(a0ar)
       a1 = list(a1ar)
+    if out.ndim == 0:
+      return out.item()
     return out  # pytype: disable=bad-return-type
   raise ValueError("trace can only be taken for tensors with ndim > 1")
 


Test patch:
diff --git a/tensornetwork/block_sparse/blocksparsetensor_test.py b/tensornetwork/block_sparse/blocksparsetensor_test.py
--- a/tensornetwork/block_sparse/blocksparsetensor_test.py
+++ b/tensornetwork/block_sparse/blocksparsetensor_test.py
@@ -737,3 +737,23 @@ def test_flat_charges():
   a = a.transpose(order)
   for n, o in enumerate(a.flat_order):
     charge_equal(a.flat_charges[n], a._charges[o])
+
+
+def test_item():
+  t1 = BlockSparseTensor(
+      data=np.array(1.0),
+      charges=[],
+      flows=[],
+      order=[],
+      check_consistency=False)
+  assert t1.item() == 1
+  Ds = [10, 11, 12, 13]
+  charges = [U1Charge.random(Ds[n], -5, 5) for n in range(4)]
+  flows = [True, False, True, False]
+  inds = [Index(c, flows[n]) for n, c in enumerate(charges)]
+  t2 = BlockSparseTensor.random(inds, dtype=np.float64)
+  with pytest.raises(
+      ValueError,
+      match="can only convert an array of"
+      " size 1 to a Python scalar"):
+    t2.item()