Skip to content

Scalar J-Couplings API

j_coupling

Scalar Coupling (J-coupling) calculations.

Functions

calculate_hn_ha_coupling_from_phi(phi_degrees)

Calculate the 3J(HN-HA) coupling constant from Phi angles in degrees.

EDUCATIONAL NOTE - The Karplus Equation
========================================
The magnitude of the 3-bond coupling (^3J) depends heavily on the torsion
angle between the atoms. The physical origin lies in the Fermi contact
interaction, where electron spins transmit magnetic information between nuclei.
Formula: ^3J = A * cos^2(theta) + B * cos(theta) + C
For HN-HA coupling, the relationship theta = phi - 60 is a geometric
consequence of how the phi angle and the Karplus angle are defined.
- Phi (C'-N-Ca-C') measures the rotation around the N-Ca bond.
- Theta for 3J(HN-HA) is the dihedral H-N-Ca-HA.
Alpha Helix (Phi ~ -60): theta ~ -120 -> J is small (~4 Hz)
Beta Sheet (Phi ~ -120): theta ~ -180 -> J is large (~9 Hz)

Parameters:

Name Type Description Default
phi_degrees ndarray

Backbone Phi angle in degrees (NumPy array).

required

Returns:

Type Description
ndarray

Predicted J-coupling in Hz (NumPy array).

Source code in synth_nmr/j_coupling.py
def calculate_hn_ha_coupling_from_phi(phi_degrees: np.ndarray) -> np.ndarray:
    """
    Calculate the 3J(HN-HA) coupling constant from Phi angles in degrees.

    # EDUCATIONAL NOTE - The Karplus Equation
    # ========================================
    # The magnitude of the 3-bond coupling (^3J) depends heavily on the torsion
    # angle between the atoms. The physical origin lies in the Fermi contact
    # interaction, where electron spins transmit magnetic information between nuclei.
    #
    # Formula: ^3J = A * cos^2(theta) + B * cos(theta) + C
    #
    # For HN-HA coupling, the relationship theta = phi - 60 is a geometric
    # consequence of how the phi angle and the Karplus angle are defined.
    # - Phi (C'-N-Ca-C') measures the rotation around the N-Ca bond.
    # - Theta for 3J(HN-HA) is the dihedral H-N-Ca-HA.
    #
    # Alpha Helix (Phi ~ -60): theta ~ -120 -> J is small (~4 Hz)
    # Beta Sheet (Phi ~ -120): theta ~ -180 -> J is large (~9 Hz)

    Args:
        phi_degrees: Backbone Phi angle in degrees (NumPy array).

    Returns:
        Predicted J-coupling in Hz (NumPy array).
    """
    # Theta = Phi - 60 degrees
    theta_rad = np.radians(phi_degrees - 60.0)

    cos_theta = np.cos(theta_rad)
    j_vals = (
        (KARPLUS_PARAMS["A"] * (cos_theta**2))
        + (KARPLUS_PARAMS["B"] * cos_theta)
        + KARPLUS_PARAMS["C"]
    )
    return j_vals

predict_couplings_from_phi_map(phi_map)

Predict HN-HA couplings for a set of residues from a Phi map.

Parameters:

Name Type Description Default
phi_map Dict[int, float]

Dictionary mapping Residue ID -> Phi angle (degrees).

required

Returns:

Type Description
Dict[int, float]

Dictionary mapping Residue ID -> J-coupling (Hz).

Source code in synth_nmr/j_coupling.py
def predict_couplings_from_phi_map(phi_map: Dict[int, float]) -> Dict[int, float]:
    """
    Predict HN-HA couplings for a set of residues from a Phi map.

    Args:
        phi_map: Dictionary mapping Residue ID -> Phi angle (degrees).

    Returns:
        Dictionary mapping Residue ID -> J-coupling (Hz).
    """
    res_ids = np.array(list(phi_map.keys()))
    phi_vals = np.array(list(phi_map.values()))
    j_vals = calculate_hn_ha_coupling_from_phi(phi_vals)
    return {int(rid): float(round(j, 2)) for rid, j in zip(res_ids, j_vals) if not np.isnan(j)}

