• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DIGNEA / DIGNEApy / 26462033682

26 May 2026 04:44PM UTC coverage: 89.085% (-6.9%) from 96.026%
26462033682

push

github

web-flow
Merge pull request #4 from DIGNEA/feat/map_iterative

Feat/map iterative

556 of 727 new or added lines in 38 files covered. (76.48%)

1 existing line in 1 file now uncovered.

1967 of 2208 relevant lines covered (89.09%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.66
/digneapy/visualize/_archive_plotter.py
1
#!/usr/bin/env python
2
# -*-coding:utf-8 -*-
3
"""
4
@File    :   _archive_plotter.py
5
@Time    :   2026/05/21 11:02:49
6
@Author  :   Alejandro Marrero (amarrerd@ull.edu.es)
7
@Version :   1.0
8
@Contact :   amarrerd@ull.edu.es
9
@License :   (C)Copyright 2026, Alejandro Marrero
10
@Desc    :   None
11
"""
12

13
from typing import Optional, Sequence
1✔
14

15
import matplotlib.colors as mcolors
1✔
16
import matplotlib.pyplot as plt
1✔
17
import numpy as np
1✔
18

19
from digneapy import GridArchive
1✔
20

21

22
def _archive_to_matrix(archive: GridArchive, attr: str = "p") -> np.ndarray:
1✔
23
    """Converts a 2-D GridArchive to a (rows × cols) float matrix.
24

25
    Empty cells are filled with NaN so they render as a neutral colour.
26

27
    Args:
28
        archive: A GridArchive with exactly 2 dimensions.
29
        attr:    Instance attribute to use as cell value. Defaults to ``"p"``.
30

31
    Returns:
32
        2-D numpy array of shape ``(dims[0], dims[1])``.
33
    """
NEW
34
    rows, cols = int(archive.dimensions[0]), int(archive.dimensions[1])
×
NEW
35
    matrix = np.full((rows, cols), np.nan, dtype=np.float64)
×
36

NEW
37
    if len(archive) == 0:
×
NEW
38
        return matrix
×
39

NEW
40
    flat_indices = np.array(list(archive._storage.keys()), dtype=int)
×
NEW
41
    grid_indices = archive.int_to_grid_index(flat_indices)  # (n, 2)
×
NEW
42
    values = np.array(
×
43
        [getattr(archive._storage[idx], attr) for idx in flat_indices],
44
        dtype=np.float64,
45
    )
NEW
46
    matrix[grid_indices[:, 0], grid_indices[:, 1]] = values
×
NEW
47
    return matrix
×
48

49

50
def _axis_tick_labels(
1✔
51
    archive: GridArchive, dim: int, n_ticks: int = 5
52
) -> tuple[list[float], list[str]]:
53
    """Returns (positions, labels) for cell-index ticks on one axis."""
NEW
54
    n_cells = int(archive.dimensions[dim])
×
NEW
55
    lb = archive._lower_bounds[dim]
×
NEW
56
    ub = archive._upper_bounds[dim]
×
NEW
57
    step = max(1, n_cells // (n_ticks - 1))
×
NEW
58
    positions = list(range(0, n_cells, step))
×
NEW
59
    if positions[-1] != n_cells - 1:
×
NEW
60
        positions.append(n_cells - 1)
×
NEW
61
    labels = [f"{lb + (ub - lb) * p / (n_cells - 1):.2f}" for p in positions]
×
NEW
62
    return positions, labels
×
63

64

65
class ArchivePlotter:
1✔
66
    """Maintains a live matplotlib figure showing the GridArchive as a heatmap.
67

68
    The colour of each cell encodes a chosen scalar attribute of its elite
69
    (default: ``p``, the performance bias).  Empty cells are shown in a
70
    distinct neutral colour so it is easy to see how the archive fills up.
71

72
    Args:
73
        archive (GridArchive): The 2-D archive to visualise.
74
        attr (str): Instance attribute to use as colour value. Default ``"p"``.
75
        feat_names (Sequence[str]): Labels for the two feature axes.
76
        cmap (str): Matplotlib colormap name. Default ``"viridis"``.
77
        vmin / vmax (float | None): Fixed colour scale limits.  If ``None``
78
            (default), the scale is recomputed each frame from the data.
79
        figsize (tuple[float, float]): Figure size in inches.
80
        title (str): Figure window / suptitle text.
81
    """
82

83
    def __init__(
1✔
84
        self,
85
        archive: GridArchive,
86
        attr: str = "p",
87
        feat_names: Optional[Sequence[str]] = None,
88
        cmap: str = "viridis",
89
        vmin: Optional[float] = None,
90
        vmax: Optional[float] = None,
91
        figsize: tuple[float, float] = (7, 6),
92
        title: str = "MAP-Elites Archive",
93
    ):
NEW
94
        if len(archive.dimensions) != 2:
×
NEW
95
            raise ValueError(
×
96
                "ArchivePlotter only supports 2-D GridArchives. "
97
                f"Got dimensions={archive.dimensions}"
98
            )
99

NEW
100
        self._archive = archive
×
NEW
101
        self._attr = attr
×
NEW
102
        self._feat_names = feat_names or ["Feature 0", "Feature 1"]
×
NEW
103
        self._cmap = cmap
×
NEW
104
        self._vmin = vmin
×
NEW
105
        self._vmax = vmax
×
106

NEW
107
        self._fig, self._ax = plt.subplots(figsize=figsize)
×
NEW
108
        self._fig.suptitle(title, fontsize=13, fontweight="bold")
×
NEW
109
        plt.ion()  # non-blocking interactive mode
×
110

NEW
111
        rows, cols = int(archive.dimensions[0]), int(archive.dimensions[1])
×
NEW
112
        empty = np.full((rows, cols), np.nan)
×
113

114
        # Empty-cell background (neutral grey)
NEW
115
        bg_cmap = mcolors.ListedColormap(["#d0d0d0"])
×
NEW
116
        self._ax.imshow(
×
117
            np.zeros_like(empty),
118
            cmap=bg_cmap,
119
            aspect="auto",
120
            origin="lower",
121
            extent=[-0.5, cols - 0.5, -0.5, rows - 0.5],
122
        )
123

124
        # Elite heatmap layer (NaN = transparent → shows grey background)
NEW
125
        cm = plt.get_cmap(self._cmap).copy()
×
NEW
126
        cm.set_bad(color="none")  # NaN → transparent
×
NEW
127
        self._im = self._ax.imshow(
×
128
            empty,
129
            cmap=cm,
130
            aspect="auto",
131
            origin="lower",
132
            extent=[-0.5, cols - 0.5, -0.5, rows - 0.5],
133
            interpolation="nearest",
134
        )
135

136
        # Colour bar
NEW
137
        self._cbar = self._fig.colorbar(self._im, ax=self._ax, pad=0.02)
×
NEW
138
        self._cbar.set_label(f"Elite  '{attr}'  value", fontsize=10)
×
139

140
        # Axis labels & ticks
NEW
141
        self._ax.set_xlabel(self._feat_names[0], fontsize=11)
×
NEW
142
        self._ax.set_ylabel(self._feat_names[1], fontsize=11)
×
143

NEW
144
        x_pos, x_lbl = _axis_tick_labels(archive, dim=0)
×
NEW
145
        y_pos, y_lbl = _axis_tick_labels(archive, dim=1)
×
NEW
146
        self._ax.set_xticks(x_pos)
×
NEW
147
        self._ax.set_xticklabels(x_lbl, fontsize=8)
×
NEW
148
        self._ax.set_yticks(y_pos)
×
NEW
149
        self._ax.set_yticklabels(y_lbl, fontsize=8)
×
150

151
        # Stats text box (top-left inside axes)
NEW
152
        self._info = self._ax.text(
×
153
            0.02,
154
            0.97,
155
            "",
156
            transform=self._ax.transAxes,
157
            fontsize=9,
158
            verticalalignment="top",
159
            bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.7),
160
        )
161

NEW
162
        self._fig.tight_layout()
×
NEW
163
        plt.show(block=False)
×
NEW
164
        plt.pause(0.01)
×
165

166
    def update(self, generation: int = 0) -> None:
1✔
167
        """Redraws the heatmap with the current state of the archive.
168

169
        Call this once per generation inside your evolution loop.
170

171
        Args:
172
            generation: Current generation number (shown in the stats box).
173
        """
NEW
174
        matrix = _archive_to_matrix(self._archive, self._attr)
×
175

NEW
176
        vmin = (
×
177
            self._vmin
178
            if self._vmin is not None
179
            else np.nanmin(matrix)
180
            if not np.all(np.isnan(matrix))
181
            else 0.0
182
        )
NEW
183
        vmax = (
×
184
            self._vmax
185
            if self._vmax is not None
186
            else np.nanmax(matrix)
187
            if not np.all(np.isnan(matrix))
188
            else 1.0
189
        )
190

NEW
191
        self._im.set_data(matrix)
×
NEW
192
        self._im.set_clim(vmin=vmin, vmax=vmax)
×
193

NEW
194
        filled = len(self._archive)
×
NEW
195
        total = int(self._archive.n_cells)
×
NEW
196
        coverage = 100 * filled / total if total > 0 else 0.0
×
197

NEW
198
        non_nan = matrix[~np.isnan(matrix)]
×
NEW
199
        mean_p = float(np.mean(non_nan)) if non_nan.size else 0.0
×
NEW
200
        max_p = float(np.max(non_nan)) if non_nan.size else 0.0
×
201

NEW
202
        self._info.set_text(
×
203
            f"Generation : {generation}\n"
204
            f"Cells      : {filled} / {total}  ({coverage:.1f}%)\n"
205
            f"Mean p     : {mean_p:.4f}\n"
206
            f"Max  p     : {max_p:.4f}"
207
        )
NEW
208
        self._fig.canvas.draw()
×
NEW
209
        self._fig.canvas.flush_events()
×
NEW
210
        plt.pause(0.001)
×
211

212
    def save(self, path: str, dpi: int = 150) -> None:
1✔
213
        """Saves the current figure to *path*.
214

215
        Args:
216
            path: Output file path (e.g. ``"archive_gen200.png"``).
217
            dpi:  Resolution. Default 150.
218
        """
NEW
219
        self._fig.savefig(path, dpi=dpi, bbox_inches="tight")
×
NEW
220
        print(f"[ArchivePlotter] Saved → {path}")
×
221

222
    def show(self) -> None:
1✔
223
        """Blocks until the figure window is closed (call at end of run)."""
NEW
224
        plt.ioff()
×
NEW
225
        plt.show()
×
226

227
    def close(self) -> None:
1✔
228
        """Closes the figure programmatically."""
NEW
229
        plt.close(self._fig)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc