import numpy
import functools
import math

def fval(t,W):
   if callable(W):
      return W(t)
   return W

def L(t,t0,W):
   ft0=fval(t,t0)
   fW=fval(t,W)
   x=(t-ft0)/fW
   if x<-18:
      return 0
   if x>18:
      return 1
   return 1/(1+numpy.exp(-x))
def dL(t,t0,W):
   return L(t,t0,W)*(1-L(t,t0,W))

def dLdW(t,t0,W):
   fW=fval(t,W)
   ft0=fval(t,t0)
   return dL(t,t0,W)*(t-ft0)/fW/fW

def dLdt0(t,t0,W):
   fW=fval(t,W)
   return dL(t,t0,W)/fW

def logisticPar(par):
   t0=par['t0']['value']
   W=par['W']['value']
   f=functools.partial(L,t0=t0,W=W)
   
   #D1[x] could be a function !!!
   Dt0=par['t0']['derivatives']
   dfdt0=functools.partial(dLdt0,t0=t0,W=W)
   dt0=generate(dfdt0,Dt0)
   #D2[x] could be a function !!!
   DW=par['W']['derivatives']
   dfdW=functools.partial(dLdW,t0=t0,W=W)
   dW=generate(dfdW,DW)
   #merge
   return Object(f,[dt0,dW])

def deltaW(t,t1,t2):
   ft1=fval(t,t1)
   ft2=fval(t,t2)
   return ft2-ft1

def deltaT(t,t1):
   ft1=fval(t,t1)
   return t-t1

def fq(t,t1,t2):
   return deltaT(t,t1)/deltaW(t,t1,t2)

def f1(t,C,t1,t2,L1,L2):
   fC=fval(t,C)
   return fC*fq(t,t1,t2)*L1(t)*(1-L2(t))

def f2(t,C,L1,L2):
   fC=fval(t,C)
   return fC*L1(t)*L2(t)

def lg(t,C,t1,t2,eps,L1,L2):
   fC=fval(t,C)
   return eps*fC+f1(t,C,t1,t2,L1,L2)+f2(t,C,L1,L2)

def dlgdt1(t,C,t1,t2,L1,L2):
   fC=fval(t,C)
   return -fC*L1(t)*(1-L2(t))*(1+fq(t,t1,t2))/deltaW(t,t1,t2)

def dlgdt2(t,C,t1,t2,L1,L2):
   return -f1(t,C,t1,t2,L1,L2)/deltaW(t,t1,t2)

def dlgdL1(t,C,t1,t2,L1,L2):
   fC=fval(t,C)
   fL2=L2(t)
   return fC*(fq(t,t1,t2)*(1-fL2)+fL2)

def dlgdL2(t,C,t1,t2,L1,L2):
   fC=fval(t,C)
   return fC*L1(t)*(1-fq(t,t1,t2))

def dlgdC(t,C,t1,t2,L1,L2):
   fL2=L2(t)
   return L1(t)*(fq(t,t1,t2)*(1-fL2)+fL2)

def lgFixedSlope(t,alpha,t1,t2,eps,L1,L2):
   fA=fval(t,alpha)
   ft1=fval(t,t1)
   dW=deltaW(t,t1,t2)
   return fA*(dW*eps+L1(t)*((t-ft1)*(1-L2(t))+dW*L2(t)))

def dlgFixedSlopedt1(t,alpha,L1,L2):
   fA=fval(t,alpha)
   return -fA*L1(t)

def dlgFixedSlopedt2(t,alpha,L1,L2):
   fA=fval(t,alpha)
   return -fA*L1(t)*L2(t)

def dlgFixedSlopedL1(t,alpha,t1,t2,L1,L2):
   return lgFixedSlope(t,alpha,t1,t2,L1,L2)/L1(t)

def dlgFixedSlopedL2(t,alpha,t1,t2,L1,L2):
   fA=fval(t,alpha)
   ft2=fval(t,t2)
   return fA*L1(t)*(ft2-t)

def dlgFixedSlopedalpha(t,alpha,t1,t2,L1,L2):
   fA=fval(t,alpha)
   return lgFixedSlope(t,alpha,t1,t2,L1,L2)/fA

