added 3rd party packages, elog, bigtree

This commit is contained in:
2024-02-27 15:40:00 +01:00
parent 277c22f800
commit 6b59fe16ce
69 changed files with 17449 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
from typing import Any, Dict, List
def assert_style_in_dict(
parameter: Any,
accepted_parameters: Dict[str, Any],
) -> None:
"""Raise ValueError is parameter is not in list of accepted parameters
Args:
parameter (Any): argument input for parameter
accepted_parameters (List[Any]): list of accepted parameters
"""
if parameter not in accepted_parameters and parameter != "custom":
raise ValueError(
f"Choose one of {accepted_parameters.keys()} style, use `custom` to define own style"
)
def assert_str_in_list(
parameter_name: str,
parameter: Any,
accepted_parameters: List[Any],
) -> None:
"""Raise ValueError is parameter is not in list of accepted parameters
Args:
parameter_name (str): parameter name for error message
parameter (Any): argument input for parameter
accepted_parameters (List[Any]): list of accepted parameters
"""
if parameter not in accepted_parameters:
raise ValueError(
f"Invalid input, check `{parameter_name}` should be one of {accepted_parameters}"
)
def assert_key_in_dict(
parameter_name: str,
parameter: Any,
accepted_parameters: Dict[Any, Any],
) -> None:
"""Raise ValueError is parameter is not in key of dictionary
Args:
parameter_name (str): parameter name for error message
parameter (Any): argument input for parameter
accepted_parameters (Dict[Any]): dictionary of accepted parameters
"""
if parameter not in accepted_parameters:
raise ValueError(
f"Invalid input, check `{parameter_name}` should be one of {accepted_parameters.keys()}"
)

View File

