Skip to content

plot

crosspeak.plot

plot_contour(matrix, wavenumbers, *, wavenumbers_2=None, wavenumbers_x=None, ax=None, title=None, cmap='vlag', n_levels=50, descending=True, mask_diagonal=False)

Contour plot of a 2D correlation matrix.

The matrix is indexed [ν₁, ν₂], as returned by synchronous, asynchronous and their heterospectral counterparts, and is drawn with ν₁ on the x-axis and ν₂ on the y-axis. That is the orientation Noda's rules are usually read in: for an asynchronous map, a positive peak at (x, y) alongside a positive synchronous peak means the band at x changes before the band at y. The axes are labelled ν₁ and ν₂ so the orientation is never left implicit.

Parameters:

Name Type Description Default
matrix ndarray

2D correlation matrix, shape (n_1, n_2).

required
wavenumbers ndarray

ν₁ axis, length n_1 (matrix rows). Drawn on the x-axis.

required
wavenumbers_2 ndarray | None

ν₂ axis, length n_2 (matrix columns). Drawn on the y-axis. If None, wavenumbers is used for both axes — the homospectral case.

None
wavenumbers_x ndarray | None

Deprecated alias for wavenumbers_2. Despite its name it is the ν₂ axis and is drawn on the y-axis.

None
Source code in src/crosspeak/plot.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def plot_contour(
    matrix: np.ndarray,
    wavenumbers: np.ndarray,
    *,
    wavenumbers_2: np.ndarray | None = None,
    wavenumbers_x: np.ndarray | None = None,
    ax=None,
    title: str | None = None,
    cmap: str = "vlag",
    n_levels: int = 50,
    descending: bool = True,
    mask_diagonal: bool = False,
):
    """
    Contour plot of a 2D correlation matrix.

    The matrix is indexed [ν₁, ν₂], as returned by `synchronous`, `asynchronous`
    and their heterospectral counterparts, and is drawn with ν₁ on the x-axis
    and ν₂ on the y-axis. That is the orientation Noda's rules are usually read
    in: for an asynchronous map, a positive peak at (x, y) alongside a positive
    synchronous peak means the band at x changes before the band at y. The axes
    are labelled ν₁ and ν₂ so the orientation is never left implicit.

    Parameters
    ----------
    matrix
        2D correlation matrix, shape (n_1, n_2).
    wavenumbers
        ν₁ axis, length n_1 (matrix rows). Drawn on the x-axis.
    wavenumbers_2
        ν₂ axis, length n_2 (matrix columns). Drawn on the y-axis. If None,
        `wavenumbers` is used for both axes — the homospectral case.
    wavenumbers_x
        Deprecated alias for `wavenumbers_2`. Despite its name it is the ν₂
        axis and is drawn on the y-axis.
    """

    if mask_diagonal and matrix.shape[0] == matrix.shape[1]:
        matrix = matrix.astype(float, copy=True)
        np.fill_diagonal(matrix, np.nan)

    wavenumbers_2 = _second_axis(wavenumbers_2, wavenumbers_x)
    homospectral = wavenumbers_2 is None

    matrix = np.asarray(matrix)
    wavenumbers = np.asarray(wavenumbers)
    wavenumbers_2 = wavenumbers if homospectral else np.asarray(wavenumbers_2)

    if matrix.ndim != 2:
        raise ValueError(f"matrix must be 2D, got shape {matrix.shape}")
    if wavenumbers.ndim != 1 or wavenumbers_2.ndim != 1:
        raise ValueError("wavenumber axes must be 1D")
    if matrix.shape[0] != wavenumbers.size:
        raise ValueError(
            f"matrix.shape[0] ({matrix.shape[0]}) must equal wavenumbers size ({wavenumbers.size})"
        )
    if matrix.shape[1] != wavenumbers_2.size:
        raise ValueError(
            f"matrix.shape[1] ({matrix.shape[1]}) must equal "
            f"wavenumbers_2 size ({wavenumbers_2.size})"
        )

    if mask_diagonal and matrix.shape[0] == matrix.shape[1]:
        matrix = matrix.astype(float, copy=True)
        np.fill_diagonal(matrix, np.nan)
    if wavenumbers.ndim != 1:
        raise ValueError(f"wavenumbers must be 1D, got shape {wavenumbers.shape}")
    if matrix.shape[0] != wavenumbers.size:
        raise ValueError(
            f"matrix dimension {matrix.shape[0]} doesn't match "
            f"wavenumber axis length {wavenumbers.size}"
        )

    if ax is None:
        _, ax = plt.subplots(layout="constrained")

    # Pin zero to the middle of the diverging colormap regardless of data asymmetry
    vmax = np.nanmax(np.abs(matrix))
    if vmax == 0:
        vmax = 1.0
    norm = TwoSlopeNorm(vmin=-vmax, vcenter=0, vmax=vmax)

    # contourf places Z[i, j] at (x[j], y[i]), so the matrix is transposed to
    # put rows (ν₁) on x and columns (ν₂) on y.
    cf = ax.contourf(
        wavenumbers,
        wavenumbers_2,
        matrix.T,
        levels=n_levels,
        cmap=cmap,
        norm=norm,
    )

    ax.contour(
        wavenumbers,
        wavenumbers_2,
        matrix.T,
        levels=10,
        colors="black",
        linewidths=0.4,
        alpha=0.6,
    )

    if descending:
        ax.set_xlim(wavenumbers.max(), wavenumbers.min())
        ax.set_ylim(wavenumbers_2.max(), wavenumbers_2.min())
    else:
        ax.set_xlim(wavenumbers.min(), wavenumbers.max())
        ax.set_ylim(wavenumbers_2.min(), wavenumbers_2.max())

    ax.set_xlabel(_NU1_LABEL)
    ax.set_ylabel(_NU2_LABEL)
    ax.set_aspect("equal" if wavenumbers is wavenumbers_2 else "auto")

    if title:
        ax.set_title(title)

    plt.colorbar(cf, ax=ax, label="Correlation intensity")

    return ax