def linearGrowth(par):
   #make sure we never return zero or negative
   C=par['C']['value']
   t1=par['t1']['value']
   t2=par['t2']['value']
   eps=1e-8

   pL1=logisticPar({'t0':par['t1'],'W':par['W1']})
   pL2=logisticPar({'t0':par['t2'],'W':par['W2']})
   L1=pL1['value']
   L2=pL2['value']
   f=functools.partial(lg,C=C,t1=t1,t2=t2,eps=eps,L1=L1,L2=L2)

   Dt1=par['t1']['derivatives']
   dfdt1=functools.partial(dlgdt1,C=C,t1=t1,t2=t2,L1=L1,L2=L2)
   dt1=generate(dfdt1,Dt1)
   
   Dt2=par['t2']['derivatives']
   dfdt2=functools.partial(dlgdt2,C=C,t1=t1,t2=t2,L1=L1,L2=L2)
   dt2=generate(dfdt2,Dt2)

   DL1=pL1['derivatives']
   dfdL1=functools.partial(dlgdL1,C=C,t1=t1,t2=t2,L1=L1,L2=L2)
   dL1=generate(dfdL1,DL1)

   DL2=pL2['derivatives']
   dfdL2=functools.partial(dlgdL2,C=C,t1=t1,t2=t2,L1=L1,L2=L2)
   dL2=generate(dfdL2,DL2)

   DC=par['C']['derivatives']
   dfdC=functools.partial(dlgdC,C=C,t1=t1,t2=t2,L1=L1,L2=L2)
   dC=generate(dfdC,DC)

   return Object(f,[dt1,dt2,dL1,dL2,dC])

def linearGrowthFixedSlope(par):
   #make sure we never return zero or negative
   alpha=par['alpha']['value']
   t1=par['t1']['value']
   t2=par['t2']['value']
   eps=1e-8

   pL1=logisticPar({'t0':par['t1'],'W':par['W1']})
   pL2=logisticPar({'t0':par['t2'],'W':par['W2']})
   L1=pL1['value']
   L2=pL2['value']
   f=functools.partial(lgFixedSlope,alpha=alpha,t1=t1,t2=t2,eps=eps,L1=L1,L2=L2)

   Dt1=par['t1']['derivatives']
   dfdt1=functools.partial(dlgFixedSlopedt1,alpha=alpha,L1=L1,L2=L2)
   dt1=generate(dfdt2,Dt1)
   
   Dt2=par['t2']['derivatives']
   dfdt2=functools.partial(dlgFixedSlopedt2,alpha=alpha,L1=L1,L2=L2)
   dt2=generate(dfdt2,Dt2)

   DL1=pL1['derivatives']
   dfdL1=functools.partial(dlgFixedSlopedL1,alpha=alpha,t1=t1,t2=t2,L1=L1,L2=L2)
   dL1=generate(dfdL1,DL1)

   DL2=pL2['derivatives']
   dfdL2=functools.partial(dlgFixedSlopedL2,alpha=alpha,t1=t1,t2=t2,L1=L1,L2=L2)
   dL2=generate(dfdL2,DL2)

   DA=par['alpha']['derivatives']
   dfdA=functools.partial(dlgFixedSlopedalpha,alpha=alpha,t1=t1,t2=t2,L1=L1,L2=L2)
   dA=generate(dfdA,DA)

   return Object(f,[dt1,dt2,dL1,dL2,dA])

def feps(t,A,tau):
   fA=fval(t,A)
   fTau=fval(t,tau)
   return fA*math.exp(-t/fTau)

def dfepsdA(t,A,tau):
   fTau=fval(t,tau)
   return math.exp(-t/fTau)

def dfepsdTau(t,A,tau):
   fTau=fval(t,tau)
   fA=fval(t,A)
   if fTau==0:
      return 0
   return fA*math.exp(-t/fTau)*(t/fTau/fTau)


def exp(par):
   A=par['A']['value']
   tau=par['tau']['value']
   f=functools.partial(feps,A=A,tau=tau)

   DA=par['A']['derivatives']
   dfdA=functools.partial(dfepsdA,A=A,tau=tau)
   dA=generate(dfdA,DA)

   DTau=par['tau']['derivatives']
   dfdTau=functools.partial(dfepsdTau,A=A,tau=tau)
   dTau=generate(dfdTau,DTau)
   
   return Object(f,[dA,dTau])

def Object(v,dArray):
   o={'value':v,'function':True}
   allKeys=[]
   for x in dArray:
      allKeys.extend(x.keys()) 
   allKeys=list(set(allKeys))
   o['derivatives']={x:sumDict(dArray,x) for x in allKeys}
   return o

def derivedObject(f,ders):
   o={'value':f}
   DD=[]
   for d in ders:
      df=d['df']
      D=d['D']
      DD.append({x:df*D[x] for x in D})
   allKeys=[]
   for x in DD:
      allKeys.extend(x.keys()) 
   allKeys=list(set(allKeys))
   o['derivatives']={x:sumValues(DD,x) for x in allKeys}
   return o
 