calculate_hn_ha_coupling(structure)

Calculate 3J_HNHa coupling constants for the protein backbone.

── J-Coupling Physics and Structural Biology ────────────────────────
The 3J_HNHa coupling constant is a measure of the scalar (through-bond)
magnetic interaction between the amide proton (HN) and the alpha
proton (Ha). This interaction is mediated by the intervening bonds
(HN-N, N-CA, CA-Ha).
Crucially, the magnitude of this coupling depends on the dihedral
angle Phi (φ) according to the Karplus equation:
J(φ) = A cos²(θ) + B cos(θ) + C
Scalar couplings are mediated by electrons in the chemical bonds.
The interaction is sensitive to the overlap of the electron wavefunctions,
which in turn is a function of the dihedral angle. In structural biology,
J-couplings are one of the most direct ways to measure torsion angles.
Proteins are dynamic, and the measured J-coupling is actually a
time-average over the conformational ensemble. Here we predict the
value based on a single static structure or a single frame.
Large 3J_HNHa values (~8-10 Hz) typically indicate beta-sheet regions,
where the Phi angle is around -120 to -150 degrees.
Small 3J_HNHa values (~3-5 Hz) indicate alpha-helical regions,
where the Phi angle is around -60 degrees.
─────────────────────────────────────────────────────────────────────

Parameters:

Name Type Description Default
structure AtomArray

biotite.structure.AtomArray containing the protein.

required

Returns:

Type Description
Dict[str, Dict[int, float]]

Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).

Source code in synth_nmr/j_coupling.py
def calculate_hn_ha_coupling(structure: struc.AtomArray) -> Dict[str, Dict[int, float]]:
    """
    Calculate 3J_HNHa coupling constants for the protein backbone.

    # ── J-Coupling Physics and Structural Biology ────────────────────────
    # The 3J_HNHa coupling constant is a measure of the scalar (through-bond)
    # magnetic interaction between the amide proton (HN) and the alpha
    # proton (Ha). This interaction is mediated by the intervening bonds
    # (HN-N, N-CA, CA-Ha).
    #
    # Crucially, the magnitude of this coupling depends on the dihedral
    # angle Phi (φ) according to the Karplus equation:
    #   J(φ) = A cos²(θ) + B cos(θ) + C
    # where θ is the H-N-C-H dihedral angle (related to Phi).
    #
    # Scalar couplings are mediated by electrons in the chemical bonds.
    # The interaction is sensitive to the overlap of the electron wavefunctions,
    # which in turn is a function of the dihedral angle. In structural biology,
    # J-couplings are one of the most direct ways to measure torsion angles.
    #
    # Proteins are dynamic, and the measured J-coupling is actually a
    # time-average over the conformational ensemble. Here we predict the
    # value based on a single static structure or a single frame.
    #
    # Large 3J_HNHa values (~8-10 Hz) typically indicate beta-sheet regions,
    # where the Phi angle is around -120 to -150 degrees.
    # Small 3J_HNHa values (~3-5 Hz) indicate alpha-helical regions,
    # where the Phi angle is around -60 degrees.
    # ─────────────────────────────────────────────────────────────────────

    Args:
        structure: biotite.structure.AtomArray containing the protein.

    Returns:
        Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).
    """
    from synth_nmr.structure_utils import get_residue_info

    logger.info("Calculating 3J_HNHa scalar couplings...")
    # Filter for amino acids to avoid mismatches with HETATMs (waters/ions)
    protein_mask = struc.filter_amino_acids(structure)
    structure = structure[protein_mask]
    if structure.array_length() == 0:
        return {}

    # Calculate backbone dihedrals
    phi, _, _ = struc.dihedral_backbone(structure)

    # Get residue info using unified utility
    chain_ids, res_ids, _, _ = get_residue_info(structure)

    if len(phi) != len(res_ids):
        logger.warning(
            f"Mismatch in backbone angles count ({len(phi)}) vs residue count ({len(res_ids)})."
        )
        return {}

    # Vectorized Karplus calculation
    phi_deg = np.degrees(phi)
    j_vals = calculate_hn_ha_coupling_from_phi(phi_deg)

    # Build results dictionary
    results: Dict[str, Dict[int, float]] = {}
    for chain_id, res_id, j_val in zip(chain_ids, res_ids, j_vals):
        if np.isnan(j_val):
            continue

        if chain_id not in results:
            results[chain_id] = {}
        results[chain_id][int(res_id)] = float(round(j_val, 2))

    return results

