Skip to content

Spectrum2D #

Spectrum2D(
    f1: FloatArray | None = None,
    f2: FloatArray | None = None,
    amplitudes: ndarray | None = None,
    f1_unit: FrequencyUnit = HERTZ,
    f2_unit: FrequencyUnit = HERTZ,
    metadata: dict[str, Any] | None = None,
    reference_spectrum_f1: Spectrum1D | None = None,
    reference_spectrum_f2: Spectrum1D | None = None,
    f1_reference_frequency_hz: float | None = None,
    f2_reference_frequency_hz: float | None = None,
)

2D frequency-domain spectrum for NMR experiments (e.g., COSY, HSQC).

Attributes:

Name Type Description
f1, f2

FloatArray (np.ndarray[float64]) - frequency axes

amplitudes ndarray

np.ndarray[complex] with shape (len(f1), len(f2))

f1_unit, f2_unit

FrequencyUnit

f1_reference_frequency_hz, f2_reference_frequency_hz

Reference frequencies for PPM conversion

processing_history class-attribute instance-attribute #

processing_history: ProcessingHistory | None = (
    betterproto2.field(
        8, betterproto2.TYPE_MESSAGE, optional=True
    )
)

Processing history - read-only audit log of all operations applied to this spectrum

history property #

history: Sequence[ProcessingHistoryEntry]

Read-only access to processing history entries.

Returns:

Type Description
Sequence[ProcessingHistoryEntry]

Immutable sequence of history entries. Returns empty tuple if

Sequence[ProcessingHistoryEntry]

no history has been recorded yet.

_frequency_axis_1 class-attribute instance-attribute #

_frequency_axis_1: NdAxis | None = betterproto2.field(
    1, betterproto2.TYPE_MESSAGE, optional=True
)

Exactly two frequency axes (enforced by validation)

e.g., f1

_frequency_axis_2 class-attribute instance-attribute #

_frequency_axis_2: NdAxis | None = betterproto2.field(
    2, betterproto2.TYPE_MESSAGE, optional=True
)

e.g., f2

_amplitudes class-attribute instance-attribute #

_amplitudes: NdComplexArray | None = betterproto2.field(
    3, betterproto2.TYPE_MESSAGE, optional=True
)

2D complex amplitude data (shape = [N, M])

metadata class-attribute instance-attribute #

metadata: dict[str, str] = betterproto2.field(
    5,
    betterproto2.TYPE_MAP,
    map_meta=betterproto2.map_meta(
        betterproto2.TYPE_STRING, betterproto2.TYPE_STRING
    ),
)

Field 4 removed - reference_frequency_hz now stored at axis level (NDAxis.reference_frequency_hz)

Optional metadata

amplitudes property writable #

amplitudes: ndarray

_points_f1_view instance-attribute #

_points_f1_view: ndarray | None

_points_f2_view instance-attribute #

_points_f2_view: ndarray | None

_np_dirty_points_f1 instance-attribute #

_np_dirty_points_f1: bool

_np_dirty_points_f2 instance-attribute #

_np_dirty_points_f2: bool

points_f1 property writable #

points_f1: FloatArray

points_f2 property writable #

points_f2: FloatArray

_visualization_handler class-attribute instance-attribute #

_visualization_handler: str = ''

visualization_handler property writable #

visualization_handler: str

Plugin visualization handler ID (e.g., 'nanalysis:cosy_2d').

x property writable #

x: FloatArray

x_units property writable #

x_units: FrequencyUnit

y property writable #

y: FloatArray

y_units property writable #

y_units: FrequencyUnit

nucleus_f1 property writable #

nucleus_f1: Nucleus

Nucleus type for the F1 (indirect) axis.

nucleus_f2 property writable #

nucleus_f2: Nucleus

Nucleus type for the F2 (direct) axis.

role_f1 property writable #

role_f1: AxisRole

Physical role of the F1 (indirect) axis.

role_f2 property writable #

role_f2: AxisRole

Physical role of the F2 (direct) axis.

z property writable #

z: FloatArray

__module__ class-attribute instance-attribute #

__module__ = GrpcSpectrum2D.__module__

f1_unit property writable #

f1_unit: FrequencyUnit

Frequency unit for first dimension (F1/indirect).

f2_unit property writable #

f2_unit: FrequencyUnit

Frequency unit for second dimension (F2/direct).

f1_reference_frequency_hz property writable #

