-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconnected_network.py
More file actions
155 lines (130 loc) · 6.29 KB
/
connected_network.py
File metadata and controls
155 lines (130 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from typing import Union, Optional, Tuple, Dict, Collection
from numpy.random import default_rng
# from opt_einsum.paths import _find_disconnected_subgraphs #NOTE: only for testing
from .util import get_symbol
Shapes = Collection[Tuple[int, ...]]
def connected_network(
number_of_tensors: int,
regularity: float,
number_of_output_indices: int = 0,
min_axis_size: int = 2,
max_axis_size: int = 10,
seed: Optional[int] = None,
global_dim: bool = False,
return_size_dict: bool = False,
) -> Union[Tuple[str, Shapes, Dict[str, int]], Tuple[str, Shapes]]:
"""Generate a random connected Tensor Network (TN).
Args:
number_of_tensors (int): Number of tensors/arrays in the TN.
regularity (float): 'Regularity' of the TN. This determines how
many indices/axes each tensor shares with others on average (not counting output indices and a global dimension).
number_of_output_indices (int, optional): Number of output indices/axes (i.e. the number of non-contracted indices) including the global dimension.
Defaults to 0 in case of no global dimension, i.e., a contraction resulting in a scalar, and to 1 in case there is a global dimension.
min_axis_size (int, optional): Minimum size of an axis/index (dimension) of the tensors.
max_axis_size (int, optional): Maximum size of an axis/index (dimension) of the tensors.
seed (int, optional): If not None, seed numpy's random generator with this.
global_dim (bool, optional): Add a global, 'broadcast', dimension to every operand.
return_size_dict (bool, optional): Return the mapping of indices to sizes.
Returns:
Tuple[str, List[Tuple[int]], Optional[Dict[str, int]]]: The einsum expression string, the shapes of the tensors/arrays, and the dict of index sizes (only returned if return_size_dict=True).
Example:
```python
eq, shapes, size_dict = random_tensor_network(
number_of_tensors = 10,
regularity = 3.5,
number_of_output_indices = 5,
min_axis_size = 2,
max_axis_size = 4,
return_size_dict = True,
global_dim = False,
seed = 12345
)
# Then, eq, shapes, and size_dict are:
eq = 'gafoj,mpab,uhlbcdn,cqlipe,drstk,ve,fk,ongmq,hj,i->sturv'
shapes = [(3, 4, 4, 2, 3), (3, 2, 4, 2), (4, 4, 2, 2, 4, 2, 3), (4, 2, 2, 4, 2, 2), (2, 4, 3, 4, 4), (2, 2), (4, 4), (2, 3, 3, 3, 2), (4, 3), (4,)]
size_dict = {'a': 4, 'b': 2, 'c': 4, 'd': 2, 'e': 2, 'f': 4, 'g': 3, 'h': 4, 'i': 4, 'j': 3, 'k': 4, 'l': 2, 'm': 3, 'n': 3, 'o': 2, 'p': 2, 'q': 2, 'r': 4, 's': 3, 't': 4, 'u': 4, 'v': 2}
```
"""
# handle inputs
assert (
number_of_tensors >= 0
), f"number_of_tensors {number_of_tensors} has to be non-negative."
assert regularity >= 0, f"regularity {regularity} has to be non-negative."
assert (
number_of_output_indices >= 0
), f"number_of_output_indices {number_of_output_indices} has to be non-negative."
assert min_axis_size >= 0, f"min_axis_size {min_axis_size} has to be non-negative."
assert max_axis_size >= 0, f"max_axis_size {max_axis_size} has to be non-negative."
# create rng
if seed is None:
rng = default_rng()
else:
rng = default_rng(seed)
# total number of indices
assert (
number_of_output_indices > 0
) or not global_dim, f"If a global dimension is to be used, the number of output indices has to be at least 1."
number_of_output_indices -= (
1 * global_dim
) # reserve one output index for global dimension
number_of_indices = (
int(number_of_tensors * regularity) // 2 + number_of_output_indices
) # NOTE: output indices are not counted for degree.
tensors = []
output = []
size_dict = {
get_symbol(i): rng.integers(min_axis_size, max_axis_size + 1)
for i in range(number_of_indices)
}
# generate TN as einsum string
for index_number, index in enumerate(size_dict):
# generate first two tensors connected by an edge to start with
if index_number == 0:
tensors.append(index)
tensors.append(index)
continue
# generate a bound/edge
if index_number < number_of_indices - number_of_output_indices:
# add tensors and connect to existing tensors, until number of tensors is reached
if len(tensors) < number_of_tensors:
connect_to_tensor = rng.integers(0, len(tensors))
tensors[connect_to_tensor] += index
tensors.append(index)
# add edges between existing tensors
else:
tensor_1 = rng.integers(0, len(tensors))
tensor_2 = rng.integers(0, len(tensors))
while tensor_2 == tensor_1:
tensor_2 = rng.integers(0, len(tensors))
tensors[tensor_1] += index
tensors[tensor_2] += index
# generate an output index
else:
tensor = rng.integers(0, len(tensors))
tensors[tensor] += index
output += index
# check specs
assert (
len(tensors) == number_of_tensors
), f"number generated tensors/tensors = {len(tensors)} does not match number_of_tensors = {number_of_tensors}."
assert (
len(output) == number_of_output_indices
), f"number of generated output indices = {len(output)} does not match number_of_output_indices = {number_of_output_indices}."
# assert len(_find_disconnected_subgraphs([set(input) for input in tensors], set(output))) == 1, "the generated graph is not connected." # check if graph is connected
# possibly add the same global dim to every arg
if global_dim:
gdim = get_symbol(number_of_indices)
size_dict[gdim] = rng.integers(min_axis_size, max_axis_size + 1)
for i in range(number_of_tensors):
tensors[i] += gdim
output += gdim
# randomly transpose the output indices and form equation
output = "".join(rng.permutation(output))
tensors = ["".join(rng.permutation(list(tensor))) for tensor in tensors]
eq = "{}->{}".format(",".join(tensors), output)
# make the shapes
shapes = [tuple(size_dict[ix] for ix in op) for op in tensors]
ret = (eq, shapes)
if return_size_dict:
ret += (size_dict,)
return ret