added 3rd party packages, elog, bigtree
This commit is contained in:
0
python310/packages/bigtree/utils/__init__.py
Normal file
0
python310/packages/bigtree/utils/__init__.py
Normal file
53
python310/packages/bigtree/utils/assertions.py
Normal file
53
python310/packages/bigtree/utils/assertions.py
Normal 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()}"
|
||||
)
|
||||
165
python310/packages/bigtree/utils/constants.py
Normal file
165
python310/packages/bigtree/utils/constants.py
Normal 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]
|
||||
126
python310/packages/bigtree/utils/exceptions.py
Normal file
126
python310/packages/bigtree/utils/exceptions.py
Normal 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
|
||||
19
python310/packages/bigtree/utils/groot.py
Normal file
19
python310/packages/bigtree/utils/groot.py
Normal 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()))])
|
||||
587
python310/packages/bigtree/utils/iterators.py
Normal file
587
python310/packages/bigtree/utils/iterators.py
Normal 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)
|
||||
354
python310/packages/bigtree/utils/plot.py
Normal file
354
python310/packages/bigtree/utils/plot.py
Normal 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)
|
||||
Reference in New Issue
Block a user