@@ -0,0 +1,165 @@
from enum import Enum, auto
from typing import Dict, List, Tuple
class ExportConstants:
DOWN_RIGHT = "\u250c"
VERTICAL_RIGHT = "\u251c"
VERTICAL_LEFT = "\u2524"
VERTICAL_HORIZONTAL = "\u253c"
UP_RIGHT = "\u2514"
VERTICAL = "\u2502"
HORIZONTAL = "\u2500"
DOWN_RIGHT_ROUNDED = "\u256D"
UP_RIGHT_ROUNDED = "\u2570"
DOWN_RIGHT_BOLD = "\u250F"
VERTICAL_RIGHT_BOLD = "\u2523"
VERTICAL_LEFT_BOLD = "\u252B"
VERTICAL_HORIZONTAL_BOLD = "\u254B"
UP_RIGHT_BOLD = "\u2517"
VERTICAL_BOLD = "\u2503"
HORIZONTAL_BOLD = "\u2501"
DOWN_RIGHT_DOUBLE = "\u2554"
VERTICAL_RIGHT_DOUBLE = "\u2560"
VERTICAL_LEFT_DOUBLE = "\u2563"
VERTICAL_HORIZONTAL_DOUBLE = "\u256C"
UP_RIGHT_DOUBLE = "\u255a"
VERTICAL_DOUBLE = "\u2551"
HORIZONTAL_DOUBLE = "\u2550"
PRINT_STYLES: Dict[str, Tuple[str, str, str]] = {
"ansi": ("| ", "|-- ", "`-- "),
"ascii": ("| ", "|-- ", "+-- "),
"const": (
f"{VERTICAL} ",
f"{VERTICAL_RIGHT}{HORIZONTAL}{HORIZONTAL} ",
f"{UP_RIGHT}{HORIZONTAL}{HORIZONTAL} ",
),
"const_bold": (
f"{VERTICAL_BOLD} ",
f"{VERTICAL_RIGHT_BOLD}{HORIZONTAL_BOLD}{HORIZONTAL_BOLD} ",
f"{UP_RIGHT_BOLD}{HORIZONTAL_BOLD}{HORIZONTAL_BOLD} ",
),
"rounded": (
f"{VERTICAL} ",
f"{VERTICAL_RIGHT}{HORIZONTAL}{HORIZONTAL} ",
f"{UP_RIGHT_ROUNDED}{HORIZONTAL}{HORIZONTAL} ",
),
"double": (
f"{VERTICAL_DOUBLE} ",
f"{VERTICAL_RIGHT_DOUBLE}{HORIZONTAL_DOUBLE}{HORIZONTAL_DOUBLE} ",
f"{UP_RIGHT_DOUBLE}{HORIZONTAL_DOUBLE}{HORIZONTAL_DOUBLE} ",
),
}
HPRINT_STYLES: Dict[str, Tuple[str, str, str, str, str, str, str]] = {
"ansi": ("/", "+", "+", "+", "\\", "|", "-"),
"ascii": ("+", "+", "+", "+", "+", "|", "-"),
"const": (
DOWN_RIGHT,
VERTICAL_RIGHT,
VERTICAL_LEFT,
VERTICAL_HORIZONTAL,
UP_RIGHT,
VERTICAL,
HORIZONTAL,
),
"const_bold": (
DOWN_RIGHT_BOLD,
VERTICAL_RIGHT_BOLD,
VERTICAL_LEFT_BOLD,
VERTICAL_HORIZONTAL_BOLD,
UP_RIGHT_BOLD,
VERTICAL_BOLD,
HORIZONTAL_BOLD,
),
"rounded": (
DOWN_RIGHT_ROUNDED,
VERTICAL_RIGHT,
VERTICAL_LEFT,
VERTICAL_HORIZONTAL,
UP_RIGHT_ROUNDED,
VERTICAL,
HORIZONTAL,
),
"double": (
DOWN_RIGHT_DOUBLE,
VERTICAL_RIGHT_DOUBLE,
VERTICAL_LEFT_DOUBLE,
VERTICAL_HORIZONTAL_DOUBLE,
UP_RIGHT_DOUBLE,
VERTICAL_DOUBLE,
HORIZONTAL_DOUBLE,
),
}
class MermaidConstants:
RANK_DIR: List[str] = ["TB", "BT", "LR", "RL"]
LINE_SHAPES: List[str] = [
"basis",
"bumpX",
"bumpY",
"cardinal",
"catmullRom",
"linear",
"monotoneX",
"monotoneY",
"natural",
"step",
"stepAfter",
"stepBefore",
]
NODE_SHAPES: Dict[str, str] = {
"rounded_edge": """("{label}")""",
"stadium": """(["{label}"])""",
"subroutine": """[["{label}"]]""",
"cylindrical": """[("{label}")]""",
"circle": """(("{label}"))""",
"asymmetric": """>"{label}"]""",
"rhombus": """{{"{label}"}}""",
"hexagon": """{{{{"{label}"}}}}""",
"parallelogram": """[/"{label}"/]""",
"parallelogram_alt": """[\\"{label}"\\]""",
"trapezoid": """[/"{label}"\\]""",
"trapezoid_alt": """[\\"{label}"/]""",
"double_circle": """((("{label}")))""",
}
EDGE_ARROWS: Dict[str, str] = {
"normal": "-->",
"bold": "==>",
"dotted": "-.->",
"open": "---",
"bold_open": "===",
"dotted_open": "-.-",
"invisible": "~~~",
"circle": "--o",
"cross": "--x",
"double_normal": "<-->",
"double_circle": "o--o",
"double_cross": "x--x",
}
class NewickState(Enum):
PARSE_STRING = auto()
PARSE_ATTRIBUTE_NAME = auto()
PARSE_ATTRIBUTE_VALUE = auto()
class NewickCharacter(str, Enum):
OPEN_BRACKET = "("
CLOSE_BRACKET = ")"
ATTR_START = "["
ATTR_END = "]"
ATTR_KEY_VALUE = "="
ATTR_QUOTE = "'"
SEP = ":"
NODE_SEP = ","
@classmethod
def values(cls) -> List[str]:
return [c.value for c in cls]

View File

