# Tensor Node Notation

# Nested boxtimes

## Determining the order of contractions

Once we have any mode name appearing more than twice in a -product, we may not simply resolve brackets. For example for as follows,

.

The result may be rather unintended.

n = assign_mode_size({'alpha','beta','gamma'},[4,3,2]);

N1 = init_node({'alpha','beta'},n);

N2 = init_node({'beta','gamma'},n);

N3 = N1;

N4 = N2;

N1 = randomize_node(N1)

N2 = randomize_node(N2)

N3 = randomize_node(N3)

N4 = randomize_node(N4)

w = boxtimes(node_transpose(N2),node_transpose(N1),N3,N4);

w.data

net_view(node_transpose(N2),node_transpose(N1),N3,N4)

But instead of performing three single -products, we can transfer the bracket structure as before to one single boxtimes in form of nested cells. boxtimes will never call itself!

r1 = boxtimes(node_transpose(boxtimes(N1,N2)),boxtimes(N3,N4));

r1.data

r2 = boxtimes(net_transpose({N1,N2}),{N3,N4});

r2.data

net_view(net_transpose({N1,N2}),{N3,N4})

In the following call we can see that, interestingly, the optimal order of contractions (in terms of complexity) is not the one prescribed by the original brackets . So boxtimes indeed does not call itself, but uses the nested input solely to determine contraction rules.

a = n.alpha; b = n.beta; c = n.gamma;

cost_as_by_brackets = (a*b*c) + (a*b*c) + (a*c)

boxtimes(net_transpose({N1,N2}),{N3,N4},'mode','optimal_show')

The difference is tiny for now, but points at a central idea behind tensor networks.

## Scalar products between different tensor formats

This mechanic allows us to be more flexible when for example taking the scalar product between two different tensor formats in which we used β as mode names in both cases.

load('TT-format')

boxtimes(G)

load('Tucker-format')

boxtimes(G,{C,U})

G{:}

net_view(G,{C,U}) % node numbers for G: 1-4, C: 5, U: 6-9

Here, the order of contractions is crucial. If we would first evaluate , then let alone the resulting tensor has size . However, the optimal order of multiplication just requires an order of complexity of (calculated classically) and might not be the order of contractions one expects at a first glance.

node_size(G)

node_size(C)

boxtimes(G,{C,U},'mode','optimal_show')

boxtimes(boxtimes(G),boxtimes(C,U),'mode','optimal_show')

## Boxtimes memory

Once this order of computations is found, we may of course reuse this insight for the next analogous multiplication.

activate_boxtimes_mem()

clear_boxtimes_mem()

tic

boxtimes(G,{C,U},'mode','optimal');

toc

Once the boxtimes memory is activated, the required cpu time may reduce considerably:

tic

boxtimes(G,{C,U},'mode','optimal');

toc

The memory of boxtimes is smart, but does not overthink, since its main purpose is to avoid repeated calculations in loops:

So while it realizes that changing the order of mode names in the single node Cdoes not change it (since it has no dublicate mode names)...

CT = node_transpose(C)

tic

boxtimes(G,{CT,U},'mode','optimal');

toc

...it does not realize that transposing makes no difference either.

{C,U}

CUT = net_transpose({C,U})

tic

boxtimes(CUT,G,'mode','optimal');

toc

deactivate_boxtimes_mem()