Skip to content

noda

crosspeak.noda

AutopeakResult

Bases: NamedTuple

Autopeaks found on a synchronous diagonal.

Attributes:

Name Type Description
positions ndarray

Wavenumbers at which autopeaks were detected, in the order of the input wavenumber axis.

intensities ndarray

Autopower values (diagonal of the synchronous matrix) at those positions.

Source code in src/crosspeak/noda.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
class AutopeakResult(NamedTuple):
    """Autopeaks found on a synchronous diagonal.

    Attributes
    ----------
    positions
        Wavenumbers at which autopeaks were detected, in the order of the
        input wavenumber axis.
    intensities
        Autopower values (diagonal of the synchronous matrix) at those
        positions.
    """

    positions: np.ndarray
    intensities: np.ndarray

hilbert_noda_matrix(n)

The Hilbert-Noda matrix for n perturbation points.

The discrete kernel that turns the asynchronous calculation into a matrix product: with N in hand, Ψ = Y^T @ N @ Y / (m - 1). Off the diagonal N_jk = 1 / (π(k - j)); the diagonal is zero, since a point has no phase lag against itself.

The sign convention matters. With N positive in the upper triangle, a positive Ψ(ν₁, ν₂) alongside a positive Φ means ν₁ leads ν₂ under Noda's rules. Transpose N and every ordering flips — so if this ever gets swapped out, check the sign against a sequence you already know.

Parameters:

Name Type Description Default
n

Number of perturbation points (m in the usual notation), at least 2.

required

Returns:

Type Description
ndarray

The (n, n) matrix, antisymmetric with a zero diagonal.

Source code in src/crosspeak/noda.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def hilbert_noda_matrix(n):
    """The Hilbert-Noda matrix for `n` perturbation points.

    The discrete kernel that turns the asynchronous calculation into a matrix
    product: with N in hand, Ψ = Y^T @ N @ Y / (m - 1). Off the diagonal
    N_jk = 1 / (π(k - j)); the diagonal is zero, since a point has no phase
    lag against itself.

    The sign convention matters. With N positive in the upper triangle, a
    positive Ψ(ν₁, ν₂) alongside a positive Φ means ν₁ leads ν₂ under Noda's
    rules. Transpose N and every ordering flips — so if this ever gets swapped
    out, check the sign against a sequence you already know.

    Parameters
    ----------
    n
        Number of perturbation points (m in the usual notation), at least 2.

    Returns
    -------
    np.ndarray
        The (n, n) matrix, antisymmetric with a zero diagonal.
    """
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"n must be an integer, got {type(n).__name__}")
    if n < 2:
        raise ValueError(f"need at least 2 perturbation points, got {n}")

    indices = np.arange(n)
    diff = indices[None, :] - indices[:, None]

    matrix = np.zeros((n, n))
    off_diagonal = diff != 0
    matrix[off_diagonal] = 1.0 / (np.pi * diff[off_diagonal])

    return matrix

synchronous(series)

Synchronous 2DCOS matrix Φ of a spectral series.

Φ(ν₁, ν₂) = Y^T @ Y / (m - 1) over the mean-centred intensities, which is just the sample covariance across the perturbation. Large positive values mean two wavenumbers rise and fall together; negative means they move in opposition. The diagonal is the autopower spectrum — where the perturbation does the most work.

Parameters:

Name Type Description Default
series

A SpectralSeries with at least two perturbation points.

required

Returns:

Type Description
ndarray

The (n, n) synchronous matrix, symmetric by construction.

Source code in src/crosspeak/noda.py
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
def synchronous(series):
    """Synchronous 2DCOS matrix Φ of a spectral series.

    Φ(ν₁, ν₂) = Y^T @ Y / (m - 1) over the mean-centred intensities, which is
    just the sample covariance across the perturbation. Large positive values
    mean two wavenumbers rise and fall together; negative means they move in
    opposition. The diagonal is the autopower spectrum — where the perturbation
    does the most work.

    Parameters
    ----------
    series
        A `SpectralSeries` with at least two perturbation points.

    Returns
    -------
    np.ndarray
        The (n, n) synchronous matrix, symmetric by construction.
    """
    if not isinstance(series, SpectralSeries):
        raise TypeError(f"expected SpectralSeries, got {type(series).__name__}")

    m = series.n_perturbations
    Y = series.intensities - series.intensities.mean(axis=0)
    return (Y.T @ Y) / (m - 1)

