Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_ppb / simulator / utils.py: 98%
59 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Helpers used by the pipeline simulator: numeric coercion, colouring, decorators."""
16import time
17from functools import wraps
18from typing import Any, Callable, List, Tuple, Union
20import numpy as np
21from matplotlib import colors
23from hyper_parallel.auto_parallel.sapp_ppb.utils.logger import logger
25ScalarOrMatrix = Union[int, float, List[List[Union[int, float]]], Tuple[Tuple[Union[int, float], ...], ...]]
28def format_2d_inputs(a: ScalarOrMatrix, raw: int, col: int) -> np.ndarray:
29 """Coerce ``a`` into a 2-D :class:`numpy.ndarray` of shape ``(raw, col)``.
31 Args:
32 a: Scalar broadcast to ``(raw, col)``, a flat sequence treated as one row,
33 or a nested sequence interpreted as a 2-D matrix.
34 raw: Number of rows when broadcasting a scalar.
35 col: Number of columns when broadcasting a scalar.
37 Returns:
38 A 2-D array matching the supplied data.
40 Raises:
41 ValueError: If ``a`` does not match any of the supported shapes.
42 """
43 if isinstance(a, (int, float)):
44 return np.broadcast_to(a, (raw, col))
45 if isinstance(a, (list, tuple)):
46 if all(isinstance(item, (list, tuple)) for item in a):
47 return np.array(a)
48 if all(isinstance(item, (int, float)) for item in a):
49 return np.array([a])
50 raise ValueError(f"Unsupported inputs: {a}")
51 raise ValueError(f"Unsupported inputs: {a}")
54def apply_color(target_list: list, c: List[str]) -> list:
55 """Wrap each element of ``target_list`` with an ANSI colour escape from ``c``.
57 Args:
58 target_list: Values to colour (floats are formatted to four decimals).
59 c: One ANSI colour code per target element.
61 Returns:
62 The same list with each element wrapped in the matching colour escape.
63 """
64 for i, target in enumerate(target_list):
65 target = f'{target:.4f}' if isinstance(target, float) else target
66 target_list[i] = f"\033[{c[i]}m{target}\033[0m"
67 return target_list
70def apply_format(target_list: list) -> str:
71 """Join a sequence of pre-coloured values into the single-line bubble report.
73 Args:
74 target_list: Coloured strings produced by :func:`apply_color`.
76 Returns:
77 The formatted single-line string.
78 """
79 s = f'{target_list[0]:^22}'
80 symbol = ['=', '+', '+', '+', '+', '+']
81 for i in range(len(target_list) - 1):
82 s = f'{s}{symbol[i]}{target_list[i + 1]:^22}'
83 return s
86def color_mix(c1: Any, c2: Any, w1: float = 0.5, w2: float = 0.5) -> Tuple[float, float, float, float]:
87 """Blend two matplotlib colours with weights ``w1`` and ``w2``.
89 Args:
90 c1: First colour in any format understood by :func:`matplotlib.colors.to_rgba`.
91 c2: Second colour.
92 w1: Weight for ``c1``. Default: 0.5.
93 w2: Weight for ``c2``. Default: 0.5.
95 Returns:
96 A ``(r, g, b, a)`` tuple with values in ``[0, 1]``.
97 """
98 rgb = (np.array(colors.to_rgba(c1, 1)) * w1 + np.array(colors.to_rgba(c2, 1)) * w2) / (w1 + w2)
99 return colors.to_rgba(rgb)
102def dfs_builder(comm: bool = False) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
103 """Build a decorator that guards a DFS visit against re-entry and unmet dependencies.
105 Args:
106 comm: When ``True``, use the communication-aware ``depend_pre``/``depend_left``
107 attributes; otherwise use the compute-only ``pre``/``left`` attributes.
109 Returns:
110 A decorator wrapping a DFS visit method on :class:`BlockSim`-like objects.
111 """
113 def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
114 """Attach the DFS visit guards to ``func``."""
116 @wraps(func)
117 def wrapper(*args: Any, **kwargs: Any) -> Any:
118 """Run ``func`` exactly once per node after asserting dependencies."""
119 self = args[0]
120 pre, left = (self.depend_pre, self.depend_left) if comm else (self.pre, self.left)
121 if self.finish:
122 return None
123 if pre is None or left is None:
124 raise NotImplementedError
125 if self.in_queue:
126 raise ValueError("Dependency loop detected during DFS traversal")
127 self.in_queue = True
128 res = func(*args, **kwargs)
129 self.finish = True
130 self.in_queue = False
131 return res
132 return wrapper
134 return decorator
137def timer(func: Callable[..., Any]) -> Callable[..., Any]:
138 """Log the wall-clock time a function takes.
140 Args:
141 func: Callable to time.
143 Returns:
144 A wrapper that logs the elapsed time at INFO level after ``func`` returns.
145 """
147 @wraps(func)
148 def wrapper(*args: Any, **kwargs: Any) -> Any:
149 """Time one call to ``func`` and log the elapsed wall clock."""
150 t0 = time.time()
151 res = func(*args, **kwargs)
152 t1 = time.time() - t0
153 logger.info("function `%s` time used: %.4f s", func.__name__, t1)
154 return res
156 return wrapper