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
#
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
visualization_handler
property
writable
#
Plugin visualization handler ID (e.g., 'nanalysis:cosy_2d').
f1_unit
property
writable
#
Frequency unit for first dimension (F1/indirect).
f1_reference_frequency_hz
property
writable
#
Reference frequency for F1 dimension (for PPM conversion).
f2_reference_frequency_hz
property
writable
#
Reference frequency for F2 dimension (for PPM conversion).
_init_history
#
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 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 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
|
__getitem__
#
Slice-only 2D indexing. Example: spec[:, 10:50] (Indexing with ints that reduce dimensionality is not supported here.)
normalize
#
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
#
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
#
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 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 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.
apply_reference_correction
#
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 |
_setup_from_arrays
#
_setup_from_arrays(
f1: FloatArray,
f2: FloatArray,
amplitudes: ndarray,
f1_unit: FrequencyUnit,
f2_unit: FrequencyUnit,
) -> None
_baseline_plane
#
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
#
Override to sync reference spectra before serialization.
_baseline_edge_1d
staticmethod
#
Simple DC offset correction using edge regions.
_baseline_poly_1d
staticmethod
#
Polynomial baseline fit to edge regions.
_baseline_als_1d
staticmethod
#
Asymmetric least squares baseline correction for 1D array.