f1_reference_frequency_hz: float | None

Reference frequency for F1 dimension (for PPM conversion).

f2_reference_frequency_hz property writable #

f2_reference_frequency_hz: float | None

Reference frequency for F2 dimension (for PPM conversion).

reference_spectrum_f1 instance-attribute #

reference_spectrum_f1 = reference_spectrum_f1

reference_spectrum_f2 instance-attribute #

reference_spectrum_f2 = reference_spectrum_f2

f1 property writable #

f1: FloatArray

Frequency coordinates for first dimension.

f2 property writable #

f2: FloatArray

Frequency coordinates for second dimension.

shape property #

shape: tuple[int, int]

is_complex property #

is_complex: bool

Whether the spectrum has complex amplitudes.

_init_history #

_init_history() -> None

Initialize the processing history.

Called from init of Signal1D/Spectrum1D. Always creates a ProcessingHistory instance - history is always enabled.

_record_history_entry #

_record_history_entry(
    operation: str,
    parameters: dict[str, str],
    shape_before: tuple[int, ...],
    shape_after: tuple[int, ...],
    source: str = "tqt_nmr",
) -> None

Record a processing operation to the history.

This is an internal method - not exposed to users. Called by @track_operation decorator and TrackedArray.

Parameters:

Name Type Description Default
operation str

Name of the operation (e.g., "correct_phase_manual").

required
parameters dict[str, str]

Operation parameters as string key-value pairs.

required
shape_before tuple[int, ...]

Shape of amplitudes before operation.

required
shape_after tuple[int, ...]

Shape of amplitudes after operation.

required
source str

Source of the operation (e.g., "tqt_nmr", "numpy").

'tqt_nmr'

_copy_history_to #

_copy_history_to(other: Self) -> None

Copy processing history to another object.

Used by copy() methods to preserve history lineage.

Parameters:

Name Type Description Default
other Self

Object to copy history to.

required

find_peaks #

find_peaks(
    min_distance_points: int | None = None,
    min_height_ratio: float = 0.05,
    use_magnitude: bool = True,
    adaptive: bool = False,
    snr_threshold: float = 5.0,
    method: Literal[
        "local_max", "topology", "hsqc"
    ] = "local_max",
    topology_limit: int = 15,
) -> list[PeakInfo2D]

Find peaks in 2D spectrum.

Parameters:

Name Type Description Default
min_distance_points int | None

Minimum peak separation in points (in each dimension). If None, auto-calculates as ~2% of each axis.

None
min_height_ratio float

Minimum peak height as ratio of max height (0-1). Ignored if adaptive=True.

0.05
use_magnitude bool

If True, work on |z| else on Re(z).

True
adaptive bool

If True, use noise-adaptive thresholds instead of fixed ratios.

False
snr_threshold float

When adaptive=True, minimum SNR for peak detection.

5.0
method Literal['local_max', 'topology', 'hsqc']

Peak picking algorithm: "local_max" — classical local maximum + threshold (fast, default). "hsqc" — Q99.9 threshold + f1_ratio filter, designed for HSQC spectra where t1 noise ridges are the dominant artifact.

'local_max'
topology_limit int

Minimum topological persistence for "topology" method. Higher = fewer but more confident peaks.

15

Returns:

Type Description
list[PeakInfo2D]

List of PeakInfo2D objects containing peak positions and heights.

calculate_peak_volumes #

calculate_peak_volumes(
    peaks: list[PeakInfo2D],
    radius_factor: float = 3.0,
    use_magnitude: bool = True,
) -> list[PeakInfo2D]

Calculate volumes for 2D peaks using elliptical integration regions.

For each peak, estimates the peak width in both dimensions by finding the half-height contour, then integrates within an ellipse of radius_factor * half-width.

Parameters:

Name Type Description Default
peaks list[PeakInfo2D]

List of PeakInfo2D objects from find_peaks().

required
radius_factor float

Multiplier for the half-width to define the integration ellipse (default 3.0 = ~99% of Gaussian).

3.0
use_magnitude bool

If True, integrate |z|, else integrate Re(z).

True

Returns:

Type Description
list[PeakInfo2D]

The same peaks with volume field populated.

estimate_snr #

estimate_snr(
    signal_region: tuple[
        tuple[float, float], tuple[float, float]
    ]
    | None = None,
) -> float

Estimate signal-to-noise ratio of the 2D spectrum.