def sumDict(dArray,x):
   #print('sumFunctions for {}'.format(x))
   fs=[]
   for a in dArray:
      #print('\tChecking {}'.format(a))
      try:
         #check if a[x] is a function with a dummy parameter
         fs.append(a[x])
         #print('\tAdding [{}]'.format(v))
      except KeyError:
         #print('\t{} not in table'.format(x))
         continue
   return sumArray(fs)

def sumArray(fs,w=1):
   return lambda t,fs=fs,w=w: w*sum([to(f)(t) for f in fs])

def sumValues(dArray,x):
   s=0
   for a in dArray:
      try:
         s+=a[x]
      except KeyError:
         continue
   return s

def isFunction(vObject):
   if callable(vObject['value']):
      return True
   return False

def to(a):
   if callable(a):
      return a
   return lambda t,a=a:a
 
def contains(arr):
   for a in arr:
      if callable(a):
         return True
   return False

def multiply(t,a,b):
   #abstract actual type of a and b
   fa=fval(t,a)
   fb=fval(t,b)
   return fa*fb

def generate(df,D):
   return {x:functools.partial(multiply,a=D[x],b=df) for x in D}

def product(pA,pB):
   #return a product of two values with derivatives
   #print('{}*{}'.format(d['a'],d['b']))
   a=pA['value']
   DA=pA['derivatives']
   b=pB['value']
   DB=pB['derivatives']
   if any(['function' in pA,'function' in pB]):
      fa=to(a)
      fb=to(b)
      f=lambda t,a=fa,b=fb,:a(t)*b(t)
      dfdA=lambda t,b=fb: b(t)
      dfdB=lambda t,a=fa: a(t)
      dA=generate(dfdA,DA)
      dB=generate(dfdB,DB)
      return Object(f,[dA,dB])
   else:
      return derivedObject(a*b,[{'df':b,'D':DA},{'df':a,'D':DB}])

def power(pA,pN):
   #return pA to the power of pN, numpy.power(pA,pN), with derivatives
   a=pA['value']
   DA=pA['derivatives']
   n=pN['value']
   DN=pN['derivatives']
   if any(['function' in pA,'function' in pN]):
      fa=to(a)
      fn=to(n)
      f=lambda t,a=fa,n=fn:numpy.power(a(t),n(t))
      dfdA=lambda t,n=fn,f=f,a=fa:n(t)*f(t)/a(t)
      dfdN=lambda t,a=fa,f=f:numpy.log(a(t))*f(t)
      dA=generate(dfdA,DA)
      dN=generate(dfdN,DN)
      return Object(f,[dA,dN])
   else:
      f=numpy.power(a,n)
      return derivedObject(f,[{'df':n*f/a,'D':DA},{'df':f*numpy.log(a),'D':DN}])

def ratio(pA,pB):
   #return ratio pA/pB with all derivatives
   #print('{}/{}'.format(d['a'],d['b']))
   a=pA['value']
   DA=pA['derivatives']
   b=pB['value']
   DB=pB['derivatives']
   if any(['function' in pA,'function' in pB]):
      fa=to(a)
      fb=to(b)
      f=lambda t,a=fa,b=fb,:a(t)/b(t)
      dfdA=lambda t,f=f,a=fa: f(t)/a(t)
      dfdB=lambda t,f=f,b=fb: -f(t)/b(t)
      dA=generate(dfdA,DA)
      dB=generate(dfdB,DB)
      return Object(f,[dA,dB])
   else:
      return derivedObject(a/b,[{'df':1/b,'D':DA},{'df':-a/b/b,'D':DB}])

def add(pA,pB):
   #return sum of pA and pB with derivatives
#print('{}+{}'.format(d['a'],d['b']))
   a=pA['value']
   DA=pA['derivatives']
   b=pB['value']
   DB=pB['derivatives']
#even more generic -> df/dp=[df/dA*dA/dp+df/dB*dB/dp]
   if any(['function' in pA,'function' in pB]):
      fa=to(a)
      fb=to(b)
      f=lambda t,a=fa,b=fb,:a(t)+b(t)
      dfdA=lambda t: 1
      dfdB=lambda t: 1
      dA=generate(dfdA,DA)
      dB=generate(dfdB,DB)
      return Object(f,[dA,dB])
   else:
      return derivedObject(a+b,[{'df':1,'D':DA},{'df':1,'D':DB}])



