Skip to content

Commit 0f1c622

Browse files
committed
Paths is picklable
fix in permutations computations for dag extraction
1 parent 557e597 commit 0f1c622

File tree

5 files changed

+132
-29
lines changed

5 files changed

+132
-29
lines changed

pathpy/classes/paths.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import numpy as np
2929
from pathpy.utils import Log, Severity
3030
from pathpy.utils.exceptions import PathpyError
31+
from pathpy.utils.default_containers import nested_zero_default as _nested_zero_default
32+
from pathpy.utils.default_containers import zero_array_default as _zero_array_default
3133

3234

3335
class Paths:
@@ -54,8 +56,7 @@ def __init__(self, separator=','):
5456
# subpath of a longer path, and j refers to the number of times p
5557
# occurs as a *real* or *longest* path (i.e. not being a subpath
5658
# of a longer path)
57-
self.paths = defaultdict(
58-
lambda: defaultdict(lambda: np.array([0.0, 0.0])))
59+
self.paths = _nested_zero_default()
5960

6061
# The character used to separate nodes on paths
6162
self.separator = separator
@@ -145,7 +146,7 @@ def path_lengths(self):
145146
146147
147148
"""
148-
lengths = defaultdict(lambda: np.array([0., 0.]))
149+
lengths = _zero_array_default()
149150

150151
for k in self.paths:
151152
for p in self.paths[k]:

pathpy/path_extraction/dag_paths.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import sys
2525
import itertools as it
2626
import functools as ft
27-
import random
2827
from collections import defaultdict
2928

3029
from tqdm import tqdm
@@ -54,9 +53,11 @@ def paths_from_dag(dag, node_mapping=None, max_subpath_length=None, separator=',
5453
5554
"""
5655
# Try to topologically sort the graph if not already sorted
57-
58-
test_key = list(node_mapping.keys())[0]
59-
ONE_TO_MANY = isinstance(node_mapping[test_key], set)
56+
if node_mapping:
57+
test_key = list(node_mapping.keys())[0]
58+
ONE_TO_MANY = isinstance(node_mapping[test_key], set)
59+
else:
60+
ONE_TO_MANY = False
6061

