Skip to content

API Reference

deer_trace(distances, time, modulation_depth=0.3, background_decay=0.05)

Simulate a DEER (Double Electron-Electron Resonance) time-domain trace.

Parameters:

Name Type Description Default
distances ndarray

(M,) distribution of distances.

required
time ndarray

(T,) time points in microseconds.

required
modulation_depth float

Modulation depth λ.

0.3
background_decay float

Decay rate of the background signal.

0.05

Returns:

Type Description
ndarray

V(t) normalized DEER signal.

Source code in diff_epr/kernels.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def deer_trace(
    distances: jnp.ndarray,
    time: jnp.ndarray,
    modulation_depth: float = 0.3,
    background_decay: float = 0.05,
) -> jnp.ndarray:
    """
    Simulate a DEER (Double Electron-Electron Resonance) time-domain trace.

    Args:
        distances: (M,) distribution of distances.
        time: (T,) time points in microseconds.
        modulation_depth: Modulation depth λ.
        background_decay: Decay rate of the background signal.

    Returns:
        V(t) normalized DEER signal.
    """
    # Dipolar coupling frequency: ω = 2π * ν_dd
    # ν_dd = 52040 / r^3 (MHz for Angstroms)
    nu_dd = 52040.0 / (distances**3)
    omega = 2.0 * jnp.pi * nu_dd

    # Kernel matrix (T, M)
    # V(t) = 1 - λ(1 - cos(ωt))
    kernel = 1.0 - modulation_depth * (1.0 - jnp.cos(omega[None, :] * time[:, None]))

    # Integrate over distance distribution (assumed uniform weight here for simplicity)
    signal = jnp.mean(kernel, axis=-1)

    # Background decay
    background = jnp.exp(-background_decay * time)

    return signal * background

deer_trace_oriented(distance, relative_orientation, time, modulation_depth=0.3)

Simulate an oriented (single-crystal) DEER trace for a rigid spin pair using the Polyhach et al. (2007) 5-angle geometric model.

In this model the five angles are

theta_r1, phi_r1 : orientation of the interspin vector r in the g-tensor principal-axis frame of spin label 1. alpha, beta, gamma: ZYZ Euler angles rotating the g-tensor frame of label 1 into that of label 2.

For a single-crystal experiment the external field B₀ is fixed along the z-axis of the lab frame. The dipolar coupling frequency depends on the angle between r and B₀, which is theta_r1 when the molecule is oriented so that spin-1's g-frame z-axis is aligned with B₀.

For full orientation-selection (selecting a sub-set of B₀ orientations via the observer pulse bandwidth) the four remaining angles (phi_r1, alpha, beta, gamma) are needed; that extension is deferred to a future version.

Parameters:

Name Type Description Default
distance float

Inter-spin distance r in Angstroms.

required
relative_orientation ndarray

(5,) angles (theta_r1, phi_r1, alpha, beta, gamma) in radians. Only theta_r1 is used in the current implementation.

required
time ndarray

(T,) time points in microseconds.

required
modulation_depth float

Modulation depth λ.

0.3

Returns:

Type Description
ndarray

V(t) normalised DEER signal of shape (T,).

Source code in diff_epr/kernels.py
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
154
155
156
157
158
159
160
161
162
163
164
165
166
def deer_trace_oriented(
    distance: float,
    relative_orientation: jnp.ndarray,
    time: jnp.ndarray,
    modulation_depth: float = 0.3,
) -> jnp.ndarray:
    """
    Simulate an oriented (single-crystal) DEER trace for a rigid spin pair
    using the Polyhach et al. (2007) 5-angle geometric model.

    In this model the five angles are:
        theta_r1, phi_r1 : orientation of the interspin vector **r** in the
                           g-tensor principal-axis frame of spin label 1.
        alpha, beta, gamma: ZYZ Euler angles rotating the g-tensor frame of
                            label 1 into that of label 2.

    For a **single-crystal** experiment the external field B₀ is fixed along
    the z-axis of the lab frame.  The dipolar coupling frequency depends on
    the angle between **r** and B₀, which is theta_r1 when the molecule is
    oriented so that spin-1's g-frame z-axis is aligned with B₀.

    For full orientation-selection (selecting a sub-set of B₀ orientations via
    the observer pulse bandwidth) the four remaining angles (phi_r1, alpha,
    beta, gamma) are needed; that extension is deferred to a future version.

    Args:
        distance: Inter-spin distance r in Angstroms.
        relative_orientation: (5,) angles (theta_r1, phi_r1, alpha, beta, gamma)
            in radians.  Only theta_r1 is used in the current implementation.
        time: (T,) time points in microseconds.
        modulation_depth: Modulation depth λ.

    Returns:
        V(t) normalised DEER signal of shape (T,).
    """
    # Dipolar frequency constant C = 52040 MHz·Å³
    nu_max = 52040.0 / (distance**3)

    # theta_r1: polar angle of the interspin vector relative to B₀ in the
    # single-crystal (oriented) frame.
    theta_r1 = relative_orientation[0]

    # Dipolar coupling frequency at this fixed orientation.
    # ν_dd = ν_max · (1 − 3·cos²θ)  — changes sign through the magic angle.
    nu_dd = nu_max * (1.0 - 3.0 * jnp.cos(theta_r1) ** 2)
    omega = 2.0 * jnp.pi * nu_dd

    # Single-crystal DEER trace (no powder average over θ).
    return 1.0 - modulation_depth * (1.0 - jnp.cos(omega * time))