@@ -0,0 +1,126 @@
from functools import wraps
from typing import Any, Callable, TypeVar
from warnings import simplefilter, warn
T = TypeVar("T")
class TreeError(Exception):
"""Generic tree exception"""
pass
class LoopError(TreeError):
"""Error during node creation"""
pass
class CorruptedTreeError(TreeError):
"""Error during node creation"""
pass
class DuplicatedNodeError(TreeError):
"""Error during tree creation"""
pass
class NotFoundError(TreeError):
"""Error during tree pruning or modification"""
pass
class SearchError(TreeError):
"""Error during tree search"""
pass
def deprecated(
alias: str,
) -> Callable[[Callable[..., T]], Callable[..., T]]: # pragma: no cover
def decorator(func: Callable[..., T]) -> Callable[..., T]:
"""
This is a decorator which can be used to mark functions as deprecated.
It will raise a DeprecationWarning when the function is used.
Source: https://stackoverflow.com/a/30253848
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
simplefilter("always", DeprecationWarning)
warn(
"{old_func} is going to be deprecated, use {new_func} instead".format(
old_func=func.__name__,
new_func=alias,
),
category=DeprecationWarning,
stacklevel=2,
)
simplefilter("default", DeprecationWarning) # reset filter
return func(*args, **kwargs)
return wrapper
return decorator
def optional_dependencies_pandas(
func: Callable[..., T]
) -> Callable[..., T]: # pragma: no cover
"""
This is a decorator which can be used to import optional pandas dependency.
It will raise a ImportError if the module is not found.
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
try:
import pandas as pd # noqa: F401
except ImportError:
raise ImportError(
"pandas not available. Please perform a\n\n"
"pip install 'bigtree[pandas]'\n\nto install required dependencies"
) from None
return func(*args, **kwargs)
return wrapper
def optional_dependencies_image(
package_name: str = "",
) -> Callable[[Callable[..., T]], Callable[..., T]]:
def decorator(func: Callable[..., T]) -> Callable[..., T]:
"""
This is a decorator which can be used to import optional image dependency.
It will raise a ImportError if the module is not found.
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
if not package_name or package_name == "pydot":
try:
import pydot # noqa: F401
except ImportError: # pragma: no cover
raise ImportError(
"pydot not available. Please perform a\n\n"
"pip install 'bigtree[image]'\n\nto install required dependencies"
) from None
if not package_name or package_name == "Pillow":
try:
from PIL import Image, ImageDraw, ImageFont # noqa: F401
except ImportError: # pragma: no cover
raise ImportError(
"Pillow not available. Please perform a\n\n"
"pip install 'bigtree[image]'\n\nto install required dependencies"
) from None
return func(*args, **kwargs)
return wrapper
return decorator

View File

@@ -0,0 +1,19 @@
def whoami() -> str:
"""Groot utils
Returns:
(str)
"""
return "I am Groot!"
def speak_like_groot(sentence: str) -> str:
"""Convert sentence into Groot langauge
Args:
sentence (str): Sentence to convert to groot language
Returns:
(str)
"""
return " ".join([whoami() for _ in range(len(sentence.split()))])

View File

@@ -0,0 +1,587 @@
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Callable,
Iterable,
List,
Optional,
Tuple,
TypeVar,
Union,
)
if TYPE_CHECKING:
from bigtree.node.basenode import BaseNode
from bigtree.node.binarynode import BinaryNode
from bigtree.node.dagnode import DAGNode
BaseNodeT = TypeVar("BaseNodeT", bound=BaseNode)
BinaryNodeT = TypeVar("BinaryNodeT", bound=BinaryNode)
DAGNodeT = TypeVar("DAGNodeT", bound=DAGNode)
T = TypeVar("T", bound=Union[BaseNode, DAGNode])
__all__ = [
"inorder_iter",
"preorder_iter",
"postorder_iter",
"levelorder_iter",
"levelordergroup_iter",
"zigzag_iter",
"zigzaggroup_iter",
"dag_iterator",
]
def inorder_iter(
tree: BinaryNodeT,
filter_condition: Optional[Callable[[BinaryNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[BinaryNodeT]:
"""Iterate through all children of a tree.
In-Order Iteration Algorithm, LNR
1. Recursively traverse the current node's left subtree.
2. Visit the current node.
3. Recursively traverse the current node's right subtree.
Examples:
>>> from bigtree import BinaryNode, list_to_binarytree, inorder_iter
>>> num_list = [1, 2, 3, 4, 5, 6, 7, 8]
>>> root = list_to_binarytree(num_list)
>>> root.show()
1
├── 2
│ ├── 4
│ │ └── 8
│ └── 5
└── 3
├── 6
└── 7
>>> [node.node_name for node in inorder_iter(root)]
['8', '4', '2', '5', '1', '6', '3', '7']
>>> [node.node_name for node in inorder_iter(root, filter_condition=lambda x: x.node_name in ["1", "4", "3", "6", "7"])]
['4', '1', '6', '3', '7']
>>> [node.node_name for node in inorder_iter(root, max_depth=3)]
['4', '2', '5', '1', '6', '3', '7']
Args:
tree (BinaryNode): input tree
filter_condition (Optional[Callable[[BinaryNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Iterable[BinaryNode])
"""
if tree and (not max_depth or not tree.depth > max_depth):
yield from inorder_iter(tree.left, filter_condition, max_depth)
if not filter_condition or filter_condition(tree):
yield tree
yield from inorder_iter(tree.right, filter_condition, max_depth)
def preorder_iter(
tree: T,
filter_condition: Optional[Callable[[T], bool]] = None,
stop_condition: Optional[Callable[[T], bool]] = None,
max_depth: int = 0,
) -> Iterable[T]:
"""Iterate through all children of a tree.
Pre-Order Iteration Algorithm, NLR
1. Visit the current node.
2. Recursively traverse the current node's left subtree.
3. Recursively traverse the current node's right subtree.
It is topologically sorted because a parent node is processed before its child nodes.
Examples:
>>> from bigtree import Node, list_to_tree, preorder_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in preorder_iter(root)]
['a', 'b', 'd', 'e', 'g', 'h', 'c', 'f']
>>> [node.node_name for node in preorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['a', 'd', 'e', 'g', 'f']
>>> [node.node_name for node in preorder_iter(root, stop_condition=lambda x: x.node_name == "e")]
['a', 'b', 'd', 'c', 'f']
>>> [node.node_name for node in preorder_iter(root, max_depth=3)]
['a', 'b', 'd', 'e', 'c', 'f']
Args:
tree (Union[BaseNode, DAGNode]): input tree
filter_condition (Optional[Callable[[T], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[T], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Union[Iterable[BaseNode], Iterable[DAGNode]])
"""
if (
tree
and (not max_depth or not tree.get_attr("depth") > max_depth)
and (not stop_condition or not stop_condition(tree))
):
if not filter_condition or filter_condition(tree):
yield tree
for child in tree.children:
yield from preorder_iter(child, filter_condition, stop_condition, max_depth) # type: ignore
def postorder_iter(
tree: BaseNodeT,
filter_condition: Optional[Callable[[BaseNodeT], bool]] = None,
stop_condition: Optional[Callable[[BaseNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[BaseNodeT]:
"""Iterate through all children of a tree.
Post-Order Iteration Algorithm, LRN
1. Recursively traverse the current node's left subtree.
2. Recursively traverse the current node's right subtree.
3. Visit the current node.
Examples:
>>> from bigtree import Node, list_to_tree, postorder_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in postorder_iter(root)]
['d', 'g', 'h', 'e', 'b', 'f', 'c', 'a']
>>> [node.node_name for node in postorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['d', 'g', 'e', 'f', 'a']
>>> [node.node_name for node in postorder_iter(root, stop_condition=lambda x: x.node_name == "e")]
['d', 'b', 'f', 'c', 'a']
>>> [node.node_name for node in postorder_iter(root, max_depth=3)]
['d', 'e', 'b', 'f', 'c', 'a']
Args:
tree (BaseNode): input tree
filter_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Iterable[BaseNode])
"""
if (
tree
and (not max_depth or not tree.depth > max_depth)
and (not stop_condition or not stop_condition(tree))
):
for child in tree.children:
yield from postorder_iter(
child, filter_condition, stop_condition, max_depth
)
if not filter_condition or filter_condition(tree):
yield tree
def levelorder_iter(
tree: BaseNodeT,
filter_condition: Optional[Callable[[BaseNodeT], bool]] = None,
stop_condition: Optional[Callable[[BaseNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[BaseNodeT]:
"""Iterate through all children of a tree.
Level-Order Iteration Algorithm
1. Recursively traverse the nodes on same level.
Examples:
>>> from bigtree import Node, list_to_tree, levelorder_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in levelorder_iter(root)]
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
>>> [node.node_name for node in levelorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['a', 'd', 'e', 'f', 'g']
>>> [node.node_name for node in levelorder_iter(root, stop_condition=lambda x: x.node_name == "e")]
['a', 'b', 'c', 'd', 'f']
>>> [node.node_name for node in levelorder_iter(root, max_depth=3)]
['a', 'b', 'c', 'd', 'e', 'f']
Args:
tree (BaseNode): input tree
filter_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[BaseNode])
"""
def _levelorder_iter(trees: List[BaseNodeT]) -> Iterable[BaseNodeT]:
"""Iterate through all children of a tree.
Args:
trees (List[BaseNode]): trees to get children for next level
Returns:
(Iterable[BaseNode])
"""
next_level = []
for _tree in trees:
if _tree:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
yield _tree
next_level.extend(list(_tree.children))
if len(next_level):
yield from _levelorder_iter(next_level)
yield from _levelorder_iter([tree])
def levelordergroup_iter(
tree: BaseNodeT,
filter_condition: Optional[Callable[[BaseNodeT], bool]] = None,
stop_condition: Optional[Callable[[BaseNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[Iterable[BaseNodeT]]:
"""Iterate through all children of a tree.
Level-Order Group Iteration Algorithm
1. Recursively traverse the nodes on same level, returns nodes level by level in a nested list.
Examples:
>>> from bigtree import Node, list_to_tree, levelordergroup_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root)]
[['a'], ['b', 'c'], ['d', 'e', 'f'], ['g', 'h']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
[['a'], [], ['d', 'e', 'f'], ['g']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, stop_condition=lambda x: x.node_name == "e")]
[['a'], ['b', 'c'], ['d', 'f']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, max_depth=3)]
[['a'], ['b', 'c'], ['d', 'e', 'f']]
Args:
tree (BaseNode): input tree
filter_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[Iterable[BaseNode]])
"""
def _levelordergroup_iter(trees: List[BaseNodeT]) -> Iterable[Iterable[BaseNodeT]]:
"""Iterate through all children of a tree.
Args:
trees (List[BaseNode]): trees to get children for next level
Returns:
(Iterable[Iterable[BaseNode]])
"""
current_tree = []
next_level = []
for _tree in trees:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
current_tree.append(_tree)
next_level.extend([_child for _child in _tree.children if _child])
yield tuple(current_tree)
if len(next_level) and (not max_depth or not next_level[0].depth > max_depth):
yield from _levelordergroup_iter(next_level)
yield from _levelordergroup_iter([tree])
def zigzag_iter(
tree: BaseNodeT,
filter_condition: Optional[Callable[[BaseNodeT], bool]] = None,
stop_condition: Optional[Callable[[BaseNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[BaseNodeT]:
"""Iterate through all children of a tree.
ZigZag Iteration Algorithm
1. Recursively traverse the nodes on same level, in a zigzag manner across different levels.
Examples:
>>> from bigtree import Node, list_to_tree, zigzag_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in zigzag_iter(root)]
['a', 'c', 'b', 'd', 'e', 'f', 'h', 'g']
>>> [node.node_name for node in zigzag_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['a', 'd', 'e', 'f', 'g']
>>> [node.node_name for node in zigzag_iter(root, stop_condition=lambda x: x.node_name == "e")]
['a', 'c', 'b', 'd', 'f']
>>> [node.node_name for node in zigzag_iter(root, max_depth=3)]
['a', 'c', 'b', 'd', 'e', 'f']
Args:
tree (BaseNode): input tree
filter_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[BaseNode])
"""
def _zigzag_iter(
trees: List[BaseNodeT], reverse_indicator: bool = False
) -> Iterable[BaseNodeT]:
"""Iterate through all children of a tree.
Args:
trees (List[BaseNode]): trees to get children for next level
reverse_indicator (bool): indicator whether it is in reverse order
Returns:
(Iterable[BaseNode])
"""
next_level = []
for _tree in trees:
if _tree:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
yield _tree
next_level_nodes = list(_tree.children)
if reverse_indicator:
next_level_nodes = next_level_nodes[::-1]
next_level.extend(next_level_nodes)
if len(next_level):
yield from _zigzag_iter(
next_level[::-1], reverse_indicator=not reverse_indicator
)
yield from _zigzag_iter([tree])
def zigzaggroup_iter(
tree: BaseNodeT,
filter_condition: Optional[Callable[[BaseNodeT], bool]] = None,
stop_condition: Optional[Callable[[BaseNodeT], bool]] = None,
max_depth: int = 0,
) -> Iterable[Iterable[BaseNodeT]]:
"""Iterate through all children of a tree.
ZigZag Group Iteration Algorithm
1. Recursively traverse the nodes on same level, in a zigzag manner across different levels,
returns nodes level by level in a nested list.
Examples:
>>> from bigtree import Node, list_to_tree, zigzaggroup_iter
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [[node.node_name for node in group] for group in zigzaggroup_iter(root)]
[['a'], ['c', 'b'], ['d', 'e', 'f'], ['h', 'g']]
>>> [[node.node_name for node in group] for group in zigzaggroup_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
[['a'], [], ['d', 'e', 'f'], ['g']]
>>> [[node.node_name for node in group] for group in zigzaggroup_iter(root, stop_condition=lambda x: x.node_name == "e")]
[['a'], ['c', 'b'], ['d', 'f']]
>>> [[node.node_name for node in group] for group in zigzaggroup_iter(root, max_depth=3)]
[['a'], ['c', 'b'], ['d', 'e', 'f']]
Args:
tree (BaseNode): input tree
filter_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Return node if condition evaluates to `True`
stop_condition (Optional[Callable[[BaseNode], bool]]): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[Iterable[BaseNode]])
"""
def _zigzaggroup_iter(
trees: List[BaseNodeT], reverse_indicator: bool = False
) -> Iterable[Iterable[BaseNodeT]]:
"""Iterate through all children of a tree.
Args:
trees (List[BaseNode]): trees to get children for next level
reverse_indicator (bool): indicator whether it is in reverse order
Returns:
(Iterable[Iterable[BaseNode]])
"""
current_tree = []
next_level = []
for _tree in trees:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
current_tree.append(_tree)
next_level_nodes = [_child for _child in _tree.children if _child]
if reverse_indicator:
next_level_nodes = next_level_nodes[::-1]
next_level.extend(next_level_nodes)
yield tuple(current_tree)
if len(next_level) and (not max_depth or not next_level[0].depth > max_depth):
yield from _zigzaggroup_iter(
next_level[::-1], reverse_indicator=not reverse_indicator
)
yield from _zigzaggroup_iter([tree])
def dag_iterator(dag: DAGNodeT) -> Iterable[Tuple[DAGNodeT, DAGNodeT]]:
"""Iterate through all nodes of a Directed Acyclic Graph (DAG).
Note that node names must be unique.
Note that DAG must at least have two nodes to be shown on graph.
1. Visit the current node.
2. Recursively traverse the current node's parents.
3. Recursively traverse the current node's children.
Examples:
>>> from bigtree import DAGNode, dag_iterator
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> [(parent.node_name, child.node_name) for parent, child in dag_iterator(a)]
[('a', 'c'), ('a', 'd'), ('b', 'c'), ('c', 'd'), ('d', 'e')]
Args:
dag (DAGNode): input dag
Returns:
(Iterable[Tuple[DAGNode, DAGNode]])
"""
visited_nodes = set()
def _dag_iterator(node: DAGNodeT) -> Iterable[Tuple[DAGNodeT, DAGNodeT]]:
"""Iterate through all children of a DAG.
Args:
node (DAGNode): current node
Returns:
(Iterable[Tuple[DAGNode, DAGNode]])
"""
node_name = node.node_name
visited_nodes.add(node_name)
# Parse upwards
for parent in node.parents:
parent_name = parent.node_name
if parent_name not in visited_nodes:
yield parent, node
# Parse downwards
for child in node.children:
child_name = child.node_name
if child_name not in visited_nodes:
yield node, child
# Parse upwards
for parent in node.parents:
parent_name = parent.node_name
if parent_name not in visited_nodes:
yield from _dag_iterator(parent)
# Parse downwards
for child in node.children:
child_name = child.node_name
if child_name not in visited_nodes:
yield from _dag_iterator(child)
yield from _dag_iterator(dag)

View File

@@ -0,0 +1,354 @@
from typing import Optional, TypeVar
from bigtree.node.basenode import BaseNode
T = TypeVar("T", bound=BaseNode)
__all__ = [
"reingold_tilford",
]
def reingold_tilford(
tree_node: T,
sibling_separation: float = 1.0,
subtree_separation: float = 1.0,
level_separation: float = 1.0,
x_offset: float = 0.0,
y_offset: float = 0.0,
) -> None:
"""
Algorithm for drawing tree structure, retrieves `(x, y)` coordinates for a tree structure.
Adds `x` and `y` attributes to every node in the tree. Modifies tree in-place.
This algorithm[1] is an improvement over Reingold Tilford algorithm[2].
According to Reingold Tilford's paper, a tree diagram should satisfy the following aesthetic rules,
1. Nodes at the same depth should lie along a straight line, and the straight lines defining the depths should be parallel.
2. A left child should be positioned to the left of its parent node and a right child to the right.
3. A parent should be centered over their children.
4. A tree and its mirror image should produce drawings that are reflections of one another; a subtree should be drawn the same way regardless of where it occurs in the tree.
Examples:
>>> from bigtree import reingold_tilford, list_to_tree
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> root.show()
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> reingold_tilford(root)
>>> root.show(attr_list=["x", "y"])
a [x=1.25, y=3.0]
├── b [x=0.5, y=2.0]
│ ├── d [x=0.0, y=1.0]
│ └── e [x=1.0, y=1.0]
│ ├── g [x=0.5, y=0.0]
│ └── h [x=1.5, y=0.0]
└── c [x=2.0, y=2.0]
└── f [x=2.0, y=1.0]
References
- [1] Walker, J. (1991). Positioning Nodes for General Trees. https://www.drdobbs.com/positioning-nodes-for-general-trees/184402320?pgno=4
- [2] Reingold, E., Tilford, J. (1981). Tidier Drawings of Trees. IEEE Transactions on Software Engineering. https://reingold.co/tidier-drawings.pdf
Args:
tree_node (BaseNode): tree to compute (x, y) coordinate
sibling_separation (float): minimum distance between adjacent siblings of the tree
subtree_separation (float): minimum distance between adjacent subtrees of the tree
level_separation (float): fixed distance between adjacent levels of the tree
x_offset (float): graph offset of x-coordinates
y_offset (float): graph offset of y-coordinates
"""
_first_pass(tree_node, sibling_separation, subtree_separation)
x_adjustment = _second_pass(tree_node, level_separation, x_offset, y_offset)
_third_pass(tree_node, x_adjustment)
def _first_pass(
tree_node: T, sibling_separation: float, subtree_separation: float
) -> None:
"""
Performs post-order traversal of tree and assigns `x`, `mod` and `shift` values to each node.
Modifies tree in-place.
Notation:
- `lsibling`: left-sibling of node
- `lchild`: last child of node
- `fchild`: first child of node
- `midpoint`: midpoint of node wrt children, :math:`midpoint = (lchild.x + fchild.x) / 2`
- `sibling distance`: sibling separation
- `subtree distance`: subtree separation
There are two parts in the first pass,
1. In the first part, we assign `x` and `mod` values to each node
`x` value is the initial x-position of each node purely based on the node's position
- :math:`x = 0` for leftmost node and :math:`x = lsibling.x + sibling distance` for other nodes
- Special case when leftmost node has children, then it will try to center itself, :math:`x = midpoint`
`mod` value is the amount to shift the subtree (all descendant nodes excluding itself) to make the children centered with itself
- :math:`mod = 0` for node does not have children (no need to shift subtree) or it is a leftmost node (parent is already centered, from above point)
- Special case when non-leftmost nodes have children, :math:`mod = x - midpoint`
2. In the second part, we assign `shift` value of nodes due to overlapping subtrees.
For each node on the same level, ensure that the leftmost descendant does not intersect with the rightmost
descendant of any left sibling at every subsequent level. Intersection happens when the subtrees are not
at least `subtree distance` apart.
If there are any intersections, shift the whole subtree by a new `shift` value, shift any left sibling by a
fraction of `shift` value, and shift any right sibling by `shift` + a multiple of the fraction of
`shift` value to keep nodes centralized at the level.
Args:
tree_node (BaseNode): tree to compute (x, y) coordinate
sibling_separation (float): minimum distance between adjacent siblings of the tree
subtree_separation (float): minimum distance between adjacent subtrees of the tree
"""
# Post-order iteration (LRN)
for child in tree_node.children:
_first_pass(child, sibling_separation, subtree_separation)
_x = 0.0
_mod = 0.0
_shift = 0.0
_midpoint = 0.0
if tree_node.is_root:
tree_node.set_attrs({"x": _get_midpoint_of_children(tree_node)})
tree_node.set_attrs({"mod": _mod})
tree_node.set_attrs({"shift": _shift})
else:
# First part - assign x and mod values
if tree_node.children:
_midpoint = _get_midpoint_of_children(tree_node)
# Non-leftmost node
if tree_node.left_sibling:
_x = tree_node.left_sibling.get_attr("x") + sibling_separation
if tree_node.children:
_mod = _x - _midpoint
# Leftmost node
else:
if tree_node.children:
_x = _midpoint
tree_node.set_attrs({"x": _x})
tree_node.set_attrs({"mod": _mod})
tree_node.set_attrs({"shift": tree_node.get_attr("shift", _shift)})
# Second part - assign shift values due to overlapping subtrees
parent_node = tree_node.parent
tree_node_idx = parent_node.children.index(tree_node)
if tree_node_idx:
for idx_node in range(tree_node_idx):
left_subtree = parent_node.children[idx_node]
_shift = max(
_shift,
_get_subtree_shift(
left_subtree=left_subtree,
right_subtree=tree_node,
left_idx=idx_node,
right_idx=tree_node_idx,
subtree_separation=subtree_separation,
),
)
# Shift siblings (left siblings, itself, right siblings) accordingly
for multiple, sibling in enumerate(parent_node.children):
sibling.set_attrs(
{
"shift": sibling.get_attr("shift", 0)
+ (_shift * multiple / tree_node_idx)
}
)
def _get_midpoint_of_children(tree_node: BaseNode) -> float:
"""Get midpoint of children of a node
Args:
tree_node (BaseNode): tree node to obtain midpoint of their child/children
Returns:
(float)
"""
if tree_node.children:
first_child_x: float = tree_node.children[0].get_attr("x") + tree_node.children[
0
].get_attr("shift")
last_child_x: float = tree_node.children[-1].get_attr("x") + tree_node.children[
-1
].get_attr("shift")
return (last_child_x + first_child_x) / 2
return 0.0
def _get_subtree_shift(
left_subtree: T,
right_subtree: T,
left_idx: int,
right_idx: int,
subtree_separation: float,
left_cum_shift: float = 0,
right_cum_shift: float = 0,
cum_shift: float = 0,
initial_run: bool = True,
) -> float:
"""Get shift amount to shift the right subtree towards the right such that it does not overlap with the left subtree
Args:
left_subtree (BaseNode): left subtree, with right contour to be traversed
right_subtree (BaseNode): right subtree, with left contour to be traversed
left_idx (int): index of left subtree, to compute overlap for relative shift (constant across iteration)
right_idx (int): index of right subtree, to compute overlap for relative shift (constant across iteration)
subtree_separation (float): minimum distance between adjacent subtrees of the tree (constant across iteration)
left_cum_shift (float): cumulative `mod + shift` for left subtree from the ancestors, defaults to 0
right_cum_shift (float): cumulative `mod + shift` for right subtree from the ancestors, defaults to 0
cum_shift (float): cumulative shift amount for right subtree, defaults to 0
initial_run (bool): indicates whether left_subtree and right_subtree are the main subtrees, defaults to True
Returns:
(float)
"""
new_shift = 0.0
if not initial_run:
x_left = (
left_subtree.get_attr("x") + left_subtree.get_attr("shift") + left_cum_shift
)
x_right = (
right_subtree.get_attr("x")
+ right_subtree.get_attr("shift")
+ right_cum_shift
+ cum_shift
)
new_shift = max(
(x_left + subtree_separation - x_right) / (1 - left_idx / right_idx), 0
)
# Search for a left sibling of left_subtree that has children
while left_subtree and not left_subtree.children and left_subtree.left_sibling:
left_subtree = left_subtree.left_sibling
# Search for a right sibling of right_subtree that has children
while (
right_subtree and not right_subtree.children and right_subtree.right_sibling
):
right_subtree = right_subtree.right_sibling
if left_subtree.children and right_subtree.children:
# Iterate down the level, for the rightmost child of left_subtree and the leftmost child of right_subtree
return _get_subtree_shift(
left_subtree=left_subtree.children[-1],
right_subtree=right_subtree.children[0],
left_idx=left_idx,
right_idx=right_idx,
subtree_separation=subtree_separation,
left_cum_shift=(
left_cum_shift
+ left_subtree.get_attr("mod")
+ left_subtree.get_attr("shift")
),
right_cum_shift=(
right_cum_shift
+ right_subtree.get_attr("mod")
+ right_subtree.get_attr("shift")
),
cum_shift=cum_shift + new_shift,
initial_run=False,
)
return cum_shift + new_shift
def _second_pass(
tree_node: T,
level_separation: float,
x_offset: float,
y_offset: float,
cum_mod: Optional[float] = 0.0,
max_depth: Optional[int] = None,
x_adjustment: Optional[float] = 0.0,
) -> float:
"""
Performs pre-order traversal of tree and determines the final `x` and `y` values for each node.
Modifies tree in-place.
Notation:
- `depth`: maximum depth of tree
- `distance`: level separation
- `x'`: x offset
- `y'`: y offset
Final position of each node
- :math:`x = node.x + node.shift + sum(ancestor.mod) + x'`
- :math:`y = (depth - node.depth) * distance + y'`
Args:
tree_node (BaseNode): tree to compute (x, y) coordinate
level_separation (float): fixed distance between adjacent levels of the tree (constant across iteration)
x_offset (float): graph offset of x-coordinates (constant across iteration)
y_offset (float): graph offset of y-coordinates (constant across iteration)
cum_mod (Optional[float]): cumulative `mod + shift` for tree/subtree from the ancestors
max_depth (Optional[int]): maximum depth of tree (constant across iteration)
x_adjustment (Optional[float]): amount of x-adjustment for third pass, in case any x-coordinates goes below 0
Returns
(float)
"""
if not max_depth:
max_depth = tree_node.max_depth
final_x: float = (
tree_node.get_attr("x") + tree_node.get_attr("shift") + cum_mod + x_offset
)
final_y: float = (max_depth - tree_node.depth) * level_separation + y_offset
tree_node.set_attrs({"x": final_x, "y": final_y})
# Pre-order iteration (NLR)
if tree_node.children:
return max(
[
_second_pass(
child,
level_separation,
x_offset,
y_offset,
cum_mod + tree_node.get_attr("mod") + tree_node.get_attr("shift"),
max_depth,
x_adjustment,
)
for child in tree_node.children
]
)
return max(x_adjustment, -final_x)
def _third_pass(tree_node: BaseNode, x_adjustment: float) -> None:
"""Adjust all x-coordinates by an adjustment value so that every x-coordinate is greater than or equal to 0.
Modifies tree in-place.
Args:
tree_node (BaseNode): tree to compute (x, y) coordinate
x_adjustment (float): amount of adjustment for x-coordinates (constant across iteration)
"""
if x_adjustment:
tree_node.set_attrs({"x": tree_node.get_attr("x") + x_adjustment})
# Pre-order iteration (NLR)
for child in tree_node.children:
_third_pass(child, x_adjustment)