calculate_ha_hb_coupling(structure)

Calculate 3J_HaHb coupling constants dependent on the chi1 angle.

Parameters:

Name Type Description Default
structure AtomArray

biotite.structure.AtomArray containing the protein.

required

Returns:

Type Description
Dict[str, Dict[int, float]]

Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).

Source code in synth_nmr/j_coupling.py
def calculate_ha_hb_coupling(structure: struc.AtomArray) -> Dict[str, Dict[int, float]]:
    """
    Calculate 3J_HaHb coupling constants dependent on the chi1 angle.

    Args:
        structure: biotite.structure.AtomArray containing the protein.

    Returns:
        Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).
    """
    logger.info("Calculating 3J_HaHb side-chain couplings...")
    chi1_angles = _get_chi1_angles(structure)
    results: Dict[str, Dict[int, float]] = {}

    for chain_id, residues in chi1_angles.items():
        results[chain_id] = {}
        for res_id, chi1_rad in residues.items():
            # Phase shifts depend exactly on pro-R/pro-S proton alignments,
            # but we use a simplified idealized theta relation for this implementation.
            theta = float(chi1_rad - np.deg2rad(120.0))

            cos_theta = float(np.cos(theta))
            j_val = float(
                (KARPLUS_HA_HB_PARAMS["A"] * (cos_theta**2))
                + (KARPLUS_HA_HB_PARAMS["B"] * cos_theta)
                + KARPLUS_HA_HB_PARAMS["C"]
            )
            results[chain_id][res_id] = float(round(j_val, 2))

    return results

calculate_c_cg_coupling(structure)

Calculate 3J(C', Cg) coupling constants dependent on the chi1 angle.

Parameters:

Name Type Description Default
structure AtomArray

biotite.structure.AtomArray containing the protein.

required

Returns:

Type Description
Dict[str, Dict[int, float]]

Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).

Source code in synth_nmr/j_coupling.py
def calculate_c_cg_coupling(structure: struc.AtomArray) -> Dict[str, Dict[int, float]]:
    """
    Calculate 3J(C', Cg) coupling constants dependent on the chi1 angle.

    Args:
        structure: biotite.structure.AtomArray containing the protein.

    Returns:
        Dict keyed by Chain ID -> Residue ID -> J-coupling value (Hz).
    """
    logger.info("Calculating 3J_C'Cg side-chain couplings...")
    chi1_angles = _get_chi1_angles(structure)
    results: Dict[str, Dict[int, float]] = {}

    for chain_id, residues in chi1_angles.items():
        results[chain_id] = {}
        for res_id, chi1_rad in residues.items():
            # The dihedral angle pathway for C' - CA - CB - CG intersects chi1
            theta = float(chi1_rad)

            cos_theta = float(np.cos(theta))
            j_val = float(
                (KARPLUS_C_CG_PARAMS["A"] * (cos_theta**2))
                + (KARPLUS_C_CG_PARAMS["B"] * cos_theta)
                + KARPLUS_C_CG_PARAMS["C"]
            )
            results[chain_id][res_id] = float(round(j_val, 2))

    return results