asynchronous(series)

Asynchronous 2DCOS matrix Ψ of a spectral series.

Ψ(ν₁, ν₂) = Y^T @ N @ Y / (m - 1), with N the Hilbert-Noda matrix. Where Φ says which bands move together, Ψ says in what order: read against the sign of Φ, a nonzero Ψ resolves which of two overlapping features responds first. Antisymmetric, with a zero diagonal.

Same mean-centring and conventions as synchronous; see hilbert_noda_matrix for the sign that fixes the direction of the ordering.

Parameters:

Name Type Description Default
series

A SpectralSeries with at least two perturbation points.

required

Returns:

Type Description
ndarray

The (n, n) asynchronous matrix.

Source code in src/crosspeak/noda.py
 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
def asynchronous(series):
    """Asynchronous 2DCOS matrix Ψ of a spectral series.

    Ψ(ν₁, ν₂) = Y^T @ N @ Y / (m - 1), with N the Hilbert-Noda matrix. Where Φ
    says which bands move together, Ψ says in what order: read against the sign
    of Φ, a nonzero Ψ resolves which of two overlapping features responds
    first. Antisymmetric, with a zero diagonal.

    Same mean-centring and conventions as `synchronous`; see
    `hilbert_noda_matrix` for the sign that fixes the direction of the ordering.

    Parameters
    ----------
    series
        A `SpectralSeries` with at least two perturbation points.

    Returns
    -------
    np.ndarray
        The (n, n) asynchronous matrix.
    """
    if not isinstance(series, SpectralSeries):
        raise TypeError(f"expected SpectralSeries, got {type(series).__name__}")

    m = series.n_perturbations
    Y = series.intensities - series.intensities.mean(axis=0)
    N = hilbert_noda_matrix(m)
    return (Y.T @ N @ Y) / (m - 1)

find_autopeaks(phi, wavenumbers, *, prominence_frac=0.02, **kwargs)

Detect autopeaks on the diagonal of a synchronous 2DCOS matrix.

The diagonal of Phi is the autopower spectrum — the variance of each wavenumber across the perturbation series. Local maxima of this spectrum are Noda's autopeaks and identify the wavenumbers that change most strongly under the perturbation.

Parameters:

Name Type Description Default
phi ndarray

Synchronous 2DCOS matrix, shape (n, n), as returned by synchronous().

required
wavenumbers ndarray

Wavenumber axis, shape (n,), strictly monotonic.

required
prominence_frac float

Minimum peak prominence, as a fraction of the diagonal maximum. Default 0.02. Set to 0 to disable the prominence filter.

0.02
**kwargs

Additional keyword arguments passed through to scipy.signal.find_peaks (e.g. height, width, distance). Passing prominence directly overrides the value derived from prominence_frac.

{}

Returns:

Type Description
AutopeakResult

Named tuple of (positions, intensities). Empty arrays if none found.

Source code in src/crosspeak/noda.py
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def find_autopeaks(
    phi: np.ndarray,
    wavenumbers: np.ndarray,
    *,
    prominence_frac: float = 0.02,
    **kwargs,
) -> AutopeakResult:
    """Detect autopeaks on the diagonal of a synchronous 2DCOS matrix.

    The diagonal of Phi is the autopower spectrum — the variance of each
    wavenumber across the perturbation series. Local maxima of this spectrum
    are Noda's autopeaks and identify the wavenumbers that change most
    strongly under the perturbation.

    Parameters
    ----------
    phi
        Synchronous 2DCOS matrix, shape (n, n), as returned by `synchronous()`.
    wavenumbers
        Wavenumber axis, shape (n,), strictly monotonic.
    prominence_frac
        Minimum peak prominence, as a fraction of the diagonal maximum.
        Default 0.02. Set to 0 to disable the prominence filter.
    **kwargs
        Additional keyword arguments passed through to `scipy.signal.find_peaks`
        (e.g. `height`, `width`, `distance`). Passing `prominence` directly
        overrides the value derived from `prominence_frac`.

    Returns
    -------
    AutopeakResult
        Named tuple of (positions, intensities). Empty arrays if none found.
    """
    phi = np.asarray(phi)
    wavenumbers = np.asarray(wavenumbers)

    if phi.ndim != 2:
        raise ValueError(f"phi must be 2D, got shape {phi.shape}")
    if phi.shape[0] != phi.shape[1]:
        raise ValueError(f"phi must be square, got shape {phi.shape}")
    if wavenumbers.ndim != 1:
        raise ValueError(f"wavenumbers must be 1D, got shape {wavenumbers.shape}")
    if wavenumbers.size != phi.shape[0]:
        raise ValueError(
            f"wavenumbers size ({wavenumbers.size}) does not match phi shape ({phi.shape})"
        )

    diagonal = np.diag(phi)

    # User-supplied absolute prominence wins over prominence_frac
    kwargs.setdefault("prominence", prominence_frac * diagonal.max())

    indices, _ = find_peaks(diagonal, **kwargs)

    return AutopeakResult(
        positions=wavenumbers[indices],
        intensities=diagonal[indices],
    )