deer_trace_rotamers(rotamers1, weights1, rotamers2, weights2, time, modulation_depth=0.3, background_decay=0.05)

Simulate a DEER trace using weighted rotamer libraries for both labels.

Parameters:

Name Type Description Default
rotamers1 ndarray

(N1, 3) coordinates of label 1 rotamers.

required
weights1 ndarray

(N1,) weights for label 1 rotamers (sum to 1).

required
rotamers2 ndarray

(N2, 3) coordinates of label 2 rotamers.

required
weights2 ndarray

(N2,) weights for label 2 rotamers (sum to 1).

required
time ndarray

(T,) time points.

required
modulation_depth float

Modulation depth λ.

0.3
background_decay float

Background decay rate.

0.05

Returns:

Type Description
ndarray

V(t) normalized DEER signal.

Source code in diff_epr/kernels.py
 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
def deer_trace_rotamers(
    rotamers1: jnp.ndarray,
    weights1: jnp.ndarray,
    rotamers2: jnp.ndarray,
    weights2: jnp.ndarray,
    time: jnp.ndarray,
    modulation_depth: float = 0.3,
    background_decay: float = 0.05,
) -> jnp.ndarray:
    """
    Simulate a DEER trace using weighted rotamer libraries for both labels.

    Args:
        rotamers1: (N1, 3) coordinates of label 1 rotamers.
        weights1: (N1,) weights for label 1 rotamers (sum to 1).
        rotamers2: (N2, 3) coordinates of label 2 rotamers.
        weights2: (N2,) weights for label 2 rotamers (sum to 1).
        time: (T,) time points.
        modulation_depth: Modulation depth λ.
        background_decay: Background decay rate.

    Returns:
        V(t) normalized DEER signal.
    """
    # 1. Compute all pairwise distances (N1, N2)
    diff = rotamers1[:, None, :] - rotamers2[None, :, :]
    dist = jnp.sqrt(jnp.sum(diff**2, axis=-1) + 1e-9)

    # 2. Compute pairwise weights (N1, N2)
    p_ij = (
        weights1[:, None]
        * weights2[
            None,
            :,
        ]
    )

    # 3. Simulate DEER kernel for each distance
    # Reshape distances to 1D for deer_trace (N1*N2,)
    dist_flat = dist.flatten()
    p_flat = p_ij.flatten()

    # Dipolar frequencies
    nu_dd = 52040.0 / (dist_flat**3)
    omega = 2.0 * jnp.pi * nu_dd

    # Kernel matrix (T, N_pairs)
    kernel = 1.0 - modulation_depth * (1.0 - jnp.cos(omega[None, :] * time[:, None]))

    # 4. Weighted average over rotamer pairs
    signal = jnp.sum(kernel * p_flat[None, :], axis=-1)

    # Background decay
    background = jnp.exp(-background_decay * time)

    return signal * background

spin_distance(coords1, coords2)

Compute distance between spin centers.

Parameters:

Name Type Description Default
coords1 ndarray

(N, 3) coordinates of first spin label.

required
coords2 ndarray

(N, 3) coordinates of second spin label.

required

Returns:

Type Description
ndarray

Distances (N,).

Source code in diff_epr/kernels.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def spin_distance(
    coords1: jnp.ndarray,
    coords2: jnp.ndarray,
) -> jnp.ndarray:
    """
    Compute distance between spin centers.

    Args:
        coords1: (N, 3) coordinates of first spin label.
        coords2: (N, 3) coordinates of second spin label.

    Returns:
        Distances (N,).
    """
    dist_sq = jnp.sum((coords1 - coords2) ** 2, axis=-1)
    # Safe distance for gradients (avoids NaN at dist=0)
    dist = jnp.sqrt(jnp.where(dist_sq > 0, dist_sq, 1.0))
    return jnp.where(dist_sq > 0, dist, 0.0)