6162
if dag.is_acyclic is None:
6263
dag.topsort()
@@ -84,8 +85,7 @@ def paths_from_dag(dag, node_mapping=None, max_subpath_length=None, separator=',
8485
path_counter = defaultdict(lambda: 0)
8586
for root in tqdm(dag.roots, unit='roots', desc='count unique paths'):
8687
for set_path in dag.routes_from_node(root, node_mapping):
87-
blown_up_paths = blowup_set_paths(set_path)
88-
for blown_up_path in blown_up_paths:
88+
for blown_up_path in expand_set_paths(set_path):
8989
path_counter[blown_up_path] += 1
9090

9191
for path, count in tqdm(path_counter.items(), unit='path', desc='add paths'):
@@ -97,7 +97,7 @@ def paths_from_dag(dag, node_mapping=None, max_subpath_length=None, separator=',
9797
return p
9898

9999

100-
def blowup_set_paths(set_path):
100+
def expand_set_paths(set_path):
101101
"""returns all possible paths which are consistent with the sequence of sets
102102
103103
Parameters
@@ -107,29 +107,36 @@ def blowup_set_paths(set_path):
107107
108108
Examples
109109
-------
110-
>>> node_path = [{'320', '324'}, {'324'}, {'324', '429'}]
111-
>>> blowup_set_paths(node_path)
112-
[('324', '324', '324'),
113-
('320', '324', '429'),
114-
('324', '324', '324'),
115-
('320', '324', '429')]
116-
117-
118-
119-
Returns
120-
-------
121-
list
122-
a list of paths
110+
>>> node_path = [{1, 2}, {2, 5}, {1, 2}]
111+
>>> list(expand_set_paths(node_path))
112+
[(1, 2, 1), (2, 2, 1), (1, 5, 1), (2, 5, 1), (1, 2, 2), (2, 2, 2), (1, 5, 2), (2, 5, 2)]
113+
>>> node_path = [{1, 2}, {5}, {2, 5}]
114+
>>> list(expand_set_paths(node_path))
115+
[(1, 5, 2), (2, 5, 2), (1, 5, 5), (2, 5, 5)]
116+
117+
118+
Yields
119+
------
120+
tuple
121+
a possible path
123122
"""
124123
# how many possible combinations are there
125-
node_sizes = (len(n) for n in set_path)
124+
node_sizes = [len(n) for n in set_path]
126125
num_possibilities = ft.reduce(lambda x, y: x * y, node_sizes, 1)
127126

128-
all_paths = []
129-
iterator = [it.cycle(node_set) for node_set in set_path]
127+
# create a list of lists such that each iterator is repeated the number of times
128+
# his predecessors have completed their cycle
129+
all_periodics = []
130+
current_length = 1
131+
for node_set in set_path:
132+
periodic_num = []
133+
for num in node_set:
134+
periodic_num.extend([num] * current_length)
135+
current_length *= len(node_set)
136+
all_periodics.append(periodic_num)
137+
138+
iterator = [it.cycle(periodic) for periodic in all_periodics]
130139
for i, elements in enumerate(zip(*iterator)):
131140
if i >= num_possibilities:
132141
break
133-
all_paths.append(elements)
134-
return all_paths
135-
142+
yield elements

pathpy/utils/default_containers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""This module contains the containers for the various classes
2+
which are used to store nodes, edges and similar
3+
4+
To make the various classes pickle-able the defaultdicts need to be publicly addressable
5+
function names, this means that no lambda functions are allowed.
6+
7+
All Pathpy classes which required a default value as a container, should use these here.
8+
"""
9+
from collections import defaultdict
10+
import numpy as np
11+
12+
13+
def nested_zero_default():
14+
"""returns a nested default dict (2 levels)
15+
with a numpy zero array of length 0 as default
16+
"""
17+
return defaultdict(zero_array_default)
18+
19+
20+
def _zero_array():
21+
"""returns a zero numpy array of length 2"""
22+
return np.array([0.0, 0.0])
23+
24+
25+
def zero_array_default():
26+
"""returns a default dict with numpy zero array af length 2 as default"""
27+
return defaultdict(_zero_array)

tests/test_DAG.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@ def test_dag_path_mapping(dag_object):
8383
assert paths_mapped2.observation_count == 7
8484

8585

86+
def test_dag_path_mapping_to_many(dag_object):
87+
dag = dag_object
88+
dag.topsort()
89+
90+
mapping = {
91+
'a': {1, 2},
92+
'b': {2, 5},
93+
'c': {5},
94+
'e': {1, 2},
95+
'f': {2, 3},
96+
'g': {2, 5},
97+
'h': {1},
98+
'i': {1, 5},
99+
'j': {4}
100+
}
101+
paths_mapped2 = pp.path_extraction.paths_from_dag(dag, node_mapping=mapping)
102+
103+
assert paths_mapped2.observation_count is None
104+
105+
86106
edges1, types1 = [(1, 2), (1, 3), (2, 3)], ({1}, {2}, {3})
87107
edges2 = [(1, 2), (1, 3), (2, 3), (3, 7), (4, 2), (4, 5), (4, 6), (5, 7), (6, 5)]
88108
types2 = ({1, 4}, {2, 6, 3, 5}, {7})
@@ -128,3 +148,29 @@ def test_remove_edge(dag_object: pp.DAG):
128148
assert 'e' in dag_object.roots
129149
assert 'e' in dag_object.leafs
130150
assert 'e' in dag_object.isolate_nodes()
151+
152+
153+
def test_set_path_expansion(dag_object: pp.DAG):
154+
from collections import defaultdict
155+
mapping = {'a': {1, 2},
156+
'b': {2, 5},
157+
'c': {5},
158+
'e': {1, 2},
159+
'f': {2, 3},
160+
'g': {2, 5},
161+
'h': {1},
162+
'i': {1, 5},
163+
'j': {4}
164+
}
165+
166+
path_count = defaultdict(lambda: 0)
167+
# for root in dag_object.roots:
168+
# for set_path in dag_object.routes_from_node(root):
169+
170+
171+
172+
173+
174+
175+
176+

tests/test_Path.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,25 @@ def test_path_multiplication(random_paths, factor):
230230
mult_inplace *= factor
231231

232232
assert sum(mult_paths.paths[2][TEST_PATH]) == sum(mult_inplace.paths[2][TEST_PATH])
233+
234+
235+
def test_pickle(random_paths, tmpdir):
236+
import pickle
237+
from pathpy import Paths
238+
239+
dir_path = tmpdir.join("test_path.pkl")
240+
paths = random_paths(90, 0, 20)
241+
242+
with open(str(dir_path), 'wb') as f:
243+
pickle.dump(paths, f)
244+
245+
with open(str(dir_path), 'rb') as f:
246+
paths_back = pickle.load(f) # type: Paths
247+
248+
assert paths.diameter() == paths_back.diameter()
249+
assert paths.paths.keys() == paths_back.paths.keys()
250+
assert paths.observation_count == paths.observation_count
251+
252+
253+
254+

0 commit comments

Comments
 (0)