Uses Median Absolute Deviation (MAD) for robust noise estimation.

Parameters:

Name Type Description Default
signal_region tuple[tuple[float, float], tuple[float, float]] | None

Optional ((f1_min, f1_max), (f2_min, f2_max)) to restrict signal calculation. If None, uses entire spectrum.

None

Returns:

Type Description
float

Signal-to-noise ratio (linear scale)

infer_multiplets #

infer_multiplets(
    peaks: list[PeakInfo2D] | None = None,
    group_axis: Literal["f1", "f2", "both"] = "f1",
    tolerance: float | None = None,
    snr_threshold: float = 30.0,
    margin_factor: float = 2.0,
) -> list[MultipletGroup2D]

Infer multiplet patterns from 2D spectrum using slice-and-project.

For each cluster of 2D peaks sharing a grouping-axis coordinate, cuts a rectangle from the 2D spectrum, projects (sums) along the grouping axis to produce a 1D micro-spectrum along the spread axis, then runs 1D multiplet scoring (spacing regularity + amplitude symmetry) on the projection peaks.

For HSQC (group_axis="f1"): Groups by 13C shift → projects onto 1H axis → detects 1H multiplet. For COSY (group_axis="f2"): Groups by 1H shift → projects onto 13C axis → detects coupling partners. For general (group_axis="both"): Groups in 2D using normalized distance, then projects each cluster.

Parameters:

Name Type Description Default
peaks list[PeakInfo2D] | None

Pre-detected peaks. If None, runs find_peaks(adaptive=True).

None
group_axis Literal['f1', 'f2', 'both']

Which axis to cluster along. "f1" — group by F1 (indirect), spread along F2 gives multiplicity. "f2" — group by F2 (direct), spread along F1 gives multiplicity. "both" — group by 2D proximity (uses existing group_peaks).

'f1'
tolerance float | None

Max distance on the grouping axis. If None, auto-calculates as ~1% of that axis range.

None
snr_threshold float

SNR threshold for peak detection (if peaks not provided).

30.0
margin_factor float

How much to expand the bounding box around each cluster (as a multiple of the cluster extent in each axis).

2.0

Returns:

Type Description
list[MultipletGroup2D]

List of MultipletGroup2D with pattern, coupling constant, and score.

group_peaks staticmethod #

group_peaks(
    peaks: list[PeakInfo2D], linkage_method: str = "single"
) -> list[PeakInfo2D]

Group 2D peaks using hierarchical clustering on normalized coordinates.

Normalizes F1 and F2 axes independently (min-max) so that clustering works correctly for heteronuclear experiments where axes have very different scales (e.g. 1H 0-10 ppm vs 13C 0-200 ppm).

Parameters:

Name Type Description Default
peaks list[PeakInfo2D]

List of PeakInfo2D objects from find_peaks().

required
linkage_method str

Linkage method for scipy hierarchical clustering.

'single'

Returns:

Type Description
list[PeakInfo2D]

The same peaks with group_id assigned.

_find_peaks_local_max #

_find_peaks_local_max(
    z: ndarray,
    min_distance_points: int | None,
    min_height_ratio: float,
    adaptive: bool,
    snr_threshold: float,
) -> list[PeakInfo2D]

Classical local maximum + threshold peak detection.

_find_peaks_hsqc #

_find_peaks_hsqc(
    z: ndarray,
    percentile: float = 99.9,
    f1_ratio_threshold: float = 3.0,
) -> list[PeakInfo2D]

HSQC-specialized peak picker with t1 noise rejection.

Two-step approach: 1. Q-percentile intensity threshold + local maxima detection. 2. f1_ratio filter: for each peak, compute height / mean(F1 column). Real cross-peaks stand out above their column (ratio >> 1), while t1 noise ridges elevate the entire column (ratio ~ 1-2).

Falls back to topology if too few peaks survive.

Parameters:

Name Type Description Default
z ndarray

2D magnitude array.

required
percentile float

Intensity percentile for hard threshold (default 99.9).

99.9
f1_ratio_threshold float

Minimum peak-to-column-mean ratio (default 3.0).

3.0

__post_init__ #

__post_init__() -> None

__bytes__ #

__bytes__() -> bytes

load #

load(
    stream: SupportsRead[bytes], size: int | None = None
) -> Self

parse classmethod #

parse(data: bytes) -> Self

FromString classmethod #

FromString(s: bytes) -> Self