plot_sync_async(sync, asyn, wavenumbers, *, wavenumbers_2=None, wavenumbers_x=None, title=None, cmap='vlag', n_levels=50, descending=True, figsize=(12, 5))

Plot synchronous and asynchronous 2DCOS matrices side-by-side.

Convenience wrapper around two plot_contour calls. The asynchronous panel has its diagonal masked automatically. Figure uses constrained layout, so colorbars and titles don't crowd the plot area.

Parameters:

Name Type Description Default
sync ndarray

Synchronous correlation matrix.

required
asyn ndarray

Asynchronous correlation matrix. Must have the same shape as sync.

required
wavenumbers ndarray

ν₁ axis (matrix rows), drawn on the x-axis of both panels.

required
wavenumbers_2 ndarray | None

ν₂ axis (matrix columns), drawn on the y-axis. If None, wavenumbers is used for both axes (homospectral). For heterospectral plots, supply the second series' wavenumbers here.

None
wavenumbers_x ndarray | None

Deprecated alias for wavenumbers_2.

None
title str | None

Optional figure-level title (suptitle).

None
cmap str

Passed through to plot_contour / plt.subplots as expected.

'vlag'
n_levels str

Passed through to plot_contour / plt.subplots as expected.

'vlag'
descending str

Passed through to plot_contour / plt.subplots as expected.

'vlag'
figsize str

Passed through to plot_contour / plt.subplots as expected.

'vlag'

Returns:

Type Description
(fig, axes)

axes is a length-2 numpy array of matplotlib Axes. Unpack as fig, (ax_sync, ax_async) = plot_sync_async(...) to reference each.

Raises:

Type Description
ValueError

If sync and asyn have different shapes.

Source code in src/crosspeak/plot.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def plot_sync_async(
    sync: np.ndarray,
    asyn: np.ndarray,
    wavenumbers: np.ndarray,
    *,
    wavenumbers_2: np.ndarray | None = None,
    wavenumbers_x: np.ndarray | None = None,
    title: str | None = None,
    cmap: str = "vlag",
    n_levels: int = 50,
    descending: bool = True,
    figsize: tuple[float, float] = (12, 5),
):
    """Plot synchronous and asynchronous 2DCOS matrices side-by-side.

    Convenience wrapper around two `plot_contour` calls. The asynchronous
    panel has its diagonal masked automatically. Figure uses constrained
    layout, so colorbars and titles don't crowd the plot area.

    Parameters
    ----------
    sync
        Synchronous correlation matrix.
    asyn
        Asynchronous correlation matrix. Must have the same shape as `sync`.
    wavenumbers
        ν₁ axis (matrix rows), drawn on the x-axis of both panels.
    wavenumbers_2
        ν₂ axis (matrix columns), drawn on the y-axis. If None, `wavenumbers`
        is used for both axes (homospectral). For heterospectral plots, supply
        the second series' wavenumbers here.
    wavenumbers_x
        Deprecated alias for `wavenumbers_2`.
    title
        Optional figure-level title (suptitle).
    cmap, n_levels, descending, figsize
        Passed through to `plot_contour` / `plt.subplots` as expected.

    Returns
    -------
    (fig, axes)
        `axes` is a length-2 numpy array of matplotlib Axes. Unpack as
        `fig, (ax_sync, ax_async) = plot_sync_async(...)` to reference each.

    Raises
    ------
    ValueError
        If `sync` and `asyn` have different shapes.
    """
    wavenumbers_2 = _second_axis(wavenumbers_2, wavenumbers_x)
    if sync.shape != asyn.shape:
        raise ValueError(
            f"sync and asyn must have the same shape; got {sync.shape} and {asyn.shape}"
        )

    fig, axes = plt.subplots(1, 2, figsize=figsize, layout="constrained")
    plot_contour(
        sync,
        wavenumbers,
        wavenumbers_2=wavenumbers_2,
        ax=axes[0],
        title="Synchronous",
        cmap=cmap,
        n_levels=n_levels,
        descending=descending,
    )
    plot_contour(
        asyn,
        wavenumbers,
        wavenumbers_2=wavenumbers_2,
        ax=axes[1],
        title="Asynchronous",
        cmap=cmap,
        n_levels=n_levels,
        descending=descending,
        mask_diagonal=True,
    )
    if title is not None:
        fig.suptitle(title)
    return fig, axes