synchronous_hetero(series_1, series_2)

Heterospectral synchronous 2DCOS matrix.

Computes Φ(ν₁, ν₂) = Y_1^T @ Y_2 / (m - 1), where Y_1 and Y_2 are the mean-centred intensities of the two series. The two series must share the same perturbation array (rows are paired by index).

Parameters:

Name Type Description Default
series_1 SpectralSeries

SpectralSeries supplying ν₁ — maps to matrix axis 0 (rows), and to the y-axis when the result is plotted.

required
series_2 SpectralSeries

SpectralSeries supplying ν₂ — maps to matrix axis 1 (cols), and to the x-axis when the result is plotted.

required

Returns:

Type Description
ndarray

Shape (n_1, n_2). Not symmetric in general.

Raises:

Type Description
ValueError

If the two series have different perturbation arrays.

Source code in src/crosspeak/noda.py
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
def synchronous_hetero(
    series_1: SpectralSeries,
    series_2: SpectralSeries,
) -> np.ndarray:
    """Heterospectral synchronous 2DCOS matrix.

    Computes Φ(ν₁, ν₂) = Y_1^T @ Y_2 / (m - 1), where Y_1 and Y_2 are the
    mean-centred intensities of the two series. The two series must share
    the same perturbation array (rows are paired by index).

    Parameters
    ----------
    series_1
        SpectralSeries supplying ν₁ — maps to matrix axis 0 (rows), and to
        the y-axis when the result is plotted.
    series_2
        SpectralSeries supplying ν₂ — maps to matrix axis 1 (cols), and to
        the x-axis when the result is plotted.

    Returns
    -------
    np.ndarray
        Shape `(n_1, n_2)`. Not symmetric in general.

    Raises
    ------
    ValueError
        If the two series have different perturbation arrays.
    """
    _check_paired_perturbations(series_1, series_2)
    m = series_1.n_perturbations
    Y_1 = series_1.intensities - series_1.intensities.mean(axis=0)
    Y_2 = series_2.intensities - series_2.intensities.mean(axis=0)
    return (Y_1.T @ Y_2) / (m - 1)

asynchronous_hetero(series_1, series_2)

Heterospectral asynchronous 2DCOS matrix.

Computes Ψ(ν₁, ν₂) = Y_1^T @ N @ Y_2 / (m - 1), where N is the Hilbert-Noda matrix of shape (m, m).

See synchronous_hetero for axis conventions.

Raises:

Type Description
ValueError

If the two series have different perturbation arrays.

Source code in src/crosspeak/noda.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def asynchronous_hetero(
    series_1: SpectralSeries,
    series_2: SpectralSeries,
) -> np.ndarray:
    """Heterospectral asynchronous 2DCOS matrix.

    Computes Ψ(ν₁, ν₂) = Y_1^T @ N @ Y_2 / (m - 1), where N is the
    Hilbert-Noda matrix of shape (m, m).

    See `synchronous_hetero` for axis conventions.

    Raises
    ------
    ValueError
        If the two series have different perturbation arrays.
    """
    _check_paired_perturbations(series_1, series_2)
    m = series_1.n_perturbations
    Y_1 = series_1.intensities - series_1.intensities.mean(axis=0)
    Y_2 = series_2.intensities - series_2.intensities.mean(axis=0)
    N = hilbert_noda_matrix(m)
    return (Y_1.T @ N @ Y_2) / (m - 1)