_pb_amplitudes #

_pb_amplitudes() -> NdComplexArray

_ensure_storage_initialized #

_ensure_storage_initialized() -> None

_mark_dirty #

_mark_dirty(field: str) -> None

_sync_proto_to_numpy #

_sync_proto_to_numpy() -> None

_pb_axis_f1 #

_pb_axis_f1() -> NdAxis

_pb_axis_f2 #

_pb_axis_f2() -> NdAxis

_ensure_axes_initialized #

_ensure_axes_initialized() -> None

to_nd #

to_nd(
    visualization_handler: str | None = None,
) -> SpectrumNd

__len__ #

__len__() -> int

__getitem__ #

__getitem__(key: tuple[int, int]) -> Self

Slice-only 2D indexing. Example: spec[:, 10:50] (Indexing with ints that reduce dimensionality is not supported here.)

copy #

copy() -> Self

Create a deep copy of this spectrum.

normalize #

normalize(
    method: Literal["max", "rms", "unit"] = "max",
) -> Self

Normalize spectrum amplitudes.

Parameters:

Name Type Description Default
method Literal['max', 'rms', 'unit']

Normalization method ("max", "rms", "unit")

'max'

Returns:

Type Description
Self

Self for method chaining

to_ppm #

to_ppm() -> Self

Convert spectrum frequency axes to PPM scale.

Returns:

Type Description
Self

New spectrum with PPM units (or copy if already in PPM)

Raises:

Type Description
ValueError

If reference frequency is not set for either axis and not already in PPM

smooth #

smooth(sigma: float = 2.0) -> Self

Apply 2D Gaussian smoothing to spectrum.

Parameters:

Name Type Description Default
sigma float

Standard deviation for Gaussian kernel

2.0

Returns:

Type Description
Self

Self for method chaining

symmetrize #

symmetrize() -> Self

Symmetrize a 2D homonuclear spectrum by averaging with its transpose.

This is valid only for homonuclear experiments (COSY, NOESY, TOCSY) where F1 and F2 correspond to the same nucleus, and the amplitude matrix is square.

Returns:

Type Description
Self

Self for method chaining

Raises:

Type Description
ProcessingParameterError

If the spectrum is not square or not homonuclear

trim_frequency_range #

trim_frequency_range(
    f1_min: float = -inf,
    f1_max: float = inf,
    f2_min: float = -inf,
    f2_max: float = inf,
) -> Self

Trim spectrum to specified frequency ranges.

Parameters:

Name Type Description Default
f1_min float

Range for f1 axis (from)

-inf
f1_max float

Range for f1 axis (to)

inf
f2_min float

Range for f2 axis (from)

-inf
f2_max float

Range for f2 axis (to)

inf

Returns:

Type Description
Self

A new, trimmed spectrum (the original is left unchanged).

correct_phase_manual #

correct_phase_manual(
    p0: float,
    p1: float = 0.0,
    axis: Literal["f1", "f2", "both"] = "both",
) -> Self

Apply manual phase correction to 2D spectrum.

Parameters:

Name Type Description Default
p0 float

Zero-order phase correction (degrees)

required
p1 float

First-order phase correction (degrees)

0.0
axis Literal['f1', 'f2', 'both']

Which axis to correct ("f1", "f2", or "both")

'both'

Returns:

Type Description
Self

Self for method chaining

correct_phase_auto #

correct_phase_auto(
    axis: Literal["f1", "f2", "both"] = "both",
    method: Literal["acme", "peak_minima"] = "acme",
) -> Self

Apply automatic phase correction to 2D spectrum.

Parameters:

Name Type Description Default
axis Literal['f1', 'f2', 'both']

Which axis to correct ("f1", "f2", or "both")

'both'
method Literal['acme', 'peak_minima']

Automatic phase correction method

'acme'

Returns:

Type Description
Self

Self for method chaining

correct_baseline #

correct_baseline(
    method: Literal[
        "polynomial", "plane", "edge", "als"
    ] = "polynomial",
    axis: Literal["f1", "f2", "both"] = "both",
    edge_fraction: float = 0.1,
    poly_order: int = 1,
    als_lambda: float = 100000.0,
    als_p: float = 0.01,
    als_iterations: int = 10,
) -> Self

Apply baseline correction to 2D spectrum.

Parameters:

Name Type Description Default
method Literal['polynomial', 'plane', 'edge', 'als']

