33# SPDX-License-Identifier: BSD-3-Clause
44
55"""Analyze mean square displacements (MSDs)"""
6- from collections import namedtuple , OrderedDict
76import math
7+ from collections import OrderedDict , namedtuple
8+ from typing import Literal , Tuple
89
9- import pandas as pd
1010import numpy as np
11+ import pandas as pd
1112import scipy .optimize
1213
13- from . import msd_base
1414from .. import config
15+ from . import msd_base
1516
1617
1718class Msd :
@@ -209,32 +210,40 @@ def fit(self, model="brownian", *args, **kwargs):
209210class AnomalousDiffusion :
210211 r"""Fit anomalous diffusion parameters to MSD values
211212
212- Fit a function :math:`msd(t_\text{lag}) = 4 D t_\text{lag}^\alpha +
213- 4 \epsilon^2`
213+ Fit a function :math:`msd(t_\text{lag}) = 2 n_\text{dim} D t_\text{lag}^\alpha +
214+ 2 n_\text{dim} \epsilon^2`
214215 to the tlag-vs.-MSD graph, where :math:`D` is the diffusion coefficient,
215- :math:`\epsilon` is the positional accuracy ( uncertainty) , and
216- :math:`\alpha` the anomalous diffusion exponent.
216+ :math:`\epsilon` is the positional uncertainty, and :math:`\alpha` the anomalous
217+ diffusion exponent.
217218 """
218219 _fit_parameters = ["D" , "eps" , "alpha" ]
219220
220- def __init__ (self , msd_data , n_lag = np .inf , exposure_time = 0. ,
221- initial = (0.5 , 0.05 , 1. )):
221+ def __init__ (
222+ self ,
223+ msd_data : msd_base .MsdData ,
224+ n_lag : int | Literal [math .inf ] = math .inf ,
225+ exposure_time : float = 0.0 ,
226+ n_dim : int = 2 ,
227+ initial : Tuple [float , float , float ] = (0.5 , 0.05 , 1.0 ),
228+ ):
222229 r"""Parameters
223230 ----------
224- msd_data : msd_base.MsdData
231+ msd_data
225232 MSD data
226- n_lag : int or inf, optional
233+ n_lag
227234 Maximum number of lag times to use for fitting. Defaults to
228235 `inf`, i.e. using all.
229- exposure_time : float, optional
230- Exposure time. Defaults to 0, i.e. no exposure time correction
231- initial : tuple of float, optional
236+ exposure_time
237+ Exposure time. 0 disables exposure time correction.
238+ n_dim
239+ Number of spatial dimensions
240+ initial
232241 Initial guesses for the fitting for :math:`D`, :math:`\epsilon`,
233- and :math:`\alpha`. Defaults to ``(0.5, 0.05, 1.)``.
242+ and :math:`\alpha`.
234243 """
235244 def residual (x , lagt , target ):
236245 d , eps , alpha = x
237- r = self .theoretical (lagt , d , eps , alpha , exposure_time )
246+ r = self .theoretical (lagt , d , eps , alpha , exposure_time , n_dim )
238247 return r - target
239248
240249 initial = np .asarray (initial )
@@ -249,9 +258,11 @@ def residual(x, lagt, target):
249258 lt = lagt [fin ]
250259 nl = min (n_lag , len (tgt ))
251260 f = scipy .optimize .least_squares (
252- residual , initial ,
261+ residual ,
262+ initial ,
253263 bounds = ([0 , - np .inf , 0 ], [np .inf , np .inf , np .inf ]),
254- kwargs = {"lagt" : lt [:nl ], "target" : tgt [:nl ]})
264+ kwargs = {"lagt" : lt [:nl ], "target" : tgt [:nl ]},
265+ )
255266 r .append (f .x )
256267 r = np .array (r )
257268 self ._results [particle ] = np .mean (r , axis = 0 )
@@ -262,6 +273,7 @@ def residual(x, lagt, target):
262273
263274 self ._msd_data = msd_data
264275 self .exposure_time = exposure_time
276+ self .n_dim = n_dim
265277
266278 @staticmethod
267279 def exposure_time_corr (t , alpha , exposure_time , n = 100 ,
@@ -321,7 +333,15 @@ def exposure_time_corr(t, alpha, exposure_time, n=100,
321333 return (np .sum (s , axis = (1 , 2 )) / n ** 2 )** (1 / alpha )
322334
323335 @staticmethod
324- def theoretical (t , d , eps , alpha = 1 , exposure_time = 0 , squeeze_result = True ):
336+ def theoretical (
337+ t : np .ndarray | float ,
338+ d : np .ndarray | float ,
339+ eps : np .ndarray | float ,
340+ alpha : np .ndarray | float = 1.0 ,
341+ exposure_time : float = 0.0 ,
342+ n_dim : int = 2 ,
343+ squeeze_result : bool = True ,
344+ ) -> np .ndarray | float :
325345 r"""Calculate theoretical MSDs for different lag times
326346
327347 Calculate :math:`msd(t_\text{lag}) = 4 D t_\text{app}^\alpha + 4
@@ -331,24 +351,25 @@ def theoretical(t, d, eps, alpha=1, exposure_time=0, squeeze_result=True):
331351
332352 Parameters
333353 ----------
334- t : array-like or scalar
354+ t
335355 Lag times
336- d : float
356+ d
337357 Diffusion coefficient
338- eps : float
339- Positional accuracy.
340- alpha : float, optional
341- Anomalous diffusion exponent. Defaults to 1.
342- exposure_time : float, optional
343- Exposure time. Defaults to 0.
344- squeeze_result : bool, optional
358+ eps
359+ Positional uncertainty.
360+ alpha
361+ Anomalous diffusion exponent.
362+ exposure_time
363+ Exposure time.
364+ n_dim
365+ Number of spatial dimensions
366+ squeeze_result
345367 If `True`, return the result as a scalar type or 1D array if
346- possible. Otherwise, always return a 2D array. Defaults to `True`.
368+ possible. Otherwise, always return a 2D array.
347369
348370 Returns
349371 -------
350- numpy.ndarray or scalar
351- Calculated theoretical MSDs
372+ Calculated theoretical MSDs
352373 """
353374 t = np .atleast_1d (t )
354375 d = np .atleast_1d (d )
@@ -357,17 +378,18 @@ def theoretical(t, d, eps, alpha=1, exposure_time=0, squeeze_result=True):
357378 if d .shape != eps .shape or d .shape != alpha .shape :
358379 raise ValueError ("`d`, `eps`, and `alpha` should have same shape." )
359380 if t .ndim > 1 or d .ndim > 1 or eps .ndim > 1 :
360- raise ValueError ("Number of dimensions of `t`, `d`, `eps`, and "
361- "`alpha` need to be less than 2" )
381+ raise ValueError (
382+ "Number of dimensions of `t`, `d`, `eps`, and `alpha` need to be"
383+ "less than 2"
384+ )
362385
363- ic = 4 * eps ** 2
386+ ic = 2 * n_dim * eps ** 2
364387 ic [eps < 0 ] *= - 1
365388 t_corr = np .empty ((len (t ), len (alpha )), dtype = float )
366389 for i , a in enumerate (alpha ):
367- t_corr [:, i ] = AnomalousDiffusion .exposure_time_corr (
368- t , a , exposure_time )
390+ t_corr [:, i ] = AnomalousDiffusion .exposure_time_corr (t , a , exposure_time )
369391
370- ret = 4 * d [None , :] * t_corr ** alpha [None , :] + ic [None , :]
392+ ret = 2 * n_dim * d [None , :] * t_corr ** alpha [None , :] + ic [None , :]
371393
372394 if squeeze_result :
373395 if ret .size == 1 :
@@ -520,7 +542,7 @@ def _plot_single(self, data_id, n_lag, name, ax, color):
520542 d_err , eps_err , alpha_err = self ._err .get (data_id , (np .nan ,) * 3 )
521543
522544 x = np .linspace (0 , n_lag , 100 )
523- y = self .theoretical (x , d , eps , alpha , self .exposure_time )
545+ y = self .theoretical (x , d , eps , alpha , self .exposure_time , self . n_dim )
524546
525547 legend = []
526548 if name :
@@ -544,15 +566,23 @@ class BrownianMotion(AnomalousDiffusion):
544566 """
545567 _fit_parameters = ["D" , "eps" ]
546568
547- def __init__ (self , msd_data , n_lag = 2 , exposure_time = 0 ):
569+ def __init__ (
570+ self ,
571+ msd_data : msd_base .MsdData ,
572+ n_lag : int | Literal [math .inf ] = 2 ,
573+ exposure_time : float = 0.0 ,
574+ n_dim : int = 2 ,
575+ ):
548576 """Parameters
549577 ----------
550- msd_data : msd_base.MsdData
578+ msd_data
551579 MSD data
552- n_lag : int or inf, optional
553- Maximum number of lag times to use for fitting. Defaults to 2.
554- exposure_time : float, optional
555- Exposure time. Defaults to 0, i.e. no exposure time correction
580+ n_lag
581+ Maximum number of lag times to use for fitting.
582+ exposure_time
583+ Exposure time. 0 disables exposure time correction.
584+ n_dim
585+ Number of spatial dimensions
556586 """
557587 self ._results = OrderedDict ()
558588 self ._err = OrderedDict ()
@@ -578,8 +608,8 @@ def __init__(self, msd_data, n_lag=2, exposure_time=0):
578608 else :
579609 s , i = np .polyfit (lagt [:nl ] - exposure_time / 3 , m [:nl , :], 1 )
580610
581- d = s / 4
582- eps = np .sqrt (i .astype (complex )) / 2
611+ d = s / ( 2 * n_dim )
612+ eps = np .sqrt (i .astype (complex ) / ( 2 * n_dim ))
583613 eps = np .where (i > 0 , np .real (eps ), - np .imag (eps ))
584614
585615 self ._results [particle ] = [np .mean (d ), np .mean (eps )]
@@ -590,9 +620,17 @@ def __init__(self, msd_data, n_lag=2, exposure_time=0):
590620
591621 self ._msd_data = msd_data
592622 self .exposure_time = exposure_time
623+ self .n_dim = n_dim
593624
594625 @staticmethod
595- def theoretical (t , d , eps , exposure_time = 0 ):
626+ def theoretical ( # pyright: ignore reportIncompatibleMethodOverride
627+ t : np .ndarray | float ,
628+ d : np .ndarray | float ,
629+ eps : np .ndarray | float ,
630+ exposure_time : float = 0.0 ,
631+ n_dim : int = 2 ,
632+ squeeze_result : bool = False ,
633+ ) -> np .ndarray | float :
596634 r"""Calculate theoretical MSDs for different lag times
597635
598636 Calculate :math:`msd(t_\text{lag}) = 4 D t_\text{app} + 4
@@ -602,34 +640,34 @@ def theoretical(t, d, eps, exposure_time=0):
602640
603641 Parameters
604642 ----------
605- t : array-like or scalar
643+ t
606644 Lag times
607- d : float
645+ d
608646 Diffusion coefficient
609- eps : float
610- Positional accuracy.
611- alpha : float, optional
612- Anomalous diffusion exponent. Defaults to 1.
613- exposure_time : float, optional
614- Exposure time. Defaults to 0.
615- squeeze_result : bool, optional
647+ eps
648+ Positional uncertainty
649+ exposure_time
650+ Exposure time
651+ n_dim
652+ Number of spatial dimensions
653+ squeeze_result
616654 If `True`, return the result as a scalar type or 1D array if
617655 possible. Otherwise, always return a 2D array. Defaults to `True`.
618656
619657 Returns
620658 -------
621- numpy.ndarray or scalar
622- Calculated theoretical MSDs
659+ Calculated theoretical MSDs
623660 """
624- return AnomalousDiffusion .theoretical (t , d , eps , np .ones_like (d ),
625- exposure_time )
661+ return AnomalousDiffusion .theoretical (
662+ t , d , eps , np .ones_like (d ), exposure_time , n_dim , squeeze_result
663+ )
626664
627665 def _plot_single (self , data_id , n_lag , name , ax , color ):
628666 d , eps = self ._results [data_id ]
629667 d_err , eps_err = self ._err .get (data_id , (np .nan ,) * 2 )
630668
631669 x = np .linspace (0 , n_lag , 100 )
632- y = self .theoretical (x , d , eps , self .exposure_time )
670+ y = self .theoretical (x , d , eps , self .exposure_time , self . n_dim )
633671
634672 legend = []
635673 if name :
0 commit comments