Skip to content

Commit 62067c8

Browse files
author
VadimMitsenko
committed
Replacing the networkx library with a depth-first search
1 parent bc7d425 commit 62067c8

File tree

4 files changed

+37
-32
lines changed

4 files changed

+37
-32
lines changed

TM1py/Services/HierarchyService.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@
88

99
import json
1010
import math
11+
from collections import defaultdict
1112
from typing import Dict, Iterable, List, Optional, Tuple
1213

13-
try:
14-
import networkx as nx
15-
16-
_has_networkx = True
17-
except ImportError:
18-
_has_networkx = False
1914

2015
from requests import Response
2116

@@ -32,7 +27,6 @@
3227
case_and_space_insensitive_equals,
3328
format_url,
3429
require_data_admin,
35-
require_networkx,
3630
require_ops_admin,
3731
require_pandas,
3832
verify_version,
@@ -61,22 +55,47 @@ def __init__(self, rest: RestService):
6155
self.elements = ElementService(rest)
6256

6357
@staticmethod
64-
@require_networkx
6558
def _validate_edges(df: "pd.DataFrame"):
66-
graph = nx.DiGraph()
67-
for _, *record in df.itertuples():
68-
child = record[0]
69-
for parent in record[1:]:
59+
graph = defaultdict(list) # Build adjacency list (child -> list of parents)
60+
for row in df.itertuples(index=False, name=None):
61+
child, *parents = row
62+
for parent in parents:
7063
if not parent:
7164
continue
7265
if isinstance(parent, float) and math.isnan(parent):
7366
continue
74-
graph.add_edge(child, parent)
67+
graph[child].append(parent)
7568
child = parent
7669

77-
cycles = list(nx.simple_cycles(graph))
70+
visited = set() # nodes already fully explored
71+
active_path = set() # nodes currently being explored (for cycle detection)
72+
cycles = [] # stores detected cycles
73+
74+
def explore_relationships(node, path):
75+
if node in active_path:
76+
# Found a cycle: extract the part of the path that loops
77+
loop_start = path.index(node)
78+
cycles.append(path[loop_start:])
79+
return
80+
if node in visited:
81+
return
82+
83+
visited.add(node)
84+
active_path.add(node)
85+
86+
for parent in graph.get(node, []):
87+
explore_relationships(parent, path + [parent])
88+
89+
active_path.remove(node)
90+
91+
for node in graph:
92+
if node not in visited:
93+
explore_relationships(node, [node])
94+
7895
if cycles:
79-
raise ValueError(f"Circular reference{'s' if len(cycles) > 1 else ''} found in edges: {cycles}")
96+
raise ValueError(
97+
f"Circular reference{'s' if len(cycles) > 1 else ''} found in edges: {cycles}"
98+
)
8099

81100
@staticmethod
82101
def _validate_alias_uniqueness(df: "pd.DataFrame"):
@@ -493,7 +512,6 @@ def update_or_create_hierarchy_from_dataframe(
493512
Abort early if element names are not unique
494513
:param verify_edges:
495514
Abort early if edges contain a circular reference.
496-
`True` requires optional dependency `networkx`
497515
:param unwind_all: bool
498516
Unwind hierarch before creating new edges
499517
:param unwind_consolidations: list

TM1py/Utils/Utils.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,6 @@ def wrapper(self, *args, **kwargs):
165165
return wrapper
166166

167167

168-
@decohints
169-
def require_networkx(func):
170-
@functools.wraps(func)
171-
def wrapper(*args, **kwargs):
172-
try:
173-
import networkx # noqa: F401
174-
175-
return func(*args, **kwargs)
176-
except ImportError:
177-
raise ImportError(f"Function '{func.__name__}' requires networkx")
178-
179-
return wrapper
180-
181-
182168
def get_all_servers_from_adminhost(adminhost="localhost", port=None, use_ssl=False) -> List:
183169
from TM1py.Objects import Server
184170

Tests/HierarchyService_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,7 @@ def test_update_or_create_hierarchy_from_dataframe_circular_reference(self):
13051305
element_column=self.region_dimension_name,
13061306
element_type_column="ElementType",
13071307
unwind_all=True,
1308+
verify_edges=True
13081309
)
13091310

13101311
def test_update_or_create_hierarchy_from_dataframe_circular_references(self):
@@ -1336,6 +1337,7 @@ def test_update_or_create_hierarchy_from_dataframe_circular_references(self):
13361337
element_column=self.region_dimension_name,
13371338
element_type_column="ElementType",
13381339
unwind_all=True,
1340+
verify_edges=True
13391341
)
13401342

13411343
def test_update_or_create_hierarchy_from_dataframe_no_weight_columns(self):

Tests/_gh_README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,7 @@ install_requires=[
286286
'requests',
287287
'pytz',
288288
'requests_negotiate_sspi;platform_system=="Windows"',
289-
'mdxpy>=1.3.1',
290-
'networkx'],
289+
'mdxpy>=1.3.1'],
291290
extras_require={
292291
"pandas": ["pandas"],
293292
"dev": [

0 commit comments

Comments
 (0)