Baseline correction method: - "polynomial": Fit polynomial to edge regions per row/column (default) - "plane": Fit and subtract a 2D plane using corner values - "edge": Simple DC offset from edge regions per row/column - "als": Asymmetric least squares per row/column

'polynomial'
axis Literal['f1', 'f2', 'both']

Which axis to correct ("f1", "f2", or "both")

'both'
edge_fraction float

Fraction of edges to use for baseline estimation (default: 0.1)

0.1
poly_order int

Polynomial order for "polynomial" method (default: 1)

1
als_lambda float

Smoothness parameter for ALS (default: 1e5)

100000.0
als_p float

Asymmetry parameter for ALS (default: 0.01)

0.01
als_iterations int

Number of ALS iterations (default: 10)

10

Returns:

Type Description
Self

Self for method chaining

convert_frequency_unit #

convert_frequency_unit(
    target_unit: FrequencyUnit,
    axis: Literal["f1", "f2", "both"] = "both",
) -> Self

Convert frequency axes to different units.

Parameters:

Name Type Description Default
target_unit FrequencyUnit

Target frequency unit

required
axis Literal['f1', 'f2', 'both']

Which axis to convert

'both'

Returns:

Type Description
Self

Self for method chaining

get_base_spectrum #

get_base_spectrum() -> Self

Get the main spectrum without reference data.

Returns a copy of this spectrum with reference fields set to None.

get_reference_f1 #

get_reference_f1() -> Spectrum1D | None

Get F1 (indirect dimension) reference spectrum.

get_reference_f2 #

get_reference_f2() -> Spectrum1D | None

Get F2 (direct dimension) reference spectrum.

has_references #

has_references() -> bool

Check if any reference spectra are attached.

apply_reference_correction #

apply_reference_correction() -> Self

Apply reference correction (division/deconvolution) to remove artifacts.

This is the default processing for 2D NMR with reference acquisitions. Divides each row/column by the corresponding reference spectrum.

Returns:

Type Description
Self

Self for method chaining

to_signal #

to_signal(
    t1_unit: TimeUnit = MICROSECOND,
    t2_unit: TimeUnit = MICROSECOND,
    carrier_offset_hz_1: float | None = None,
    carrier_offset_hz_2: float | None = None,
) -> Signal2D

Convert spectrum back to time-domain signal via inverse 2D FFT.

Parameters:

Name Type Description Default
t1_unit TimeUnit

Time unit for t1 axis (default: MICROSECOND)

MICROSECOND
t2_unit TimeUnit

Time unit for t2 axis (default: MICROSECOND)

MICROSECOND
carrier_offset_hz_1 float | None

Carrier offset for F1 in Hz. If provided, subtracted from F1 frequencies before inverse FFT and stored in output signal.

None
carrier_offset_hz_2 float | None

Carrier offset for F2 in Hz. If provided, subtracted from F2 frequencies before inverse FFT and stored in output signal.

None

Returns:

Name Type Description
Signal2D Signal2D

Time-domain 2D signal

from_proto classmethod #

from_proto(proto: Spectrum2D | SpectrumNd | None) -> Self

_setup_from_arrays #

_setup_from_arrays(
    f1: FloatArray,
    f2: FloatArray,
    amplitudes: ndarray,
    f1_unit: FrequencyUnit,
    f2_unit: FrequencyUnit,
) -> None

_baseline_plane #

_baseline_plane() -> None

Fit and subtract a 2D plane baseline using corner values.

_baseline_per_axis #

_baseline_per_axis(
    axis: Literal["f1", "f2", "both"],
    edge_fraction: float,
    correct_fn: Callable[[ndarray, float], ndarray],
) -> None

Apply 1D baseline correction per row/column along specified axis.

_sync_numpy_to_proto #

_sync_numpy_to_proto() -> None

Override to sync reference spectra before serialization.

_baseline_edge_1d staticmethod #

_baseline_edge_1d(
    arr: ndarray, edge_fraction: float
) -> ndarray

Simple DC offset correction using edge regions.

_baseline_poly_1d staticmethod #

_baseline_poly_1d(
    arr: ndarray, edge_fraction: float, poly_order: int
) -> ndarray

Polynomial baseline fit to edge regions.

_baseline_als_1d staticmethod #

_baseline_als_1d(
    arr: ndarray, lam: float, p: float, n_iter: int
) -> ndarray

Asymmetric least squares baseline correction for 1D array.