Repository: google/TensorNetwork

Issue:
Flat flows and flat charges in blocksparse
Hi @mganahl, I believe there are some issues with `.flows` `._flows` and `.flat_flows` in blocksparse. Consider the following example:
 
```
# define tensor
i0 = Index(U1Charge([-1,1]), flow=False)
i1 = Index(U1Charge([-1,0,1]), flow=False)
i2 = Index(U1Charge([-1,0,0,1]), flow=False)
i3 = Index(U1Charge([-2,2]), flow=True)
i4 = Index(U1Charge([-2,0,2]), flow=True)
i5 = Index(U1Charge([-2,0,0,2]), flow=True)
T0 = BlockSparseTensor.random([i0,i1,i2,i3,i4,i5]) 
# do transposition
T1 = T0.transpose([2,3,0,1,5,4]).reshape([8,2,3,12])
```
Then when I examine `T1` the following is returned:
```
# T1._order == [[2, 3], [0], [1], [5, 4]]
# T1.flat_order == [2,3,0,1,5,4]

# T1.flows == [[False, True], [False], [False], [True, True]]
# T1._flows == [False, False, False,  True,  True,  True]
# T1.flat_flows == [False, False, False, True, True, True]

# T1.charges == [[i2,i3],i0,i1,[i5,i4]]
# T1._charges == [i0,i1,i2,i3,i4,i5]
# T1.flat_charges == [i0,i1,i2,i3,i4,i5]
```
So `T1._flows` and `T1.flat_flows` are exactly the same (similarly for the charges); they both give the flows before the lazy transposition and reshape. Is this intended behavior? It seems that it should instead be:
```
# T1.flat_flows == [False, True, False, False, True, True]
# T1.flat_charges == [i2,i3,i0,i1,i5,i4]
```
such that  `T1.flat_flows` is a flattening of  `T1.flows` rather than of `T1._flows`, which would be a useful attribute to have defined.





Discussion:
thanks @gevenbly! This is indeed intended behaviour, though it should be changed. I don't recall the exact problem, but both properties were used to ensure backwards compatibility with earlier code versions. At this point though they have become redundant, so we should remove them. 
My suggestion is to remove them completely. Would you like instead to support the above described behaviour?
Supporting the behavior I describe above would be very useful, as the `flat_flows` and `flat_charges` are something that I would otherwise have to build myself each time I needed them (which is quite often). 
sounds good, I will change it so that it has the desired behaviour.

