Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / nd / debug.py: 83%
467 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 2025 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"""Debugging utilities"""
17import os
18import colorsys
19import csv
20from enum import Enum, auto
21from pathlib import Path
22from functools import partial
23from math import isnan, sqrt
25import pandas as pd
26import matplotlib.pyplot as plt
27import matplotlib.colors as mc
28from matplotlib.font_manager import FontProperties
29from scipy.stats import pearsonr
31from hyper_parallel.auto_parallel.sapp_nd.nd.logger import logger
32import hyper_parallel.auto_parallel.sapp_nd.nd.dimensions as Dim
35class PerfParts(Enum):
36 """decomposition of performance"""
38 FW_COMPUTE = auto()
39 BW_COMPUTE = auto()
40 RECOMPUTE = auto()
41 DP_COMM = auto()
42 MP_COMM = auto()
43 EP_COMM = auto()
44 CP_COMM = auto()
45 PP_COMM = auto()
46 BUBBLE = auto()
47 TOTAL = auto()
48 MEMORY = auto()
50 def __str__(self):
51 return self.name
53 def short_name(self):
54 """Returns short component name"""
55 name = "Perf"
56 if self == self.FW_COMPUTE:
57 name = "FW"
58 elif self == self.BW_COMPUTE:
59 name = "BW"
60 elif self == self.RECOMPUTE:
61 name = "Rec"
62 elif self == self.DP_COMM:
63 name = "DP"
64 elif self == self.MP_COMM:
65 name = "MP"
66 elif self == self.EP_COMM:
67 name = "EP"
68 elif self == self.CP_COMM:
69 name = "CP"
70 elif self == self.PP_COMM:
71 name = "P2P"
72 elif self == self.BUBBLE:
73 name = "BBL"
74 elif self == self.MEMORY:
75 name = "MEM"
76 return name
79class RealParts(Enum):
80 """decomposition of performance"""
82 COMP = auto()
83 DP_WAIT = auto()
84 MP_WAIT = auto()
85 EP_WAIT = auto()
86 CP_WAIT = auto()
87 PP_WAIT = auto()
88 IDLE = auto()
89 TOTAL = auto()
91 def __str__(self):
92 return self.name.lower()
95class MemParts(Enum):
96 """decomposition of memory"""
98 TOTAL = auto()
100 def __str__(self):
101 return self.name
104class Debug:
105 """Debugging tools"""
107 def __init__(
108 self,
109 parallel_dimensions,
110 info_type,
111 enable=True,
112 output_file="debug.csv",
113 ):
114 self.enable = enable
115 if self.enable:
116 self.parallel_dimensions = parallel_dimensions
117 self.info = {p: 0 for p in info_type}
118 self.output_file = os.path.join(
119 os.path.dirname(os.path.abspath(__file__)),
120 "output",
121 output_file,
122 )
123 def is_enabled(self):
124 """Check whether debugging is enabled"""
125 return self.enable
127 def column_titles(self):
128 """Parameters to debug"""
129 titles = self.parallel_dimensions.keys() + list(self.info.keys())
130 titles = [str(t) for t in titles]
131 return ",".join(titles) + "\n"
133 def values(self):
134 """values debugged"""
135 str_dims = [str(v) for v in self.parallel_dimensions.values()]
136 str_score = [str(int(v)) for v in self.info.values()]
137 return ",".join(str_dims + str_score) + "\n"
139 def write(self):
140 """Parameters to debug"""
141 if self.enable:
142 os.makedirs(os.path.dirname(self.output_file), exist_ok=True)
143 is_new = not os.path.exists(self.output_file)
144 logger.info("debug written")
145 with open(self.output_file, "a", encoding="utf-8") as outfile:
146 if is_new:
147 outfile.write(self.column_titles())
148 outfile.write(self.values())
151def pastel(color, l_delta=0.0, lbl=None, sat=None):
152 "Pastel (lighter) color of the input"
153 if color == "white":
154 return (1.0, 1.0, 1.0)
155 if color == "black":
156 return (0.5, 0.5, 0.5)
157 try:
158 color = mc.cnames[color]
159 except KeyError:
160 pass
161 color_hls = colorsys.rgb_to_hls(*mc.to_rgb(color))
162 lgt = 0.7
163 if lbl is not None:
164 lgt = lbl
165 lgt = lgt + l_delta
167 if sat is None:
168 sat = 0.6
169 return colorsys.hls_to_rgb(color_hls[0], lgt, sat)
172def near_white(color, ratio):
173 "Very light color of the input for background"
174 rgb = mc.to_rgb(color)
175 if rgb is None:
176 return "white"
177 (red, green, blue) = rgb
178 red += (1 - red) * ratio
179 green += (1 - green) * ratio
180 blue += (1 - blue) * ratio
181 return (red, green, blue)
184def dim_color(dim, default="black"):
185 """Color of parallel dimensions for plot"""
186 color = {
187 Dim.DP: "orange",
188 Dim.OP: "orange",
189 Dim.TP: "red",
190 Dim.EP: "blue",
191 Dim.CP: "teal",
192 Dim.PP: "green",
193 Dim.VPP: "green",
194 Dim.MBN: "green",
195 }
196 try:
197 dim = Dim.get_dim(dim)
198 if dim in color:
199 return color[dim]
200 return default
201 except ValueError:
202 return default
205def gen_colors(categories):
206 """Color of each time component"""
207 compute_color = "purple"
208 idle_color = "grey"
209 col_d = {
210 str(PerfParts.FW_COMPUTE): pastel(compute_color, -0.2),
211 str(PerfParts.BW_COMPUTE): pastel(compute_color, -0.1),
212 str(PerfParts.RECOMPUTE): pastel(compute_color),
213 str(PerfParts.DP_COMM): pastel(dim_color(Dim.DP)),
214 str(PerfParts.MP_COMM): pastel(dim_color(Dim.TP), -0.1),
215 str(PerfParts.EP_COMM): pastel(dim_color(Dim.EP)),
216 str(PerfParts.CP_COMM): pastel(dim_color(Dim.CP)),
217 str(PerfParts.PP_COMM): pastel(dim_color(Dim.PP)),
218 str(PerfParts.BUBBLE): pastel(dim_color(Dim.PP), -0.15),
219 "IDLE": idle_color,
220 "COMPUTATION": pastel(compute_color, -0.2),
221 }
222 return [col_d.get(cat) for cat in categories]
225def set_twin_handles(ax1, data_frame, dbg_cols):
226 """Set legend for estimation and real"""
227 handle1, label1 = ax1.get_legend_handles_labels()
228 ax2 = plt.twinx()
229 data_frame[dbg_cols].plot.bar(
230 stacked=True,
231 sharex=True,
232 ax=ax2,
233 position=0,
234 color=gen_colors(dbg_cols),
235 width=0.4,
236 rot=0,
237 )
239 handle2, label2 = ax2.get_legend_handles_labels() # type: ignore
240 for handle in handle2:
241 if handle not in handle1:
242 handle1.append(handle)
243 for lbl in label2:
244 if lbl not in label1:
245 label1.append(lbl)
246 handles = handle1
247 labels = label1
248 plt.legend(handles, labels, loc="upper left", bbox_to_anchor=(1, 1))
249 leg = ax2.get_legend()
250 pp_color = gen_colors(["PP_COMM"])[0]
251 leg.legend_handles[-1].set_facecolor(pp_color) # type: ignore
254class Plot:
255 """plot ND top configs"""
257 title: str
258 col_title: list[str]
259 row_title: list[str]
260 cell_text: list[list[str]]
261 data: list[tuple]
262 dbg_cols: list[str]
263 top: int
265 def __init__(self, title, rows, debug_parts, top=None):
266 self.title = title
267 self.top = top if top is not None else 20
268 self.row_title = rows + ["MEM"]
269 self.dbg_cols = list(map(str, debug_parts))
270 self.col_title = []
271 self.cell_text = []
272 self.data = []
274 def make_table(self):
275 """Make table below plot with each parallelism degree"""
276 self.cell_text = list(map(list, zip(*self.cell_text))) # transpose
277 max_rows = list(map(max, map(partial(map, float), self.cell_text)))
278 the_table = plt.table(
279 cellText=self.cell_text,
280 rowLabels=self.row_title,
281 colLabels=self.col_title,
282 cellLoc="center",
283 loc="bottom",
284 )
285 row_colors = list(map(dim_color, self.row_title))
286 for row in range(len(self.row_title)):
287 cell = the_table[row + 1, -1]
288 cell.set_edgecolor("none")
289 cell.get_text().set_color(row_colors[row])
290 cell.set_text_props(fontproperties=FontProperties(weight="bold"))
291 for col in range(len(self.cell_text[0])):
292 cell = the_table[row + 1, col]
293 value = float(str(cell.get_text().get_text()))
294 try:
295 ratio = 1 - (value / max_rows[row])
296 except ZeroDivisionError:
297 ratio = 0
298 logger.debug(
299 "tmax = %s, ratio = %f, col=%s, newcolor=%s",
300 str(max_rows[row]),
301 ratio,
302 str(mc.to_rgb(row_colors[row])),
303 str(near_white(row_colors[row], ratio)),
304 )
305 cell.set_facecolor(near_white(pastel(row_colors[row]), ratio))
306 cell.set_edgecolor("none")
308 for col in range(len(self.cell_text[0])):
309 the_table[0, col].set_edgecolor("none")
311 the_table.scale(xscale=1, yscale=1.2) # +len(rows)/5)
313 def close(self, output_path, filename):
314 """Plot closing statements"""
315 plt.gca().set_xticklabels([])
316 plt.gca().set_yticklabels([])
317 plt.xlim([-0.5, len(self.data) - 0.5])
318 if self.title is not None:
319 plt.title(self.title)
320 plt.subplots_adjust(left=0.1, bottom=0.047 * (2 + len(self.row_title)))
321 plotfile = os.path.join(output_path, filename + ".pdf")
322 plt.savefig(plotfile, bbox_inches="tight")
323 plt.clf()
325 def parse_data(
326 self,
327 configs_estimated,
328 **kwargs,
329 ):
330 """Parse test data for plot"""
331 real_data = kwargs.get("real_data", None)
332 plot_idle = kwargs.get("plot_idle", False)
333 min_e = configs_estimated[0][2]
334 i = 0
335 for cfg_e in configs_estimated:
336 self.cell_text.append(cfg_e[0].values() + [cfg_e[1]])
337 self.col_title.append("")
338 try:
339 self.data.append(
340 tuple([cfg_e[0], cfg_e[2], cfg_e[3]] + cfg_e[4])
341 )
342 if real_data is not None:
343 waits = cfg_e[5]
344 logger.info(waits)
345 wait_list = [
346 waits["comp"],
347 waits["dp_wait"],
348 waits["mp_wait"],
349 waits["ep_wait"],
350 waits["BUBBLE"],
351 ]
352 if plot_idle:
353 wait_list.append(waits["IDLE"])
354 real_data.append(tuple(wait_list))
355 except IndexError:
356 score = cfg_e[2]
357 if i >= self.top or (min_e is not None and score > min_e * 20):
358 self.cell_text.pop()
359 break
360 self.data.append(tuple([cfg_e[0], score] + cfg_e[3]))
361 i += 1
364def plot_nd(
365 configs_estimated, output_path, debug_parts, title=None, max_num=None
366):
367 """Plot estimation"""
368 plot = Plot(
369 title, configs_estimated[0][0].keys(), debug_parts, top=max_num
370 )
371 plot.parse_data(configs_estimated)
373 data_frame = pd.DataFrame(
374 plot.data, columns=(["config", "estim"] + plot.dbg_cols)
375 )
376 axis = data_frame[plot.dbg_cols].plot.bar(
377 stacked=True, color=gen_colors(plot.dbg_cols), width=0.4, rot=0
378 )
379 axis.set_ylim(ymin=1)
380 axis.legend(loc="upper left", bbox_to_anchor=(1, 1))
382 plot.make_table()
383 plot.close(output_path, "results")
386def plot_vs_real(
387 configs_estimated, csv_f, output_path, debug_parts, title=None
388):
389 """Plot estimation vs real global time"""
390 plot = Plot(title, configs_estimated[0][0].keys(), debug_parts)
391 plot.parse_data(configs_estimated)
393 data_frame = pd.DataFrame(
394 plot.data, columns=(["config", "Real", "estim"] + plot.dbg_cols)
395 )
396 ax1 = data_frame["Real"].plot.bar(
397 position=1.1, width=0.4, secondary_y="real", color="grey", rot=0
398 )
400 set_twin_handles(ax1, data_frame, plot.dbg_cols)
401 plot.make_table()
402 plot.close(output_path, Path(os.path.basename(csv_f)).stem)
405def plot_vs_real_comm_classified(
406 configs_estimated,
407 csv_f,
408 output_path,
409 debug_parts,
410 **kwargs,
411):
412 """Plot estimation vs real detailed time"""
413 plot_idle = kwargs.get("plot_idle", False)
414 title = kwargs.get("title", None)
415 real_data = []
417 plot = Plot(title, configs_estimated[0][0].keys(), debug_parts)
418 plot.parse_data(
419 configs_estimated,
420 real_data=real_data,
421 plot_idle=plot_idle,
422 )
424 data_frame = pd.DataFrame(
425 plot.data, columns=(["config", "real", "estim"] + plot.dbg_cols)
426 )
427 real_cols = [
428 "COMPUTATION",
429 "DP_COMM",
430 "MP_COMM",
431 "EP_COMM",
432 "BUBBLE",
433 ]
434 if plot_idle:
435 real_cols.append("IDLE")
436 real_df = pd.DataFrame(real_data, columns=real_cols)
438 ax1 = real_df[real_cols].plot.bar(
439 stacked=True,
440 sharex=True,
441 position=1,
442 secondary_y="real",
443 color=gen_colors(real_cols),
444 width=0.4,
445 rot=0,
446 legend=False,
447 )
449 set_twin_handles(ax1, data_frame, plot.dbg_cols)
450 plot.make_table()
451 plot.close(
452 output_path,
453 Path(os.path.basename(csv_f)).stem,
454 )
457def correlation_topk(configs_estimated, csv_f):
458 """Computes correlation & top-k between real & estimation"""
459 times = []
460 estims = []
461 for _, _, time, score, _ in configs_estimated:
462 times.append(time)
463 estims.append(score)
464 correl = pearsonr(times, estims).statistic # type: ignore
465 if isnan(correl):
466 logger.critical(
467 "An input array is constant: %s or %s", str(times), str(estims)
468 )
469 topk = 0
470 for i, score in enumerate(estims):
471 if not score == min(estims[i:]):
472 break
473 topk += 1
474 if topk == 0:
475 for i, score in enumerate(estims):
476 if score == min(estims[i:]):
477 break
478 topk -= 1
480 logger.info("Correlation for file %s is: %.3f", csv_f, correl * 100)
481 return correl, topk
484def get_real_data(csv_f):
485 """Read execution time of different configurations on a given csv file"""
486 configs = []
487 row_num = 0
488 with open(csv_f, newline="", encoding="utf-8") as csv_file:
489 rows = csv.DictReader(csv_file)
490 for row in rows:
491 row_num += 1
492 logger.info(row)
493 real_time = float(row.pop("time"))
494 config = []
495 for dim_str, value in row.items():
496 try:
497 dim = Dim.get_dim(dim_str)
498 logger.debug("%s : %s", str(dim), str(dim.from_str(value)))
499 config.append((dim, dim.from_str(value)))
500 except ValueError:
501 pass
502 configs.append((Dim.Dimensions(config), real_time))
503 return configs, row_num
506def get_diff_dims(csv_f):
507 """Read execution time of different configurations on a given csv file"""
508 dims = []
509 data_frame = pd.read_csv(csv_f)
510 for dim_str, degrees in data_frame.items():
511 try:
512 dim = Dim.get_dim(dim_str)
513 diff_values = len(set(degrees))
514 if diff_values > 1:
515 dims.append(dim)
516 except ValueError:
517 pass
518 return dims
521def get_comm_classified_data(csv_f, plot_idle=False):
522 """Read time components of different configurations on a given csv file"""
523 configs = []
524 with open(csv_f, newline="", encoding="utf-8") as csv_file:
525 rows = csv.DictReader(csv_file)
526 for row in rows:
527 logger.info(row)
528 time = float(row.pop("time"))
529 config = []
530 comm_wait_time_classified = {}
531 total_wait = 0
533 for component, value_str in row.items():
534 if "wait" in component:
535 value_float = float(value_str)
536 logger.info(
537 "Comm_wait = %s, v = %f", component, value_float
538 )
539 comm_wait_time_classified[component] = value_float
540 total_wait += value_float
541 elif "comp" in component:
542 value_float = float(value_str)
543 logger.info("Computation = %f", value_float)
544 comm_wait_time_classified["comp"] = value_float
545 total_wait += value_float
546 else:
547 logger.info("d = %s, v = %s", component, value_str)
548 dim = Dim.get_dim(component)
549 config.append((dim, dim.from_str(value_str)))
550 comm_wait_time_classified["BUBBLE"] = comm_wait_time_classified.get(str(RealParts.PP_WAIT))
551 if plot_idle:
552 comm_wait_time_classified["IDLE"] = time - total_wait
553 logger.info(
554 "idle = total time - total waits = %.3f - %.3f",
555 time,
556 total_wait,
557 )
558 configs.append(
559 (Dim.Dimensions(config), time, comm_wait_time_classified)
560 )
561 return configs
564def estimation_in_real_parts(
565 estimations_in_real_components, estimations, score
566):
567 """Transform the estimation components into the RealParts components for comparison with real time"""
568 estimations_in_real_components[RealParts.TOTAL].append(score)
569 estimations_in_real_components[RealParts.COMP].append(
570 estimations[PerfParts.FW_COMPUTE.value - 1]
571 + estimations[PerfParts.BW_COMPUTE.value - 1]
572 + estimations[PerfParts.RECOMPUTE.value - 1]
573 )
574 estimations_in_real_components[RealParts.DP_WAIT].append(
575 estimations[PerfParts.DP_COMM.value - 1]
576 )
577 estimations_in_real_components[RealParts.MP_WAIT].append(
578 estimations[PerfParts.MP_COMM.value - 1]
579 )
580 estimations_in_real_components[RealParts.CP_WAIT].append(
581 estimations[PerfParts.CP_COMM.value - 1]
582 )
583 estimations_in_real_components[RealParts.EP_WAIT].append(
584 estimations[PerfParts.EP_COMM.value - 1]
585 )
586 estimations_in_real_components[RealParts.PP_WAIT].append(
587 estimations[PerfParts.BUBBLE.value - 1]
588 + estimations[PerfParts.PP_COMM.value - 1]
589 )
590 return estimations_in_real_components
593def real_in_parts(parts, real, time):
594 """Transform the real time components into the RealParts components for comparison with estimation"""
595 parts[RealParts.TOTAL].append(time)
596 for part in RealParts:
597 if part not in {RealParts.TOTAL, RealParts.IDLE}:
598 if str(part) in real.keys():
599 parts[part].append(real[str(part)])
600 else:
601 logger.warning(
602 "part = %s not in real keys = %s", part, real.keys()
603 )
604 parts[part].append(0)
606 op = "op_wait"
607 if op in real.keys():
608 parts[RealParts.DP_WAIT][-1] += real["op_wait"]
610 sp = "sp_wait"
611 if sp in real.keys():
612 parts[RealParts.MP_WAIT][-1] += real["sp_wait"]
614 return parts
617def correlation_with_classified_comms(configs_estimated):
618 """Computes correlation and distance between components time & estimation."""
619 score_classified = {}
620 time_classified = {}
621 distances = {}
623 for wait in RealParts:
624 if wait not in {RealParts.IDLE}:
625 score_classified[wait] = []
626 time_classified[wait] = []
627 distances[wait] = []
629 topk = 0
630 still_top_k = True
632 for i, (_, _, time, score, values, real_values) in enumerate(
633 configs_estimated
634 ):
635 if (
636 still_top_k
637 and score == (min(configs_estimated[i:], key=lambda t: t[3]))[3]
638 ):
639 topk += 1
640 else:
641 still_top_k = False
643 score_classified = estimation_in_real_parts(
644 score_classified, values, score
645 )
647 time_classified = real_in_parts(time_classified, real_values, time)
649 square_distances_sum = 0
650 for wait in RealParts:
651 if wait not in {RealParts.TOTAL, RealParts.IDLE}:
652 distance = (
653 time_classified[wait][-1]
654 / time_classified[RealParts.TOTAL][-1]
655 - score_classified[wait][-1]
656 / score_classified[RealParts.TOTAL][-1]
657 )
658 square_distances_sum += distance * distance
659 distances[wait].append(abs(distance))
660 distances[RealParts.TOTAL] = sqrt(square_distances_sum)
662 correls = {}
663 for wait in RealParts:
664 pearson_wait(correls, time_classified, score_classified, wait)
665 return correls, distances, topk, len(configs_estimated)
668def color_diff(diff):
669 """Color difference"""
670 if diff > 0:
671 return f"\033[92m improved by {diff:.3f}%\033[00m"
672 return f"\033[91m worsened by {-diff:.3f}%\033[00m"
675def color_correl(correlation):
676 """Color correlation"""
677 res = f"{correlation*100:.3f}%"
678 if correlation > 0.9:
679 res = f" \033[92m{res}\033[00m "
680 elif correlation < 0:
681 res = f"\033[91m{res}\033[00m "
682 elif correlation < 0.5:
683 res = f" \033[91m{res}\033[00m "
684 else:
685 res = f" \033[00m{res}\033[00m "
686 return res
689def print_diff(case, prev, new, **kwargs):
690 """Print difference of correlation"""
691 topk = kwargs.get("topk", None)
692 total = kwargs.get("total", None)
693 tabsize = kwargs.get("tabsize", 40)
694 diff = (new - prev) * 100
695 msg = ""
696 if -0.1 < diff < 0.1:
697 msg = f"{case} \tcorrelation :{color_correl(new)} \033[00m\033[00m"
698 else:
699 msg = (
700 f"{case} \tcorrelation ({color_correl(new)}) is{color_diff(diff)}"
701 )
702 if topk is not None and total is not None:
703 msg += f" topk = {topk}/{total}"
704 logger.output(msg.expandtabs(tabsize))
707def get_distance_i(part, data_i):
708 """get the average distance of a given part"""
709 _, distance, _, _ = data_i
710 if part is RealParts.TOTAL:
711 return distance[part]
712 return sum(distance[part]) / len(distance[part])
715def get_correl_i(part, data_i):
716 """get the correlation of a given part"""
717 f_correl, _, _, _ = data_i
718 return f_correl[part]
721def print_part_x_file(data, fun):
722 """Prints a metric computed by fun for each couple (part, file)"""
723 msg = ""
724 for part in RealParts:
725 if part is not RealParts.IDLE:
726 msg += "\n" + str(part) + "\t"
727 col_sum = 0
728 col_num = 0
729 for data_i in data:
730 try:
731 info = fun(part, data_i)
732 msg += f"\t{(info*100):.1f}%"
733 col_sum += info
734 col_num += 1
735 except KeyError:
736 msg += "\t -"
737 if col_num > 0:
738 msg += f"\t\t{(col_sum/col_num)*100:.1f}%"
739 return msg
742def print_correlations_classified(data):
743 """Printer for estimation vs detailed profiling"""
744 msg = "\n\t"
745 for i, _ in enumerate(data):
746 msg += "\tFile " + str(i + 1)
747 msg += "\t\tavg"
749 msg += "\nCorrelation (higher is better)"
750 msg += print_part_x_file(data, get_correl_i)
752 msg += "\ntop_k\t"
753 for _, _, top_k, total in data:
754 msg += "\t" + str(top_k) + "/" + str(total)
756 msg += "\n\nEuclidean Distance (lower is better)"
757 msg += print_part_x_file(data, get_distance_i)
759 logger.output(msg)
762def is_constant(array):
763 """Whether the given array only has the same elements"""
764 if len(array) == 0:
765 return True
766 value = array[0]
767 return all(vi == value for vi in array)
770def pearson_wait(correls, real, estim, wait):
771 """Compute Pearson correlation if inputs are not empty"""
772 if wait not in {RealParts.IDLE}:
773 logger.debug(
774 "correlation for %s between real = %s && estim = %s",
775 str(wait),
776 str(real[wait]),
777 str(estim[wait]),
778 )
779 if not is_constant(real[wait]) and not is_constant(estim[wait]):
780 pearson = pearsonr(
781 real[wait], estim[wait]
782 ).statistic # type: ignore
783 logger.info(
784 "correlation[%s] of real %s vs estim %s = %f",
785 wait,
786 real[wait],
787 estim[wait],
788 pearson,
789 )
790 correls[wait] = pearson
791 else:
792 logger.warning(
793 "either estim[%s] = %s is constant", wait, str(estim[wait])
794 )
795 logger.warning(
796 "or real[%s] = %s is constant", wait, str(real[wait])
797 )