IMP logo
IMP Reference Guide  develop.6f18bfa751,2025/09/20
The Integrative Modeling Platform
kappa2.py
1 """
2 
3 """
4 import numba as nb
5 import numpy as np
6 import typing
7 
8 
9 def kappasq_dwt(
10  sD2: float,
11  sA2: float,
12  fret_efficiency: float,
13  n_samples: int = 10000,
14  n_bins: int = 31,
15  k2_min: float = 0.0,
16  k2_max: float = 4.0
17 ):
18  """
19 
20  Parameters
21  ----------
22  sD2 : float
23  Second rank order parameter S2 of the donor dye. This can correspond
24  to the fraction of trapped donor dye.
25  sA2 : float
26  Second rank order parameter S2 of the acceptor dye. This can correspond
27  to the fraction of trapped acceptor dye.
28  fret_efficiency : float
29  FRET efficiency
30  n_bins : int
31  The number of bins in the kappa2 distribution that is
32  generated.
33  k2_max : float
34  Upper kappa2 bound in the generated histogram
35  k2_min : float
36  Lower kappa2 bound in the generate histogram
37  n_samples : int
38  The number random vector pairs that are drawn (default: 10000)
39 
40  Returns
41  -------
42  """
43 
44  # Binning of the kappa2 distribution
45  k2_step = (k2_max - k2_min) / (n_bins - 1)
46  k2_scale = np.arange(k2_min, k2_max + 1e-14, k2_step, dtype=np.float64)
47 
48  # Generate random orientations for the TDM vectors
49  donor_vec = np.random.randn(n_samples, 3)
50  acceptor_vec = np.random.randn(n_samples, 3)
51 
52  # x = (R_DA/R_0)^6; relative DA distance
53  x = 1 / fret_efficiency - 1
54 
55  k2s = np.zeros(n_samples, dtype=np.float64)
56  for i in range(n_samples):
57  donor = donor_vec[i]
58  acceptor = acceptor_vec[i]
59  # Assumption here: connecting vector R_DA is along
60  # the x-axis (R_DA=[1,0,0])
61  delta = np.arccos(np.dot(donor, acceptor)
62  / (np.linalg.norm(donor) * np.linalg.norm(acceptor)))
63  beta1 = np.arccos(donor[0] / np.linalg.norm(donor))
64  beta2 = np.arccos(acceptor[0] / np.linalg.norm(acceptor))
65 
66  k2_trapped_free = kappasq(
67  delta=delta,
68  sD2=1,
69  sA2=0,
70  beta1=beta1,
71  beta2=beta2
72  )
73  k2_free_trapped = kappasq(
74  delta=delta,
75  sD2=0,
76  sA2=1,
77  beta1=beta1,
78  beta2=beta2
79  )
80  k2_trapped_trapped = kappasq(
81  delta=delta,
82  sD2=1,
83  sA2=1,
84  beta1=beta1,
85  beta2=beta2
86  )
87  ##
88  Ek2 = ((1 - sD2) * (1 - sA2) / (1 + x)
89  + sD2 * sA2 / (1 + 2 / 3. / k2_trapped_trapped * x)
90  + sD2 * (1 - sA2) / (1 + 2 / 3. / k2_trapped_free * x)
91  + (1 - sD2) * sA2 / (1 + 2 / 3. / k2_free_trapped * x))
92  k2 = 2 / 3. * x / (1 / Ek2 - 1)
93  k2s[i] = k2
94 
95  # create the histogram
96  k2hist, bins = np.histogram(k2s, k2_scale)
97  return k2_scale, k2hist, k2s
98 
99 
100 def kappasq_all_delta_new(
101  delta: float,
102  sD2: float,
103  sA2: float,
104  step: float = 0.25,
105  n_bins: int = 31,
106  k2_min: float = 0.0,
107  k2_max: float = 4.0
108 ) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray]:
109  ks = list()
110  weights = list()
111  for beta1 in np.arange(0, np.pi/2, step=step / 180. * np.pi):
112  weight_beta1 = np.sin(beta1)
113  for beta2 in np.arange(
114  start=abs(delta - beta1),
115  stop=min(delta + beta1, np.pi / 2.),
116  step=step / 180. * np.pi
117  ):
118  weight_beta2 = np.sin(beta1)
119  weights.append(
120  weight_beta1 * weight_beta2
121  )
122  ks.append(
123  kappasq(
124  sD2=sD2,
125  sA2=sA2,
126  delta=delta,
127  beta1=beta1,
128  beta2=beta2
129  )
130  )
131  ks = np.array(ks)
132 
133  # histogram bin edges
134  k2_step = (k2_max - k2_min) / (n_bins - 1)
135  k2scale = np.arange(k2_min, k2_max + 1e-14, k2_step, dtype=np.float64)
136 
137  k2hist, x = np.histogram(ks, bins=k2scale, weights=weights)
138  return k2scale, k2hist, ks
139 
140 
141 @nb.jit(nopython=True)
142 def kappasq_all_delta(
143  delta: float,
144  sD2: float,
145  sA2: float,
146  step: float = 0.25,
147  n_bins: int = 31,
148  k2_min: float = 0.0,
149  k2_max: float = 4.0
150 ) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray]:
151  """Computes a orientation factor distribution for a wobbling in
152  a cone model using parameters that can be estimated by experimental
153  anisotropies.
154 
155  The function used second rank order parameter of the donor and acceptor
156  and the angle delta between the symmetry axes of the dyes as input. These
157  parameters can be estimated by the residual anisotropy the the dyes. The
158  second rank order parameter of the donor and acceptor are estimated by the
159  dye's residual anisotropies. The angle between the symmetry axes is
160  estimated by the residual anisotropy of the FRET sensitized emission
161  (see: `chisurf.fluorescence.anisotropy.kappa2.s2delta`).
162 
163  This function computes a orientation factor distribution,
164  :math:`p(/kappa^2)`, for a wobbling in a cone model (WIC) for second rank
165  structure factors of the donor and acceptor, and an angle :math:`delta`.
166  The angle :math:`delta` is the angle between the symmetry axes of the
167  dyes and can be estimated using experimental residual anisotropies [1]_.
168 
169  Parameters
170  ----------
171  delta : float
172  The angle delta (in rad) for which the WIC orientation factor
173  distribution is calculated.
174  sD2 : float
175  Second rank order parameter S2 of the donor dye. This can correspond
176  to the fraction of trapped donor dye.
177  sA2 : float
178  Second rank order parameter S2 of the acceptor dye. This can correspond
179  to the fraction of trapped acceptor dye.
180  step : float
181  The step size in degrees that is used to sample the
182  angles.
183  n_bins : int
184  The number of bins in the kappa2 distribution that is
185  generated.
186  k2_max : float
187  Upper kappa2 bound in the generated histogram
188  k2_min : float
189  Lower kappa2 bound in the generate histogram
190 
191  Returns
192  -------
193  k2scale : numpy-array
194  A linear scale in the range of [0, 4] with *n_bins* elements
195  k2hist : numpy-array
196  The histogram of kappa2 values
197  k2 : numpy-array
198  A numpy-array containing all computed kappa2 values. The histogram
199  corresponds to a histogram over all returned kappa2 values.
200 
201  Examples
202  --------
203  >>> from scikit_fluorescence.modeling.kappa2 import kappasq_all_delta
204  >>> k2s, k2h, k2v = kappasq_all_delta(
205  ... delta=0.2,
206  ... sD2=0.15,
207  ... sA2=0.25,
208  ... step=2.0,
209  ... n_bins=31
210  ... )
211  >>> np.allclose(k2h, np.array(
212  ... [ 0. , 0. , 0. , 0. ,
213  ... 3205.72877776, 1001.19048825, 611.44917432, 252.97166906,
214  ... 0. , 0. , 0. , 0. ,
215  ... 0. , 0. , 0. , 0. ,
216  ... 0. , 0. , 0. , 0. ,
217  ... 0. , 0. , 0. , 0. ,
218  ... 0. , 0. , 0. , 0. ,
219  ... 0. , 0. ]), rtol=0.3, atol=2.0)
220  True
221 
222  Notes
223  -----
224  The angle :math:`/beta_1` is varied in the range (0,pi/2)
225  The angle :math:`/phi` is varied in the range (0, 2 pi)
226 
227  References
228  ----------
229  .. [1] Simon Sindbert, Stanislav Kalinin, Hien Nguyen, Andrea Kienzler,
230  Lilia Clima, Willi Bannwarth, Bettina Appel, Sabine Mueller, Claus A. M.
231  Seidel, "Accurate Distance Determination of Nucleic Acids via Foerster
232  Resonance Energy Transfer: Implications of Dye Linker Length and Rigidity"
233  vol. 133, pp. 2463-2480, J. Am. Chem. Soc., 2011
234 
235  """
236  # beta angles
237  beta1 = np.arange(0.001, np.pi / 2.0, step * np.pi / 180.0,
238  dtype=np.float64)
239  phi = np.arange(0.001, 2.0 * np.pi, step * np.pi / 180.0, dtype=np.float64)
240  n = beta1.shape[0]
241  m = phi.shape[0]
242  rda_vec = np.array([1, 0, 0], dtype=np.float64)
243 
244  # kappa-square values for allowed betas
245  k2 = np.zeros((n, m), dtype=np.float64)
246  k2hist = np.zeros(n_bins - 1, dtype=np.float64)
247 
248  # histogram bin edges
249  k2_step = (k2_max - k2_min) / (n_bins - 1)
250  k2scale = np.arange(k2_min, k2_max + 1e-14, k2_step, dtype=np.float64)
251  for i in range(n):
252  d1 = np.array([np.cos(beta1[i]), 0, np.sin(beta1[i])])
253  n1 = np.array([-np.sin(beta1[i]), 0, np.cos(beta1[i])])
254  n2 = np.array([0, 1, 0])
255  for j in range(m):
256  d2 = ((n1 * np.cos(phi[j]) + n2 * np.sin(phi[j]))
257  * np.sin(delta) + d1 * np.cos(delta))
258  beta2 = np.arccos(np.abs(d2.dot(rda_vec)))
259  k2[i, j] = kappasq(
260  delta=delta,
261  sD2=sD2,
262  sA2=sA2,
263  beta1=beta1[i],
264  beta2=beta2
265  )
266  y, x = np.histogram(k2[i, :], bins=k2scale)
267  k2hist += y*np.sin(beta1[i])
268  return k2scale, k2hist, k2
269 
270 
271 @nb.jit(nopython=True)
272 def kappasq_all(
273  sD2: float,
274  sA2: float,
275  n_bins: int = 81,
276  k2_min: float = 0.0,
277  k2_max: float = 4.0,
278  n_samples: int = 10000
279 ) -> typing.Tuple[np.array, np.array, np.array]:
280  """Computes a orientation factor distribution for a wobbling in a
281  cone model using specific second rank structure factors of the donor
282  and acceptor.
283 
284  This function computes a orientation factor distribution,
285  :math:`p(/kappa^2)`, for a wobbling in a cone model (WIC) for second rank
286  structure factors of the donor and acceptor estimated using experimental
287  residual anisotropies [1]_.
288 
289  Parameters
290  ----------
291  sD2 : float
292  Second rank order parameter S2 of the donor dye
293  sA2 : float
294  Second rank order parameter S2 of the acceptor dye
295  n_bins : int
296  The number of bins in the kappa2 histogram that is
297  generated.
298  k2_max : float
299  Upper kappa2 bound in the generated histogram
300  k2_min : float
301  Lower kappa2 bound in the generate histogram
302  n_samples : int
303  The number random vector pairs that are drawn (default: 10000)
304 
305  Returns
306  -------
307  k2scale : numpy-array
308  A linear scale in the range of [0, 4] with *n_bins* elements
309  k2hist : numpy-array
310  The histogram of kappa2 values
311  k2 : numpy-array
312  A numpy-array containing all computed kappa2 values. The histogram
313  corresponds to a histogram over all returned kappa2 values.
314 
315  Examples
316  --------
317  >>> from scikit_fluorescence.modeling.kappa2 import kappasq_all_delta
318  >>> k2_scale, k2_hist, k2 = kappasq_all(
319  ... sD2=0.3,
320  ... sA2=0.5,
321  ... n_bins=31,
322  ... n_samples=100000
323  ... )
324  >>> k2_scale
325  array([0. , 0.13333333, 0.26666667, 0.4 , 0.53333333,
326  0.66666667, 0.8 , 0.93333333, 1.06666667, 1.2 ,
327  1.33333333, 1.46666667, 1.6 , 1.73333333, 1.86666667,
328  2. , 2.13333333, 2.26666667, 2.4 , 2.53333333,
329  2.66666667, 2.8 , 2.93333333, 3.06666667, 3.2 ,
330  3.33333333, 3.46666667, 3.6 , 3.73333333, 3.86666667,
331  4. ])
332  >>> reference = np.array(
333  ... [0.0000e+00, 0.0000e+00, 0.0000e+00, 3.1920e+04, 4.3248e+04,
334  ... 1.4842e+04, 5.8930e+03, 2.5190e+03, 1.0840e+03, 3.9700e+02,
335  ... 9.4000e+01, 3.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
336  ... 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
337  ... 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
338  ... 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00])
339  >>> np.allclose(reference, k2_hist, rtol=0.3, atol=2.0)
340  True
341 
342  References
343  ----------
344  .. [1] Simon Sindbert, Stanislav Kalinin, Hien Nguyen, Andrea Kienzler,
345  Lilia Clima, Willi Bannwarth, Bettina Appel, Sabine Mueller, Claus A. M.
346  Seidel, "Accurate Distance Determination of Nucleic Acids via Foerster
347  Resonance Energy Transfer: Implications of Dye Linker Length and Rigidity"
348  vol. 133, pp. 2463-2480, J. Am. Chem. Soc., 2011
349 
350  """
351  k2 = np.zeros(n_samples, dtype=np.float64)
352  step = (k2_max - k2_min) / (n_bins - 1)
353  k2scale = np.arange(k2_min, k2_max + 1e-14, step, dtype=np.float64)
354  k2hist = np.zeros(k2scale.shape[0] - 1, dtype=np.float64)
355  for i in range(n_samples):
356  d1 = np.random.random(3)
357  d2 = np.random.random(3)
358  n1 = np.linalg.norm(d1)
359  n2 = np.linalg.norm(d2)
360  # Assumption here: connecting vector R_DA is along the
361  # x-axis (R_DA=[1,0,0])
362  delta = np.arccos(np.dot(d1, d2) / (n1 * n2))
363  beta1 = np.arccos(d1[0] / n1)
364  beta2 = np.arccos(d2[0] / n2)
365  k2[i] = kappasq(
366  delta=delta,
367  sD2=sD2,
368  sA2=sA2,
369  beta1=beta1,
370  beta2=beta2
371  )
372  y, x = np.histogram(k2, bins=k2scale)
373  k2hist += y
374  return k2scale, k2hist, k2
375 
376 
377 @nb.jit(nopython=True)
378 def kappa_distance(
379  d1: np.array,
380  d2: np.array,
381  a1: np.array,
382  a2: np.array
383 ) -> typing.Tuple[float, float]:
384  r"""Calculates the distance between the center of two dipoles and the
385  orientation-factor kappa of the dipoles
386 
387  Calculates for the vectors d1 and d2 pointing to the donors and the vectors
388  a1 and a2 pointing to the ends of the acceptor dipole the orientation
389  factor kappa.
390 
391  Parameters
392  ----------
393  d1 : numpy-array
394  Vector pointing to the first point of the dipole D
395  d2 : numpy-array
396  Vector pointing to the second point of the dipole D
397  a1 : numpy-array
398  Vector pointing to the first point of the dipole A
399  a2 : numpy-array
400  Vector pointing to the second point of the dipole A
401 
402  Returns
403  -------
404  tuple
405  distance between the center of the dipoles and the orientation factor
406  for the two dipoles kappa
407 
408  Notes
409  -----
410  The four vectors defining the dipole of the donor :math:`\vec{r}_{D1}` and
411  :math:`\vec{r}_{D2}` specified by the parameters `d1` and `d2` and
412  :math:`\vec{r}_{A1}` and :math:`\vec{r}_{A2}` specified by the parameters
413  `a1` and `a1` are used to compute orientation factor :math:`kappa^2`
414  and the distance between the center of the two dipoles :math:`R_{DA}`.
415 
416  The distance :math:`R_{DA}` between the dipole centers and :math:`kappa`
417  is calculated as follows:
418 
419  ..math::
420 
421  R_{D,21}=|\vec{r}_{D2} - \vec{r}_{D1}| \
422  R_{A,21}=|\vec{r}_{A2} - \vec{r}_{A1}| \
423  \hat{\mu}_{D}=1/R_{D,21} \cdot (\vec{r}_{D2}-\vec{r}_{D1}) \
424  \hat{\mu}_{A}=1/R_{A,21} \cdot (\vec{r}_{A2}-\vec{r}_{A1}) \
425  \vec{m}_{D}=\vec{r}_{D1}+1/2 \cdot \hat{\mu}_{D} \
426  \vec{m}_{A}=\vec{r}_{A1}+1/2 \cdot \hat{\mu}_{A} \
427  \vec{r}_{DA}=\vec{m}_{D}-\vec{m}_{A} \
428  R_{DA}=|\vec{m}_{D}-\vec{m}_{A}| \
429  \hat{\mu}_{DA}=\vec{r}_{DA} / R_{DA} \
430  \kappa=\langle\mu_A,\mu_D\rangle-3\cdot\langle\mu_D,\mu_{DA}\rangle \cdot \langle\mu_A,\mu_{DA}\rangle
431 
432  Examples
433  --------
434  >>> import scikit_fluorescence.modeling.kappa2
435  >>> donor_dipole = np.array(
436  ... [
437  ... [0.0, 0.0, 0.0],
438  ... [1.0, 0.0, 0.0]
439  ... ], dtype=np.float64
440  ... )
441  >>> acceptor_dipole = np.array(
442  ... [
443  ... [0.0, 0.5, 0.0],
444  ... [0.0, 0.5, 1.0]
445  ... ], dtype=np.float64
446  ... )
447  >>> scikit_fluorescence.modeling.kappa2.kappa(
448  ... donor_dipole,
449  ... acceptor_dipole
450  ... )
451  (0.8660254037844386, 1.0000000000000002)
452 
453  """ # noqa: E501
454  # coordinates of the dipole
455  d11 = d1[0]
456  d12 = d1[1]
457  d13 = d1[2]
458 
459  d21 = d2[0]
460  d22 = d2[1]
461  d23 = d2[2]
462 
463  # distance between the two end points of the donor
464  dD21 = np.sqrt(
465  (d11 - d21) * (d11 - d21) +
466  (d12 - d22) * (d12 - d22) +
467  (d13 - d23) * (d13 - d23)
468  )
469 
470  # normal vector of the donor-dipole
471  muD1 = (d21 - d11) / dD21
472  muD2 = (d22 - d12) / dD21
473  muD3 = (d23 - d13) / dD21
474 
475  # vector to the middle of the donor-dipole
476  dM1 = d11 + dD21 * muD1 / 2.0
477  dM2 = d12 + dD21 * muD2 / 2.0
478  dM3 = d13 + dD21 * muD3 / 2.0
479 
480  # -- Acceptor -- #
481  # cartesian coordinates of the acceptor
482  a11 = a1[0]
483  a12 = a1[1]
484  a13 = a1[2]
485 
486  a21 = a2[0]
487  a22 = a2[1]
488  a23 = a2[2]
489 
490  # distance between the two end points of the acceptor
491  dA21 = np.sqrt(
492  (a11 - a21) * (a11 - a21) +
493  (a12 - a22) * (a12 - a22) +
494  (a13 - a23) * (a13 - a23)
495  )
496 
497  # normal vector of the acceptor-dipole
498  muA1 = (a21 - a11) / dA21
499  muA2 = (a22 - a12) / dA21
500  muA3 = (a23 - a13) / dA21
501 
502  # vector to the middle of the acceptor-dipole
503  aM1 = a11 + dA21 * muA1 / 2.0
504  aM2 = a12 + dA21 * muA2 / 2.0
505  aM3 = a13 + dA21 * muA3 / 2.0
506 
507  # vector connecting the middle of the dipoles
508  RDA1 = dM1 - aM1
509  RDA2 = dM2 - aM2
510  RDA3 = dM3 - aM3
511 
512  # Length of the dipole-dipole vector (distance)
513  dRDA = np.sqrt(RDA1 * RDA1 + RDA2 * RDA2 + RDA3 * RDA3)
514 
515  # Normalized dipole-diple vector
516  nRDA1 = RDA1 / dRDA
517  nRDA2 = RDA2 / dRDA
518  nRDA3 = RDA3 / dRDA
519 
520  # Orientation factor kappa2
521  kappa = (muA1 * muD1 + muA2 * muD2 + muA3 * muD3
522  - 3.0 * (muD1 * nRDA1 + muD2 * nRDA2 + muD3 * nRDA3)
523  * (muA1 * nRDA1 + muA2 * nRDA2 + muA3 * nRDA3))
524  return dRDA, kappa
525 
526 
527 def kappa(
528  donor_dipole: np.ndarray,
529  acceptor_dipole: np.ndarray
530 ) -> typing.Tuple[float, float]:
531  """Calculates the orientation-factor kappa
532 
533  :param donor_dipole: 2x3 vector of the donor-dipole
534  :param acceptor_dipole: 2x3 vector of the acceptor-dipole
535  :return: distance, kappa
536 
537  Example
538  -------
539 
540  >>> import numpy as np
541  >>> donor_dipole = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
542  ... dtype=np.float64)
543  >>> acceptor_dipole = np.array([[0.0, 0.5, 0.0], [0.0, 0.5, 1.0]],
544  ... dtype=np.float64)
545  >>> kappa(donor_dipole, acceptor_dipole)
546  (0.8660254037844386, 1.0000000000000002)
547  """
548  return kappa_distance(
549  donor_dipole[0], donor_dipole[1],
550  acceptor_dipole[0], acceptor_dipole[1]
551  )
552 
553 
554 def s2delta(
555  s2_donor: float,
556  s2_acceptor: float,
557  r_inf_AD: float,
558  r_0: float = 0.38
559 ) -> typing.Tuple[float, float]:
560  """Calculate s2delta from the residual anisotropies of the donor
561  and acceptor
562 
563  Parameters
564  ----------
565  r_0 : float
566  Fundamental anisotropy, the anisotropy of the dyes at time zero (
567  default value 0.4)
568  s2_donor : float
569  The second rank oder parameter of the donor dye. The second rank oder
570  parameter can be computed using the dye's residual anisotropy (see
571  Notes below)
572  s2_acceptor : float
573  The second rank oder parameter of the direct excited acceptor dye.
574  r_inf_AD : float
575  The residual anisotropy on the acceptor excited by the donor dye.
576 
577  Returns
578  -------
579  s2delta : float
580  A second rank order parameter of the angle [1]_ eq. 10
581  delta : float
582  The angle between the two symmetry axes of the dipols in units of rad.
583 
584  Examples
585  --------
586  >>> from scikit_fluorescence.modeling.kappa2 import s2delta
587  >>> r0 = 0.38
588  >>> s2donor = 0.2
589  >>> s2acceptor = 0.3
590  >>> r_inf_AD = 0.01
591  >>> s2delta(
592  ... r_0=r0,
593  ... s2_donor=s2donor,
594  ... s2_acceptor=s2acceptor,
595  ... r_inf_AD=r_inf_AD
596  ... )
597  (0.4385964912280701, 0.6583029208008411)
598 
599  Notes
600  -----
601  The parameters `s2_donor` and `s2_acceptor`, which correspond to
602  :math:`S^{(2)}_D` and :math:`S^{(2)}_A` are calculated using the dye's
603  residual anisotropy [1]_
604 
605  ..math::
606 
607  S^{(2)}_D = - /sqrt{/frac{r_{D,inf}}{r_{0,D}}} \
608  S^{(2)}_D = /sqrt{/frac{r_{A,inf}}{r_{0,A}}} \
609 
610  References
611  ----------
612 
613  .. [1] Simon Sindbert, Stanislav Kalinin, Hien Nguyen, Andrea Kienzler,
614  Lilia Clima, Willi Bannwarth, Bettina Appel, Sabine Mueller, Claus A. M.
615  Seidel, "Accurate Distance Determination of Nucleic Acids via Foerster
616  Resonance Energy Transfer: Implications of Dye Linker Length and Rigidity"
617  vol. 133, pp. 2463-2480, J. Am. Chem. Soc., 2011
618 
619  """
620  s2_delta = r_inf_AD/(r_0 * s2_donor * s2_acceptor)
621  delta = np.arccos(np.sqrt((2.0 * s2_delta + 1.0) / 3.0))
622  return s2_delta, delta
623 
624 
625 def calculate_kappa_distance(
626  xyz: np.array,
627  aid1: int,
628  aid2: int,
629  aia1: int,
630  aia2: int
631 ) -> typing.Tuple[np.ndarray, np.ndarray]:
632  """Calculates the orientation factor kappa2 and the distance of a
633  trajectory given the atom-indices of the donor and the acceptor.
634 
635  :param xyz: numpy-array (frame, atom, xyz)
636  :param aid1: int, atom-index of d-dipole 1
637  :param aid2: int, atom-index of d-dipole 2
638  :param aia1: int, atom-index of a-dipole 1
639  :param aia2: int, atom-index of a-dipole 2
640 
641  :return: distances, kappa2
642  """
643  n_frames = xyz.shape[0]
644  ks = np.empty(n_frames, dtype=np.float32)
645  ds = np.empty(n_frames, dtype=np.float32)
646 
647  for i_frame in range(n_frames):
648  try:
649  d, k = kappa_distance(
650  xyz[i_frame, aid1], xyz[i_frame, aid2],
651  xyz[i_frame, aia1], xyz[i_frame, aia2]
652  )
653  ks[i_frame] = k
654  ds[i_frame] = d
655  except: # noqa: E722
656  print("Frame ", i_frame, "skipped, calculation error")
657 
658  return ds, ks
659 
660 
661 @nb.jit(nopython=True)
662 def kappasq(
663  delta: float,
664  sD2: float,
665  sA2: float,
666  beta1: float,
667  beta2: float
668 ) -> float:
669  """Calculates kappa2 given a set of oder parameters and angles
670 
671  Parameters
672  ----------
673  delta : float
674  The angle between the symmetry axis of rotation of the dyes in units
675  of rad.
676  sD2 : float
677  The second rank oder parameter of the donor
678  sA2 : float
679  The second rank oder parameter of the acceptor
680  beta1 : float
681  The angle between the symmetry axes of the rotation of the dye and
682  the distance vector RDA between the two dipoles
683  beta2
684  The angle between the symmetry axes of the rotation of the dye and
685  the distance vector RDA between the two dipoles
686 
687  Returns
688  -------
689  kappa2 : float
690  The orientation factor that corresponds to the provided angles.
691 
692  Notes
693  -----
694 
695  This function corresponds to eq. 9 in [1]_.
696 
697  References
698  ----------
699  .. [1] Simon Sindbert, Stanislav Kalinin, Hien Nguyen, Andrea Kienzler,
700  Lilia Clima, Willi Bannwarth, Bettina Appel, Sabine Mueller, Claus A. M.
701  Seidel, "Accurate Distance Determination of Nucleic Acids via Foerster
702  Resonance Energy Transfer: Implications of Dye Linker Length and Rigidity"
703  vol. 133, pp. 2463-2480, J. Am. Chem. Soc., 2011
704 
705  """
706  s2delta = (3.0 * np.cos(delta) * np.cos(delta) - 1.0) / 2.0
707  s2beta1 = (3.0 * np.cos(beta1) * np.cos(beta1) - 1.0) / 2.0
708  s2beta2 = (3.0 * np.cos(beta2) * np.cos(beta2) - 1.0) / 2.0
709  k2 = 2.0 / 3.0 * (
710  1.0 +
711  sD2 * s2beta1 +
712  sA2 * s2beta2 +
713  sD2 * sA2 * (
714  s2delta +
715  6 * s2beta1 * s2beta2 +
716  1 +
717  2 * s2beta1 +
718  2 * s2beta2 -
719  9 * np.cos(beta1) * np.cos(beta2) * np.cos(delta)
720  )
721  )
722  return k2
723 
724 
725 def p_isotropic_orientation_factor(
726  k2: np.ndarray,
727  normalize: bool = True
728 ) -> np.ndarray:
729  """Calculates an the probability of a given kappa2 according to
730  an isotropic orientation factor distribution
731 
732  Parameters
733  ----------
734  k2 : numpy-array
735  An array containing kappa squared values.
736  normalize : bool
737  If this parameter is set to True (default) the returned distribution is
738  normalized to unity.
739 
740  Returns
741  -------
742  p_k2 : numpy-array
743  The probability distribution of kappa2 for isotropic oriented dipoles
744 
745 
746  Example
747  -------
748  >>> from scikit_fluorescence.modeling import kappa2
749  >>> k2 = np.linspace(0.1, 4, 32)
750  >>> p_k2 = kappa2.p_isotropic_orientation_factor(k2=k2)
751  >>> p_k2
752  array([0.17922824, 0.11927194, 0.09558154, 0.08202693, 0.07297372,
753  0.06637936, 0.06130055, 0.05723353, 0.04075886, 0.03302977,
754  0.0276794 , 0.02359627, 0.02032998, 0.01763876, 0.01537433,
755  0.01343829, 0.01176177, 0.01029467, 0.00899941, 0.00784718,
756  0.00681541, 0.00588615, 0.00504489, 0.0042798 , 0.0035811 ,
757  0.00294063, 0.00235153, 0.001808 , 0.00130506, 0.00083845,
758  0.0004045 , 0. ])
759 
760  Notes
761  -----
762  http://www.fretresearch.org/kappasquaredchapter.pdf
763 
764  """
765  ks = np.sqrt(k2)
766  s3 = np.sqrt(3.)
767  r = np.zeros_like(k2)
768  for i, k in enumerate(ks):
769  if 0 <= k <= 1:
770  r[i] = 0.5 / (s3 * k) * np.log(2 + s3)
771  elif 1 <= k <= 2:
772  r[i] = 0.5 / (s3 * k) * np.log((2 + s3)
773  / (k + np.sqrt(k**2 - 1.0)))
774  if normalize:
775  r /= max(1.0, r.sum())
776  return r