Resolving patch:
diff --git a/tensornetwork/backends/symmetric/decompositions.py b/tensornetwork/backends/symmetric/decompositions.py
--- a/tensornetwork/backends/symmetric/decompositions.py
+++ b/tensornetwork/backends/symmetric/decompositions.py
@@ -41,7 +41,7 @@ def svd_decomposition(
   matrix = bt.reshape(tensor, [np.prod(left_dims), np.prod(right_dims)])
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, charges, shapes = _find_transposed_diagonal_sparse_blocks(
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
@@ -156,11 +156,11 @@ def flows(self) -> List[List]:
 
   @property
   def flat_charges(self) -> List[BaseCharge]:
-    return self._charges
+    return list([self._charges[o] for o in self.flat_order])
 
   @property
   def flat_flows(self) -> List:
-    return list(self._flows)
+    return list([self._flows[o] for o in self.flat_order])
 
   @property
   def flat_order(self) -> np.ndarray:
@@ -254,7 +254,7 @@ def reshape(
     flat_dims = np.asarray(
         [self._charges[n].dim for o in self._order for n in o])
 
-    if len(new_shape) > len(self.flat_charges):
+    if len(new_shape) > len(self._charges):
       raise ValueError("The shape {} is incompatible with the "
                        "elementary shape {} of the tensor.".format(
                            tuple(new_shape), tuple(flat_dims)))
@@ -312,7 +312,7 @@ def transpose_data(self) -> "ChargeArray":
     and changing `_order` to `[[0,1],[2],[3]]`.
     """
 
-    flat_charges = self.flat_charges
+    flat_charges = self._charges
     flat_shape = [c.dim for c in flat_charges]
     flat_order = self.flat_order
     tmp = np.append(0, np.cumsum([len(o) for o in self._order]))
@@ -368,7 +368,7 @@ def conj(self) -> "ChargeArray":
     return ChargeArray(
         data=np.conj(self.data),
         charges=self._charges,
-        flows=list(np.logical_not(self._flows)),
+        flows=np.logical_not(self._flows),
         order=self._order,
         check_consistency=False)
 
@@ -723,8 +723,8 @@ def transpose_data(self,
       permutation: An optional alternative order to be used to transposed the 
         tensor. If `None` defaults to `BlockSparseTensor.permutation`.
     """
-    flat_charges = self.flat_charges
-    flat_flows = self.flat_flows
+    flat_charges = self._charges
+    flat_flows = self._flows
     if permutation is None:
       permutation = self.flat_order
 
@@ -807,7 +807,7 @@ def outerproduct(tensor1: BlockSparseTensor,
   """
 
   final_charges = tensor1._charges + tensor2._charges
-  final_flows = tensor1.flat_flows + tensor2.flat_flows
+  final_flows = list(tensor1._flows) + list(tensor2._flows)
   order2 = [list(np.asarray(s) + len(tensor1._charges)) for s in tensor2._order]
 
   data = np.zeros(
@@ -961,8 +961,8 @@ def tensordot(
   flat_order_1 = flatten(new_order1)
   flat_order_2 = flatten(new_order2)
 
-  flat_charges_1, flat_flows_1 = tensor1._charges, tensor1.flat_flows
-  flat_charges_2, flat_flows_2 = tensor2._charges, tensor2.flat_flows
+  flat_charges_1, flat_flows_1 = tensor1._charges, tensor1._flows
+  flat_charges_2, flat_flows_2 = tensor2._charges, tensor2._flows
 
   left_charges = []
   right_charges = []
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
@@ -63,8 +63,7 @@ def diag(tensor: ChargeArray) -> Any:
           "`diag` currently only implemented for `ChargeArray` with ndim=1, "
           "found `ndim={}`".format(tensor.ndim))
     flat_charges = tensor._charges + tensor._charges
-    flat_flows = list(tensor.flat_flows) + list(
-        np.logical_not(tensor.flat_flows))
+    flat_flows = list(tensor._flows) + list(np.logical_not(tensor._flows))
     flat_order = list(tensor.flat_order) + list(
         np.asarray(tensor.flat_order) + len(tensor._charges))
     tr_partition = len(tensor._order[0])
@@ -92,7 +91,7 @@ def diag(tensor: ChargeArray) -> Any:
         check_consistency=False)
 
   flat_charges = tensor._charges
-  flat_flows = tensor.flat_flows
+  flat_flows = tensor._flows
   flat_order = tensor.flat_order
   tr_partition = len(tensor._order[0])
   sparse_blocks, charges, block_shapes = _find_transposed_diagonal_sparse_blocks(
@@ -213,7 +212,7 @@ def svd(matrix: BlockSparseTensor,
     raise NotImplementedError("svd currently supports only tensors of order 2.")
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, charges, shapes = _find_transposed_diagonal_sparse_blocks(
@@ -328,7 +327,7 @@ def qr(matrix: BlockSparseTensor, mode: Optional[Text] = 'reduced') -> Any:
     raise NotImplementedError("qr currently supports only rank-2 tensors.")
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, charges, shapes = _find_transposed_diagonal_sparse_blocks(
@@ -415,7 +414,7 @@ def eigh(matrix: BlockSparseTensor,
     raise NotImplementedError("eigh currently supports only rank-2 tensors.")
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, charges, shapes = _find_transposed_diagonal_sparse_blocks(
@@ -473,7 +472,7 @@ def eig(matrix: BlockSparseTensor) -> Tuple[ChargeArray, BlockSparseTensor]:
     raise NotImplementedError("eig currently supports only rank-2 tensors.")
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, charges, shapes = _find_transposed_diagonal_sparse_blocks(
@@ -530,7 +529,7 @@ def inv(matrix: BlockSparseTensor) -> BlockSparseTensor:
     raise ValueError("`inv` can only be taken for matrices, "
                      "found tensor.ndim={}".format(matrix.ndim))
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, _, shapes = _find_transposed_diagonal_sparse_blocks(
@@ -676,7 +675,7 @@ def pinv(matrix: BlockSparseTensor,
                      "found tensor.ndim={}".format(matrix.ndim))
 
   flat_charges = matrix._charges
-  flat_flows = matrix.flat_flows
+  flat_flows = matrix._flows
   flat_order = matrix.flat_order
   tr_partition = len(matrix._order[0])
   blocks, _, shapes = _find_transposed_diagonal_sparse_blocks(


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
@@ -714,3 +714,26 @@ def test_BlockSparseTensor_transpose_data_3():
   np.testing.assert_allclose(a.flows, b.flows)
   np.testing.assert_allclose(a.flows, b.flows)
   np.testing.assert_allclose(adense + bdense, cdense)
+
+
+def test_flat_flows():
+  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)]
+  a = BlockSparseTensor.random(inds, dtype=np.float64)
+  order = [0, 3, 1, 2]
+  a = a.transpose(order)
+  np.testing.assert_allclose(a.flat_flows, [a._flows[o] for o in a.flat_order])
+
+
+def test_flat_charges():
+  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)]
+  a = BlockSparseTensor.random(inds, dtype=np.float64)
+  order = [0, 3, 1, 2]
+  a = a.transpose(order)
+  for n, o in enumerate(a.flat_order):
+    charge_equal(a.flat_charges[n], a._charges[o])