Xiangiqgame
AI engine for Xiangqi
Loading...
Searching...
No Matches
game_summary_plotters.py
Go to the documentation of this file.
1"""
2Contains the GameSummaryPlotter abstract base class, and
3multiple subclasses that implement it.
4
5Each sub-class plots a different portion of data from a GameSummary.
6"""
7
8import warnings
9from abc import ABC, abstractmethod
10from typing import Dict, List, Tuple, cast
11
12import numpy as np
13import pandas as pd
14import xiangqi_bindings as bindings
15from matplotlib import pyplot as plt
16
17from xiangqipy.core_dataclass_mirrors import PointsT
18
19
21 """
22 Abstract base class for plotting data stored in pandas
23 dataframes (one df for each player) to a numpy array of matplotlib Axes
24 """
25
26 evaluating_player_line_colors = {
27 bindings.PieceColor.kRed: "red",
28 bindings.PieceColor.kBlk: "black",
29 }
30
31 player_text_labels = {
32 bindings.PieceColor.kRed: "Red Player",
33 bindings.PieceColor.kBlk: "Black Player",
34 }
35
36 non_evaluating_player_line_colors = {
37 bindings.PieceColor.kRed: "lightcoral",
38 bindings.PieceColor.kBlk: "darkgray",
39 }
40
42 self,
43 axes: np.ndarray,
44 y_labels: Tuple[str, ...],
45 log_scale_rows: int | Tuple[int, ...] = tuple(),
46 red_data: pd.DataFrame = None,
47 black_data: pd.DataFrame = None,
48 add_plot_column_titles: bool = True,
49 ):
50 self.axes = axes
51 self.y_labels = y_labels
52 if type(log_scale_rows) == int:
53 log_scale_rows = (log_scale_rows,)
54 self.log_scale_rows = log_scale_rows
55 self._red_data = red_data
56 self._black_data = black_data
57 self._add_plot_column_titles = add_plot_column_titles
58 self.validate_dfs()
59 self.set_y_axes()
60 self.set_x_axes()
61
62 @property
63 def dfs(self) -> Dict[str, pd.DataFrame]:
64 return {
65 bindings.PieceColor.kRed.name: self._red_data,
66 bindings.PieceColor.kBlk.name: self._black_data,
67 }
68
69 def has_data(self, player: bindings.PieceColor) -> bool:
70 return self.dfs[player.name] is not None
71
72 @property
73 def num_players_with_data(self) -> int:
74 return sum(
75 [
76 int(self.has_data(bindings.PieceColor.kRed)),
77 int(self.has_data(bindings.PieceColor.kBlk)),
78 ]
79 )
80
81 def validate_dfs(self):
83 assert (
84 self.dfs[bindings.PieceColor.kRed.name].columns
85 == self.dfs[bindings.PieceColor.kBlk.name].columns
86 ).all()
87
88 def set_y_axes(self):
89 for grid_row_idx, grid_row in enumerate(
90 self.axes[self.log_scale_rows, :]
91 ):
92 for ax in grid_row:
93 ax = cast(plt.Axes, ax)
94 ax.set_yscale("log")
95
96 for grid_row_idx, ax in enumerate(self.axes[:, 0]):
97 ax = cast(plt.Axes, ax)
98 ax.set_ylabel(self.y_labels[grid_row_idx], fontsize=14)
99 ax.yaxis.set_label_coords(-0.15, 0.5)
100
101 def set_x_axes(self):
102 for grid_row_idx, grid_row in enumerate(self.axes[0:-1, :]):
103 for ax in grid_row:
104 ax = cast(plt.Axes, ax)
105 ax.set_xticklabels([])
106
107 for grid_col_idx, ax in enumerate(self.axes[-1, :]):
108 ax = cast(plt.Axes, ax)
109 ax.set_xlabel("Game Move Number", fontsize=14)
110
111 @property
112 def player_plot_col(self) -> Dict[bindings.PieceColor, int]:
114 return {bindings.PieceColor.kRed: 0, bindings.PieceColor.kBlk: 1}
115 elif self.has_data(player=bindings.PieceColor.kRed):
116 return {
117 bindings.PieceColor.kRed: 0,
118 bindings.PieceColor.kBlk: None,
119 }
120 elif self.has_data(player=bindings.PieceColor.kBlk):
121 return {
122 bindings.PieceColor.kRed: None,
123 bindings.PieceColor.kBlk: 0,
124 }
125
126 @property
127 def data_columns(self) -> pd.Index:
128 return [
129 item.columns for item in self.dfs.values() if item is not None
130 ][0]
131
132 @staticmethod
133 def match_y_limits(axes_row: np.ndarray):
134 y_limits_low = []
135 y_limits_high = []
136 for col_idx in range(axes_row.shape[0]):
137 cur_ax = cast(plt.Axes, axes_row[col_idx])
138 y_limits_low.append(cur_ax.get_ylim()[0])
139 y_limits_high.append(cur_ax.get_ylim()[1])
140 for col_idx in range(axes_row.shape[0]):
141 cur_ax = cast(plt.Axes, axes_row[col_idx])
142 cur_ax.set_ylim((min(y_limits_low), max(y_limits_high)))
143
145 for row_idx in range(self.axes.shape[0]):
146 self.match_y_limits(self.axes[row_idx, :])
147
148 @abstractmethod
149 def plot_data(self):
150 pass
151
153 if self.axes.shape[1] == 2:
154 for idx, ax in enumerate(self.axes[:, 1]):
155 ax = cast(plt.Axes, ax)
156 ax.set_yticklabels([])
157
159 for idx, ax in enumerate(self.axes[0, :]):
160 ax = cast(plt.Axes, ax)
161 if idx == self.player_plot_col[bindings.PieceColor.kRed]:
162 ax.set_title(
163 self.player_text_labels[bindings.PieceColor.kRed],
164 fontsize=16,
165 )
166 if idx == self.player_plot_col[bindings.PieceColor.kBlk]:
167 ax.set_title(
168 self.player_text_labels[bindings.PieceColor.kBlk],
169 fontsize=16,
170 )
171
172 def plot(self):
173 self.plot_data()
178
179
181 """
182 Implements GameSummaryPlotter, and produces stacked plots of Minimax
183 search result counts grouped by MinimaxResultType.
184 """
185
187 self,
188 axes: np.ndarray,
189 y_labels: Tuple[str] = (
190 "Node Counts",
191 "Node Counts Log Scale",
192 ),
193 log_scale_rows: int | Tuple[int, int] = 1,
194 cmap_name: str = "Accent",
195 red_data: pd.DataFrame = None,
196 black_data: pd.DataFrame = None,
197 add_plot_column_titles: bool = True,
198 ):
199 super().__init__(
200 axes=axes,
201 y_labels=y_labels,
202 log_scale_rows=log_scale_rows,
203 red_data=red_data,
204 black_data=black_data,
205 add_plot_column_titles=add_plot_column_titles,
206 )
207 self.cmap = plt.get_cmap(cmap_name)
208
209 @property
211 self,
212 ) -> Dict[str, Tuple[float, float, float, float]]:
213 return {
214 cast(str, col_name): self.cmap(idx)
215 for idx, col_name in enumerate(self.data_columns)
216 }
217
219 self, player: bindings.PieceColor, ax: plt.Axes
220 ):
221 df = self.dfs[player.name]
222 df_sorted = df[sorted(df.columns, key=lambda col: df[col].mean())]
223 sorted_colors = [
224 self.data_column_colors[col] for col in df_sorted.columns
225 ]
226
227 with warnings.catch_warnings():
228 warnings.filterwarnings(
229 "ignore",
230 category=UserWarning,
231 message="Data has no positive values, and therefore cannot be log-scaled.",
232 )
233 ax.stackplot(
234 df.index,
235 df_sorted.T,
236 labels=df_sorted.columns,
237 colors=sorted_colors,
238 )
239
240 def plot_player_data(self, player: bindings.PieceColor):
241 for plot_row in range(2):
242 plot_col = self.player_plot_col[player]
243 ax = cast(plt.Axes, self.axes[plot_row, plot_col])
244 self.plot_search_results_by_type_stacked(player=player, ax=ax)
245
246 def add_legend(self):
247 upper_right_plot = cast(plt.Axes, self.axes[0, -1])
248 handles = [
249 plt.Line2D(
250 xdata=[0],
251 ydata=[0],
252 color=self.data_column_colors[col],
253 lw=4,
254 )
255 for col in self.data_columns
256 ]
257 upper_right_plot.legend(
258 handles,
259 self.data_columns,
260 bbox_to_anchor=(1.05, 0.50),
261 fontsize=14,
262 title="Node Type",
263 title_fontsize=15,
264 alignment="left",
265 )
266
267 def plot_data(self):
268
269 for player in [bindings.PieceColor.kRed, bindings.PieceColor.kBlk]:
270 if self.has_data(player=player):
271 self.plot_player_data(player=player)
272 self.add_legend()
273
274
276 """
277 Implements GameSummaryPlotter, and produces plots showing time spent
278 by core MinimaxMoveEvaluator(s) evaluating nodes and selecting Moves.
279 """
280
281 search_stats_time_cols = ["search_time_s", "mean_time_per_node_ns"]
282
284 self,
285 axes: np.ndarray,
286 log_scale_rows: int | Tuple[int, ...] = tuple(),
287 red_data: pd.DataFrame = None,
288 black_data: pd.DataFrame = None,
289 add_plot_column_titles: bool = True,
290 ):
291 super().__init__(
292 axes=axes,
293 y_labels=(
294 "Search Time (s)",
295 "Time per Node (ns)",
296 ),
297 log_scale_rows=log_scale_rows,
298 red_data=red_data,
299 black_data=black_data,
300 add_plot_column_titles=add_plot_column_titles,
301 )
302
303 def plot_player_search_times(self, player: bindings.PieceColor):
304 df = self.dfs[player.name]
305 plot_col = self.player_plot_col[player]
306 for data_col_idx, data_col in enumerate(self.search_stats_time_cols):
307 ax = cast(plt.Axes, self.axes[data_col_idx, plot_col])
308 ax.plot(
309 df.index,
310 df[data_col],
311 color=self.evaluating_player_line_colors[player],
312 )
313
314 def plot_data(self):
315 for player in [bindings.PieceColor.kRed, bindings.PieceColor.kBlk]:
316 if self.has_data(player=player):
317 self.plot_player_search_times(player=player)
318
319
321 """
322 Implements GameSummaryPlotter, and plots evaluated score of each move of
323 each Player using a Minimax algorithm in a Game.
324 """
325
327 self,
328 axes: np.ndarray,
329 log_scale_rows: int | Tuple[int, ...] = tuple(),
330 red_data: pd.DataFrame = None,
331 black_data: pd.DataFrame = None,
332 add_plot_column_titles: bool = True,
333 ):
334 super().__init__(
335 axes=axes,
336 y_labels=("Evaluation Score",),
337 log_scale_rows=log_scale_rows,
338 red_data=red_data,
339 black_data=black_data,
340 add_plot_column_titles=add_plot_column_titles,
341 )
342
343 @property
345 largest_score_magnitudes = []
346 inf_like_values = [np.iinfo(PointsT).max, np.iinfo(PointsT).min]
347 for df in self.dfs.values():
348 if df is not None:
349 assert df["eval_score"].dtype == PointsT
350 non_inf_like_values = df[
351 ~df["eval_score"].isin(inf_like_values)
352 ]["eval_score"]
353 largest_score_magnitudes.append(
354 np.abs(non_inf_like_values.min())
355 )
356 largest_score_magnitudes.append(
357 np.abs(non_inf_like_values.max())
358 )
359 return max(largest_score_magnitudes)
360
361 @staticmethod
363 data: pd.Series, magnitude: int | float
364 ) -> pd.Series:
365 assert magnitude >= 0
366 return cast(
367 pd.Series, np.clip(data, a_min=-1 * magnitude, a_max=magnitude)
368 )
369
371 self,
372 player: bindings.PieceColor,
373 ax: plt.Axes,
374 is_evaluating_player: bool = True,
375 ):
376 if self.has_data(player=player):
377 df = self.dfs[player.name]
378 if is_evaluating_player:
379 line_color = self.evaluating_player_line_colors[player]
380 line_style = "solid"
381 else:
382 line_color = self.non_evaluating_player_line_colors[player]
383 line_style = "dotted"
384 ax.plot(
385 df.index,
387 data=df["eval_score"],
388 magnitude=1.5 * self.largest_magnitude_noninf_eval_score,
389 ),
390 color=line_color,
391 linestyle=line_style,
392 label=self.player_text_labels[player],
393 )
394 legend = ax.legend(loc="upper left", fontsize="12")
395 legend.get_frame().set_facecolor("white")
396
397 def plot_player_and_opponent_overlay(self, player: bindings.PieceColor):
398 plot_grid_col = self.player_plot_col[player]
399 ax = cast(plt.Axes, self.axes[0, plot_grid_col])
400 self.plot_player_data(player=player, ax=ax)
401
402 if self.has_data(player=bindings.opponent_of(player)):
403 self.plot_player_data(
404 player=bindings.opponent_of(player),
405 ax=ax,
406 is_evaluating_player=False,
407 )
408
409 def plot_data(self):
410 for player in [bindings.PieceColor.kRed, bindings.PieceColor.kBlk]:
411 if self.has_data(player=player):
412 self.plot_player_and_opponent_overlay(player=player)
Implements GameSummaryPlotter, and plots evaluated score of each move of each Player using a Minimax ...
pd.Series symmetric_winsorize(pd.Series data, int|float magnitude)
def plot_player_and_opponent_overlay(self, bindings.PieceColor player)
def __init__(self, np.ndarray axes, int|Tuple[int,...] log_scale_rows=tuple(), pd.DataFrame red_data=None, pd.DataFrame black_data=None, bool add_plot_column_titles=True)
def plot_player_data(self, bindings.PieceColor player, plt.Axes ax, bool is_evaluating_player=True)
Abstract base class for plotting data stored in pandas dataframes (one df for each player) to a numpy...
def __init__(self, np.ndarray axes, Tuple[str,...] y_labels, int|Tuple[int,...] log_scale_rows=tuple(), pd.DataFrame red_data=None, pd.DataFrame black_data=None, bool add_plot_column_titles=True)
bool has_data(self, bindings.PieceColor player)
Dict[bindings.PieceColor, int] player_plot_col(self)
Implements GameSummaryPlotter, and produces stacked plots of Minimax search result counts grouped by ...
def __init__(self, np.ndarray axes, Tuple[str] y_labels=("Node Counts", "Node Counts Log Scale",), int|Tuple[int, int] log_scale_rows=1, str cmap_name="Accent", pd.DataFrame red_data=None, pd.DataFrame black_data=None, bool add_plot_column_titles=True)
def plot_search_results_by_type_stacked(self, bindings.PieceColor player, plt.Axes ax)
Dict[str, Tuple[float, float, float, float]] data_column_colors(self)
Implements GameSummaryPlotter, and produces plots showing time spent by core MinimaxMoveEvaluator(s) ...
def plot_player_search_times(self, bindings.PieceColor player)
def __init__(self, np.ndarray axes, int|Tuple[int,...] log_scale_rows=tuple(), pd.DataFrame red_data=None, pd.DataFrame black_data=None, bool add_plot_column_titles=True)
Contains classes that mirror the structure of some core C++ classes, primarily to facilitate easy IO ...