2013-07-17 11:00:24 +00:00
|
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
__author__ = 'markusro'
|
|
|
|
|
2014-04-03 18:56:50 +00:00
|
|
|
from PyQt4.QtGui import QColor
|
2014-09-17 07:46:14 +00:00
|
|
|
from PyQt4.QtCore import QObject, pyqtSignal, QThread, pyqtSlot
|
2014-04-07 11:41:39 +00:00
|
|
|
|
|
|
|
import numpy as np
|
2014-04-03 18:56:50 +00:00
|
|
|
from scipy import optimize as opt, odr
|
|
|
|
|
|
|
|
import libyaff
|
|
|
|
|
2013-07-17 11:00:24 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def id_to_color( id ):
|
2014-04-03 18:56:50 +00:00
|
|
|
colors = [
|
2014-09-24 14:25:58 +00:00
|
|
|
QColor(255, 255, 255),
|
2014-09-17 07:46:14 +00:00
|
|
|
QColor(168, 149, 17),
|
2014-09-24 14:25:58 +00:00
|
|
|
QColor(45, 142, 15),
|
|
|
|
QColor(160, 16, 36),
|
|
|
|
QColor(54, 22, 115),
|
2014-09-17 07:46:14 +00:00
|
|
|
QColor(36, 10, 85),
|
|
|
|
QColor(118, 8, 23),
|
|
|
|
QColor(31, 105, 7),
|
|
|
|
QColor(124, 109, 8),
|
|
|
|
]
|
2014-09-24 14:25:58 +00:00
|
|
|
chosen_color = colors[id%len(colors)]
|
|
|
|
return chosen_color
|
2013-07-17 11:00:24 +00:00
|
|
|
|
2014-04-11 07:29:32 +00:00
|
|
|
|
|
|
|
class FitFunctionCreator(QObject):
|
|
|
|
new_data = pyqtSignal(np.ndarray, np.ndarray)
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def __init__( self ):
|
|
|
|
super(FitFunctionCreator, self).__init__()
|
2014-03-05 15:59:35 +00:00
|
|
|
self.data = None
|
|
|
|
self.functions = Functions()
|
|
|
|
|
2014-04-11 07:29:32 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fitfcn( self, p0, x, *funcs ):
|
2014-04-11 07:29:32 +00:00
|
|
|
if x.ndim == 2:
|
2014-09-17 07:46:14 +00:00
|
|
|
self.data = np.zeros(x.shape)
|
2014-04-11 07:29:32 +00:00
|
|
|
else:
|
2014-09-17 07:46:14 +00:00
|
|
|
self.data = np.zeros((2, x.size))
|
2014-04-11 07:29:32 +00:00
|
|
|
ndx = 0
|
2014-09-17 07:46:14 +00:00
|
|
|
for fn in funcs: # loop over functions and add the results
|
2014-04-11 07:29:32 +00:00
|
|
|
f, num_p = fn.function, fn.param_number
|
2014-09-17 07:46:14 +00:00
|
|
|
p = p0[ndx:ndx+num_p]
|
2014-04-11 07:29:32 +00:00
|
|
|
if x.ndim == 2:
|
|
|
|
x = x[0]
|
|
|
|
result = f(p, x)
|
2014-09-17 07:46:14 +00:00
|
|
|
# fn.widget.updateTable(p)
|
|
|
|
self.data += result # fit functions take only 1-dim x
|
2014-04-11 07:29:32 +00:00
|
|
|
ndx += num_p
|
|
|
|
self.new_data.emit(x, self.data)
|
|
|
|
return self.data
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fitfcn_imag( self, p0, x, *funcs ):
|
2014-03-05 17:30:00 +00:00
|
|
|
if x.ndim == 2:
|
2014-09-17 07:46:14 +00:00
|
|
|
self.data = np.zeros(x.shape)
|
2014-03-05 17:30:00 +00:00
|
|
|
else:
|
2014-09-17 07:46:14 +00:00
|
|
|
self.data = np.zeros((2, x.size))
|
2014-03-05 15:59:35 +00:00
|
|
|
ndx = 0
|
2014-09-17 07:46:14 +00:00
|
|
|
for fn in funcs: # loop over functions and add the results
|
2014-04-14 13:47:16 +00:00
|
|
|
f, num_p = fn.function, fn.param_number
|
2014-09-17 07:46:14 +00:00
|
|
|
p = p0[ndx:ndx+num_p]
|
2014-03-05 17:30:00 +00:00
|
|
|
if x.ndim == 2:
|
|
|
|
x = x[0]
|
2014-04-03 18:56:50 +00:00
|
|
|
result = f(p, x)
|
2014-09-17 07:46:14 +00:00
|
|
|
self.data += result # fit functions take only 1-dim x
|
2014-03-05 15:59:35 +00:00
|
|
|
ndx += num_p
|
2014-04-11 07:29:32 +00:00
|
|
|
self.new_data.emit(x, self.data)
|
2014-04-14 13:47:16 +00:00
|
|
|
return self.data[1]
|
2014-03-05 15:59:35 +00:00
|
|
|
|
2014-04-07 11:41:39 +00:00
|
|
|
|
|
|
|
class FitRoutine(QObject):
|
|
|
|
finished_fit = pyqtSignal()
|
2014-04-11 07:29:32 +00:00
|
|
|
data_ready = pyqtSignal(np.ndarray, np.ndarray)
|
2014-04-16 15:56:32 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def __init__( self ):
|
|
|
|
super(FitRoutine, self).__init__()
|
2014-04-07 11:41:39 +00:00
|
|
|
self.f = FitFunctionCreator()
|
2014-04-11 07:29:32 +00:00
|
|
|
self.f.new_data.connect(self.data_ready.emit)
|
2014-04-07 11:41:39 +00:00
|
|
|
self._fitter = self.fit_odr_cmplx
|
|
|
|
self._odr_fit = None
|
2014-04-16 15:56:32 +00:00
|
|
|
self._start_parameter = None
|
2014-09-17 07:46:14 +00:00
|
|
|
|
2014-04-16 15:56:32 +00:00
|
|
|
@property
|
2014-09-17 07:46:14 +00:00
|
|
|
def start_parameter( self ):
|
2014-04-16 15:56:32 +00:00
|
|
|
return self._start_parameter
|
2014-04-07 11:41:39 +00:00
|
|
|
|
2014-04-16 15:56:32 +00:00
|
|
|
@start_parameter.setter
|
2014-09-17 07:46:14 +00:00
|
|
|
def start_paramter( self, p0 ):
|
2014-04-16 15:56:32 +00:00
|
|
|
self._start_parameter = p0
|
2014-09-17 07:46:14 +00:00
|
|
|
|
2014-04-07 11:41:39 +00:00
|
|
|
@property
|
2014-09-17 07:46:14 +00:00
|
|
|
def fitter( self ):
|
2014-04-07 11:41:39 +00:00
|
|
|
return self._fitter
|
|
|
|
|
|
|
|
@fitter.setter
|
2014-09-17 07:46:14 +00:00
|
|
|
def fitter( self, f ):
|
2014-04-07 11:41:39 +00:00
|
|
|
self._fitter = f
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fit_odr_cmplx( self, x, y, p0, fixed, fcns ):
|
2014-04-16 15:56:32 +00:00
|
|
|
self._start_parameter = p0
|
2014-04-07 11:41:39 +00:00
|
|
|
if np.iscomplexobj(y) and y.ndim == 1:
|
|
|
|
weights = 1/np.abs(y)**2
|
|
|
|
we = np.resize(weights, (2, weights.size))
|
2014-09-17 07:46:14 +00:00
|
|
|
# we = 1/N.array([y.real**2, y.imag**2])
|
2014-04-07 11:41:39 +00:00
|
|
|
y = np.array([y.real, y.imag])
|
|
|
|
else:
|
|
|
|
raise NotImplementedError, "need complex input for now"
|
|
|
|
dat = odr.Data(x, y, we=we)
|
|
|
|
mod = odr.Model(self.f.fitfcn, extra_args=fcns)
|
2014-04-14 13:47:16 +00:00
|
|
|
self._odr_fit = odr.ODR(dat, mod, p0, ifixx=(0,), ifixb=fixed, maxit=800)
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fit_odr_imag( self, x, y, p0, fixed, fcns ):
|
2014-04-16 15:56:32 +00:00
|
|
|
self._start_parameter = p0
|
2014-04-14 13:47:16 +00:00
|
|
|
if np.iscomplexobj(y) and y.ndim == 1:
|
|
|
|
we = 1/np.imag(y)**2
|
|
|
|
else:
|
|
|
|
raise NotImplementedError, "need complex input for now"
|
|
|
|
dat = odr.Data(x, y.imag, we=we)
|
|
|
|
mod = odr.Model(self.f.fitfcn_imag, extra_args=fcns)
|
|
|
|
self._odr_fit = odr.ODR(dat, mod, p0, ifixx=(0,), ifixb=fixed, maxit=800)
|
2014-04-07 11:41:39 +00:00
|
|
|
|
2014-04-11 07:29:32 +00:00
|
|
|
@pyqtSlot()
|
2014-09-17 07:46:14 +00:00
|
|
|
def fit( self ):
|
2014-04-16 15:56:32 +00:00
|
|
|
try:
|
|
|
|
self._odr_fit.run()
|
|
|
|
except RuntimeError:
|
|
|
|
print "muh"
|
2014-04-07 11:41:39 +00:00
|
|
|
self.finished_fit.emit()
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def result( self ):
|
2014-04-16 15:56:32 +00:00
|
|
|
if self._odr_fit.output is None:
|
2014-09-17 07:46:14 +00:00
|
|
|
self._odr_fit.output = odr.Output([self.start_parameter, None, None])
|
2014-04-16 15:56:32 +00:00
|
|
|
self._odr_fit.output.stopreason = ["Aborted by user"]
|
2014-04-07 11:41:39 +00:00
|
|
|
return self._odr_fit.output
|
|
|
|
|
|
|
|
|
2014-03-19 21:02:26 +00:00
|
|
|
class FunctionRegister:
|
2014-09-17 07:46:14 +00:00
|
|
|
def __init__( self ):
|
|
|
|
self.registry = { }
|
2014-03-19 21:02:26 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def register_function( self, obj ):
|
|
|
|
# print "FR: Registering:",obj
|
2014-04-11 16:20:16 +00:00
|
|
|
id_string = obj.id_label
|
2014-03-19 21:02:26 +00:00
|
|
|
if self.registry.has_key(obj):
|
2014-09-17 07:46:14 +00:00
|
|
|
raise AssertionError, "The object is already registered! This should NOT happen"
|
|
|
|
self.registry[obj] = id_string
|
2014-03-20 14:15:40 +00:00
|
|
|
#print "FR: ",self.registry
|
2014-03-19 21:02:26 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def unregister_function( self, obj ):
|
|
|
|
# print "FR: UnRegistering:",obj
|
2014-03-19 21:02:26 +00:00
|
|
|
if self.registry.has_key(obj):
|
|
|
|
self.registry.pop(obj)
|
|
|
|
else:
|
2014-04-14 12:01:45 +00:00
|
|
|
obj.deleteLater()
|
2014-09-17 07:46:14 +00:00
|
|
|
raise AssertionError, "The object is not in the registry! This should NOT happen"
|
|
|
|
#print "FR: ",self.registry
|
2014-03-05 15:59:35 +00:00
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def get_registered_functions( self ):
|
2014-03-19 21:02:26 +00:00
|
|
|
return self.registry
|
2014-04-14 13:47:16 +00:00
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
# ############# deprecated #####################
|
|
|
|
def fit_odr_cmplx( x, y, p0, fixed, fcns ):
|
2014-04-14 13:47:16 +00:00
|
|
|
f = FitFunctionCreator()
|
|
|
|
#if x.ndim < 2:
|
|
|
|
# x = N.resize(x, (2,x.size))
|
|
|
|
if np.iscomplexobj(y) and y.ndim == 1:
|
|
|
|
weights = 1/np.abs(y)**2
|
|
|
|
we = np.resize(weights, (2, weights.size))
|
|
|
|
#we = 1/N.array([y.real**2, y.imag**2])
|
|
|
|
y = np.array([y.real, y.imag])
|
|
|
|
else:
|
|
|
|
raise NotImplementedError, "need complex input for now"
|
|
|
|
dat = odr.Data(x, y, we=we)
|
|
|
|
mod = odr.Model(f.fitfcn, extra_args=fcns)
|
|
|
|
fit = odr.ODR(dat, mod, p0, ifixx=(0,), ifixb=fixed, maxit=8000)
|
|
|
|
fit.run()
|
|
|
|
#print fit.output.pprint()
|
|
|
|
return fit.output
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
|
2014-04-14 13:47:16 +00:00
|
|
|
### define funcs here
|
|
|
|
class Functions(QObject):
|
2014-09-17 07:46:14 +00:00
|
|
|
def __init__( self ):
|
|
|
|
super(Functions, self).__init__()
|
2014-04-14 13:47:16 +00:00
|
|
|
self.list = {
|
|
|
|
# provides functions: "id_string":(function, number_of_parameters)
|
2014-09-17 07:46:14 +00:00
|
|
|
"hn": (self.hn_cmplx, 4),
|
|
|
|
"conductivity": (self.cond_cmplx, 3),
|
|
|
|
"power": (self.power_cmplx, 2),
|
|
|
|
"static": (self.static_cmplx, 1),
|
|
|
|
"yaff": (self.yaff, 8)
|
2014-04-14 13:47:16 +00:00
|
|
|
}
|
|
|
|
self.YAFF = libyaff.Yaff()
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def hn_cmplx( self, p, x ):
|
2014-04-14 13:47:16 +00:00
|
|
|
om = 2*np.pi*x
|
|
|
|
#hn = om*1j
|
2014-09-17 07:46:14 +00:00
|
|
|
eps, t, a, b = p
|
2014-04-14 13:47:16 +00:00
|
|
|
hn = eps/(1+(1j*om*t)**a)**b
|
|
|
|
cplx = np.array([hn.real, -hn.imag])
|
|
|
|
return cplx
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def cond_cmplx( self, p, x ):
|
2014-04-14 13:47:16 +00:00
|
|
|
om = 2*np.pi*x
|
|
|
|
sgma, isgma, n = p
|
2014-09-17 07:46:14 +00:00
|
|
|
cond = sgma/(om**n)+isgma/(1j*om**n) # Jonscher (Universal Dielectric Response: e",e' prop sigma/omega**n
|
2014-04-14 13:47:16 +00:00
|
|
|
cplx = np.array([cond.real, -cond.imag])
|
|
|
|
return cplx
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def power_cmplx( self, p, x ):
|
2014-04-14 13:47:16 +00:00
|
|
|
om = 2*np.pi*x
|
2014-09-17 07:46:14 +00:00
|
|
|
sgma, n = p
|
2014-04-14 13:47:16 +00:00
|
|
|
power = sgma/(om*1j)**n
|
|
|
|
cplx = np.array([power.real, -power.imag])
|
|
|
|
return cplx
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def static_cmplx( self, p, x ):
|
2014-04-14 13:47:16 +00:00
|
|
|
eps_inf = p[0]
|
2014-09-17 07:46:14 +00:00
|
|
|
static = np.ones((2, x.size))*eps_inf
|
|
|
|
static[1, :] *= 0 # set imag part zero
|
2014-04-14 13:47:16 +00:00
|
|
|
#cplx = N.array([static.real, static.imag])
|
|
|
|
return static
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def yaff( self, p, x ):
|
|
|
|
ya = self.YAFF.yaff(p[:8], x)
|
2014-04-14 13:47:16 +00:00
|
|
|
cplx = np.array([ya.imag, ya.real])
|
|
|
|
return cplx
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def get( self, name ):
|
2014-04-14 13:47:16 +00:00
|
|
|
return self.list[name]
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def get_function( self, name ):
|
2014-04-14 13:47:16 +00:00
|
|
|
return self.list[name][0]
|
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fit_anneal( x, y, p0, fixed, funcs ):
|
2014-04-14 13:47:16 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
bounds = [(0, 1e14), (0, 1)]
|
2014-09-17 07:46:14 +00:00
|
|
|
for i in xrange(len(p0[2:])/4):
|
|
|
|
bounds.append((1e-4, 1e12)) # delta_eps
|
|
|
|
bounds.append((1e-12, 1e3)) # tau
|
|
|
|
bounds.append((0.1, 1)) # a
|
|
|
|
bounds.append((0.1, 1)) # b
|
2014-04-14 13:47:16 +00:00
|
|
|
ret = opt.anneal(mini_func, p0,
|
|
|
|
args=(x, y),
|
|
|
|
maxeval=20000,
|
|
|
|
maxiter=30000,
|
|
|
|
lower=[b[0] for b in bounds],
|
|
|
|
upper=[b[1] for b in bounds],
|
|
|
|
dwell=100,
|
|
|
|
full_output=1)
|
|
|
|
#pmin, func_min, final_Temp, cooling_iters,accepted_tests, retval
|
|
|
|
#retval : int
|
|
|
|
#Flag indicating stopping condition::
|
|
|
|
|
|
|
|
# 0 : Points no longer changing
|
|
|
|
# 1 : Cooled to final temperature
|
|
|
|
# 2 : Maximum function evaluations
|
|
|
|
# 3 : Maximum cooling iterations reached
|
|
|
|
# 4 : Maximum accepted query locations reached
|
|
|
|
# 5 : Final point not the minimum amongst encountered points
|
|
|
|
|
|
|
|
print "Stop reason", ret
|
|
|
|
return ret[0]
|
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def fit_lbfgsb( x, y, p0, fixed, funcs ):
|
2014-04-14 13:47:16 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
# TODO fixed parameters…
|
|
|
|
bounds = [(0, None), (0, 1)]
|
2014-09-17 07:46:14 +00:00
|
|
|
for i in xrange(len(p0[3:])/4):
|
|
|
|
bounds.append((1e-4, 1e12)) # delta_eps
|
|
|
|
bounds.append((1e-12, 1e3)) # tau
|
|
|
|
bounds.append((0.1, 1)) # a
|
|
|
|
bounds.append((0.1, 1)) # b
|
2014-04-14 13:47:16 +00:00
|
|
|
|
|
|
|
x, f_minvalue, info_dict = opt.fmin_l_bfgs_b(mini_func, p0,
|
|
|
|
fprime=None,
|
|
|
|
args=(x, y),
|
|
|
|
approx_grad=True,
|
|
|
|
bounds=bounds,
|
|
|
|
iprint=0,
|
|
|
|
maxfun=4000)
|
|
|
|
if info_dict['warnflag'] != 0:
|
|
|
|
print info_dict["task"]
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
|
|
# Replaced with fit_odr_cmplx
|
|
|
|
#
|
|
|
|
#def fit_odr(x, y, p0, fixed, funcs):
|
|
|
|
# dat = odr.Data(x, y, 1.0 / y**2)
|
|
|
|
# mod = odr.Model(multi_hn)
|
|
|
|
# fit = odr.ODR(dat, mod, p0, ifixx=(0,), ifixb=fixed, maxit=2000)
|
|
|
|
# fit.run()
|
|
|
|
# return fit.output.beta
|
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def hn( p, nu ):
|
2014-04-14 13:47:16 +00:00
|
|
|
delta_eps, tau, a, b = p
|
2014-09-17 07:46:14 +00:00
|
|
|
om = 2*np.pi*nu
|
|
|
|
Phi = np.arctan((om*tau)**a*np.sin(np.pi*a/2.)/(1.+(om*tau)**a*np.cos(np.pi*a/2.)))
|
|
|
|
e_loss = delta_eps*(1+2*(om*tau)**a*np.cos(np.pi*a/2.)+(om*tau)**(2.*a) )**(
|
|
|
|
-b/2.)*np.sin(b*Phi)
|
|
|
|
e_stor = delta_eps*(1+2*(om*tau)**a*np.cos(np.pi*a/2.)+(om*tau)**(2.*a) )**(
|
|
|
|
-b/2.)*np.cos(b*Phi)
|
|
|
|
return e_loss # 2* oder nicht?
|
2014-04-14 13:47:16 +00:00
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def mini_func( p, x, y ):
|
|
|
|
res = y-multi_hn(p, x)
|
2014-04-14 13:47:16 +00:00
|
|
|
# apply weights
|
2014-09-17 07:46:14 +00:00
|
|
|
res /= 1/y
|
2014-04-14 13:47:16 +00:00
|
|
|
return np.sqrt(np.dot(res, res))
|
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def multi_hn( p, nu ):
|
2014-04-14 13:47:16 +00:00
|
|
|
conductivity = p[1]
|
|
|
|
cond_beta = p[2]
|
2014-09-17 07:46:14 +00:00
|
|
|
om = 2*np.pi*nu
|
|
|
|
e_loss = conductivity/om**cond_beta
|
2014-04-14 13:47:16 +00:00
|
|
|
e_loss += p[0]
|
|
|
|
#for key, igroup in groupby(p[3:], lambda x: x//4):
|
2014-09-17 07:46:14 +00:00
|
|
|
for i in xrange(len(p[3:])/4):
|
|
|
|
delta_eps, tau, a, b = p[3+i*4:3+(i+1)*4]
|
2014-04-14 13:47:16 +00:00
|
|
|
#delta_eps, tau, a, b = list(igroup)
|
|
|
|
#print delta_eps,tau,a,b
|
|
|
|
#a = 0.5 *(1 + N.tanh(a))
|
|
|
|
#b = 0.5 *(1 + N.tanh(b))
|
2014-09-17 07:46:14 +00:00
|
|
|
Phi = np.arctan((om*tau)**a*np.sin(np.pi*a/2.)/(1.+(om*tau)**a*np.cos(np.pi*a/2.)))
|
|
|
|
e_loss += 2*delta_eps*(1+2*(om*tau)**a*np.cos(np.pi*a/2.)+(om*tau)**(2.*a) )**(
|
|
|
|
-b/2.)*np.sin(b*Phi)
|
2014-04-14 13:47:16 +00:00
|
|
|
#e_stor = delta_eps * (1+ 2*(om*tau)**a * N.cos(N.pi*a/2.) + (om*tau)**(2.*a) )**(-b/2.)*N.cos(b*Phi)
|
|
|
|
return e_loss
|
|
|
|
|
|
|
|
|
2014-09-17 07:46:14 +00:00
|
|
|
def tau_peak( f, a, b ):
|
|
|
|
tau = (np.sin(np.pi*a/2./(b+1))/np.sin(np.pi*a*b/2./(b+1)))**(1/a)
|
|
|
|
tau /= 2*np.pi*f
|
2014-04-14 13:47:16 +00:00
|
|
|
return tau
|
|
|
|
|
|
|
|
|
|
|
|
|