# Frequency Distributions

See notes 20.021.Frequency_Distributions.md

```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from IPython.core.display import HTML, display
from importlib import reload
import scipy.stats as ss

# pandas options
pd.set_option('max_rows', 50)
pd.set_option('max_columns', 30)
pd.set_option('display.max_colwidth', 150)

# matplotlib and plotting options
%matplotlib inline
%config InlineBackend.figure_format = 'svg'

# seaborn options
sns.set(context='paper', style='darkgrid', font='serif')
# sns.set(context='paper', style='ticks', font='serif')

# warnings
import warnings
# warnings.simplefilter('error')
# warnings.simplefilter('ignore')

# this file is in examples
import sys
sys.path.insert(0,'/s/telos/python/aggregate_project/')
import aggregate as agg

import aggregate.parser as parser
import aggregate.underwriter as trash
uw = trash.Underwriter(debug=False)
from aggregate.utils import sgamma_fit, MomentAggregator

```

```python
sns.palplot(sns.color_palette("husl", 7))
sns.palplot(sns.color_palette("Paired", 8))
sns.set_palette(sns.color_palette("Paired", 8))
current_palette = sns.color_palette()
sns.palplot(current_palette)
```

```python
ft = lambda x: np.fft.fft(x)

ift = lambda x : np.fft.ifft(x)
```

```python
def mixing(n, ν, f, κ, log2, xmax=500, **kwds):
    """
    n = E(N) = expected claim count
    ν = CV(mixing) = asymptotic CV of any compound aggregate whose severity has a second moment
    f = proportion of certain claims, 0 <= f < 1, higher f corresponds to greater skewnesss
    κ = claims per occurrence
    """

    def defuzz(x):
        x[np.abs(x) < 5e-16] = 0
        return x

    ma = lambda x : list(MomentAggregator._moments_to_mcvsk(*x))

    def row(ps):
        moms = [(x**k * ps).sum() for k in (1,2,3)]
        stats = ma(moms)
        return moms + stats + [np.nan]

    def Nmoms(n, c, g):
        """
        c=Var(G), g=E(G^3) return EN, EN2, EN3
        """
        return [n, n * (1 + (1 + c) * n), n * (1 + n * (3*(1+c) + n * g))]

    def asy_skew(g, c):
        """
        asymptotic skewnewss
        """
        return [(g - 3 * c - 1) / c**1.5]

    ans = pd.DataFrame(columns=['X', 'type', 'EX', 'EX2', 'EX3', 'mean', 'CV', 'Skew',  'Asym Skew'])
    ans = ans.set_index(['X', 'type'])

    N = 1 << log2
    x = np.arange(N, dtype=float)
    z = np.zeros(N)
    z[1] = 1
    fz = ft(z)

    # build poisson for comparison
    dist = 'poisson'
    kernel = n * (fz - 1)
    p = np.real(ift(np.exp(kernel)))
    p = defuzz(p)

    # for poisson c=0 and g=1 (the "mixing" distribution is G identically equal to 1)
    ans.loc[(dist, 'empirical'), :] = row(p)
    temp = Nmoms(n, 0, 1)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + [np.inf]
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # negative binomial
    # Var(G) = c, E(G)=1 so Var(G) = ν^2 = c
    # wikipedia / scipy stats use a and θ for shape and scale
    dist = 'neg bin'
    c = ν * ν   # contagion
    a = 1 / c
    θ = c
    # E(G^3): skew(G) = skew(G') = γ, so E(G-EG)^3 = γν^3, so EG^3 = γν^3 + 3(1+c) - 2 = γν^3 + 3c + 1
    # for Gamma skew = 2 / sqrt(a) = 2ν
    g = 1 + 3*c + 2*c*c
    nb = np.real(ift( (1 - θ * kernel) ** -a))
    nb = defuzz(nb)

    ans.loc[(dist, 'empirical'), :] = row(nb)
    # this row is generic: it applies to all distributions
    temp = Nmoms(n, c, g)   # [n, n * (1 + (1 + c) * n), n*(1 + n * (3*(1+c) + n * g))] # n**3*g + 3*n**2*(1+c)+n]
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # delaporte G = f + G'
    dist = 'delaporte'
    c = ν * ν   # contagion
    a = ((1 - f) / ν)**2
    θ = (1 - f) / a
    g = 2 * ν**4 / (1-f) + 3 * c + 1
    delaporte = np.real(ift( np.exp(f * kernel) *  (1 - θ * kernel) ** -a))
    delaporte = defuzz(delaporte)

    ans.loc[(dist, 'empirical'), :] = row(delaporte)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # pig
    dist = 'pig'
    c = ν * ν   # contagion
    μ = c
    λ = 1 / μ
    # our param (λ, μ) --> (λ, λμ) in Mathematica and hence skew = γ = 3 * sqrt(μ) in scip py parameterization
    γ = 3 * np.sqrt(μ)
    g = γ * ν**3 + 3 * c + 1
    pig = np.real(ift( np.exp( 1/μ * (1 - np.sqrt(1 - 2 * μ**2 * λ * kernel)) )))
    pig = defuzz(pig)
    print(f'PIG parameters μ={μ} and λ={λ}, g={g}, γ={γ}')
    ans.loc[(dist, 'empirical'), :] = row(pig)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # shifted pig
    dist = 'shifted pig'
    c = ν * ν   # contagion
    μ = c / (1 - f)**2
    λ = (1 - f) / μ
    γ = 3 * np.sqrt(μ)
    g = γ * ν**3 + 3 * c + 1
    shifted_pig = np.real(ift(  np.exp(f * kernel) * np.exp( 1/μ * (1 - np.sqrt(1 - 2 * μ**2 * λ * kernel)))))
    shifted_pig = defuzz(shifted_pig)

    ans.loc[(dist, 'empirical'), :] = row(shifted_pig)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    #  poisson pascal
    # parameters
    dist = 'poisson pascal'
    # solve for local c to hit overall c=ν^2 value input
    c = (n * ν**2 - 1 - κ) / κ
    a = 1 / c
    θ = κ * c
    λ = n / κ   # poisson parameter for number of claims
    pois_pascal = np.real(ift( np.exp(λ * ((1 - θ * (fz - 1))**-a - 1))))
    pois_pascal = defuzz(pois_pascal)

    ans.loc[(dist, 'empirical'), :] = row(pois_pascal)
    # moments for the PP are different can't use Nmoms
    g = κ*λ*(2*c**2*κ**2 + 3*c*κ**2*λ + 3*c*κ**2 + 3*c*κ + κ**2*λ**2 + 3*κ**2*λ + κ**2 + 3*κ*λ + 3*κ + 1)
    g2 = n*(2*c**2*κ**2 + 3*c*n*κ + 3*c*κ**2 + 3*c*κ + n**2 + 3*n*κ + 3*n + κ**2 + 3*κ + 1)
    print(g-g2)
    temp = [λ * κ, n * (κ * (1 + c + λ) + 1), g]
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + [np.nan]  # note: hard to interpret this, but for FIXED cv of NB it tends to zero
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # sum of all errors is small
    print( ans.loc[(slice(None), 'diff'), :].abs().sum().sum() )
#     assert ans.loc[(slice(None), 'diff'), :].abs().sum().sum() < 1e-6

    # graphic
    df = pd.DataFrame(dict(x=x, poisson=p, pois_pascal=pois_pascal, negbin=nb, pig=pig, delaporte=delaporte, shifted_pig=shifted_pig))
    df = df.query(f' x < {xmax} ')
    f, ax = plt.subplots(1,2,figsize=(11,5))
    df.plot(kind='line', x='x', ax=ax[0], **kwds)
    ax[0].set_xlim(0, 4*n)
    df.plot(kind='line', x='x', logy=True, ax=ax[1], **kwds)

    # asymptotic moments
#     asNCV = np.sqrt(c)
#     asNSK = 2 * cv
#     delSK = 2 / np.sqrt(a)

    display(ans)
    return df, ans
```

```python
df, ans = mixing(90, 0.8, 0.75, 3.5, 16, xmax=500);
```

    0.0
    1.19825333877e-07

      EX
      EX2
      EX3
      mean
      CV
      Skew
      Asym Skew

      X
      type

      poisson
      empirical
      90
      8190
      753390
      90
      0.105409
      0.105409
      NaN

      theoretical
      90
      8190
      753390
      90
      0.105409
      0.105409
      inf

      diff
      -2.38698e-14
      -4.59632e-14
      -7.60503e-14
      -2.38698e-14
      7.9492e-14
      -8.29511e-11
      NaN

      neg bin
      empirical
      90
      13374
      2.76582e+06
      90
      0.806915
      1.60006
      NaN

      theoretical
      90
      13374
      2.76582e+06
      90
      0.806915
      1.60006
      1.6

      diff
      -9.25926e-13
      -1.05882e-11
      -9.59098e-11
      -9.25926e-13
      -1.1077e-11
      -3.35609e-10
      NaN

      delaporte
      empirical
      90
      13374
      4.55741e+06
      90
      0.806915
      6.27772
      NaN

      theoretical
      90
      13374
      4.55741e+06
      90
      0.806915
      6.27772
      6.4

      diff
      -8.22842e-12
      -3.14716e-10
      -5.43426e-09
      -8.22842e-12
      -3.78168e-10
      -8.67099e-09
      NaN

      pig
      empirical
      90
      13374
      3.06442e+06
      90
      0.806915
      2.37967
      NaN

      theoretical
      90
      13374
      3.06442e+06
      90
      0.806915
      2.37967
      2.4

      diff
      -2.36022e-12
      -4.55992e-11
      -6.07689e-10
      -2.36022e-12
      -5.18308e-11
      -1.70191e-09
      NaN

      shifted pig
      empirical
      90
      13374
      5.7518e+06
      90
      0.806915
      9.39616
      NaN

      theoretical
      90
      13374
      5.7518e+06
      90
      0.806915
      9.39616
      9.6

      diff
      -2.58713e-11
      -1.72955e-09
      -4.07769e-08
      -2.58713e-11
      -2.12732e-09
      -5.69821e-08
      NaN

      poisson pascal
      empirical
      90
      13284
      2.70286e+06
      90
      0.8
      1.53834
      NaN

      theoretical
      90
      13284
      2.70286e+06
      90
      0.8
      1.53834
      NaN

      diff
      -8.4921e-13
      -9.23861e-12
      -8.07002e-11
      -8.4921e-13
      -9.66105e-12
      -2.91806e-10
      NaN

```python
from scipy.special import kv

def sichel(μ, β, λ, log2, **kwds):
    def defuzz(x):
        x[np.abs(x) < 5e-16] = 0
        return x

    def expand_moms(f1, f2, f3, emp1, emp2, emp3):
        """
        turn factorial moments f1 f2 f3 into more informative stats
        """
        ex1 = f1
        ex2 = f2 + f1
        ex3 = f3 + 3 *  f2 + f1
        m = ex1
        v = ex2 - m*m
        sd = np.sqrt(v)
        cv = sd/m
        c3 = ex3 - 3*ex2*ex1 + 2*ex1**3
        γ = c3 / sd**3
        return pd.Series((ex1, emp1, ex2, emp2, ex3, emp3, m, v, sd, cv, c3, γ),
                         index=( 'EX', 'est EX', 'EX2', 'est EX2', 'EX3', 'est EX3',  'm', 'v', 'sd', 'cv', 'c3', 'γ'))

    # basic set up
    N = 1<<log2
    z = np.zeros(N)
    z[1] = 1
    fz = ft(z)
    x = np.arange(N, dtype=float)
    z[1] = 1
    fz = ft(z)
    kernel = (fz - 1)

    # sichel
    dist = 'sichel'
    factorial = [μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]
    print('params:', μ, β, λ)
    print('fact moms', factorial)
    inner = np.sqrt(1 - 2 * β * kernel)
    sichel = np.real(ift( inner**(-λ) * kv(λ, μ * inner / β) /  kv(λ, μ / β) ))
    print('sum of probs error', sichel.sum()-1)
    moms = [(x**k * sichel).sum() for k in (1,2,3)]
    em = expand_moms(*factorial, *moms)
    mw = MomentWrangler()
    mw.factorial = factorial
    print('em manual\n', em)
    print('mw.stats\n', mw.stats)
    # poisson for comparison
    dist = 'poisson'
    n = em[0]  # mean for Poisson comparison
    p = np.real(ift(np.exp(n * kernel)))
    p = defuzz(p)

    df = pd.DataFrame(dict(x=x, p=p, sichel=sichel))
    df.plot(kind='line', x='x')
    df.plot(kind='line', x='x', logy=True)

    return em, df
```

```python
em, df = sichel(100, 1.234, -0.5, 9)
display(df.head())
em
```

    params: 100 1.234 -0.5
    fact moms [100.0, 10123.400000000001, 1037476.8267999999]
    sum of probs error 0.0
    em manual
     EX         1.000000e+02
    est EX     1.000000e+02
    EX2        1.022340e+04
    est EX2    1.022340e+04
    EX3        1.067947e+06
    est EX3    1.067947e+06
    m          1.000000e+02
    v          2.234000e+02
    sd         1.494657e+01
    cv         1.494657e-01
    c3         9.270268e+02
    γ          2.776307e-01
    dtype: float64
    mw.stats
     ex      100.000000
    var     223.400000
    sd       14.946572
    cv        0.149466
    skew      0.277631
    dtype: float64

      x
      p
      sichel

      0
      0.0
      0.0
      1.374768e-16

      1
      1.0
      0.0
      1.345495e-16

      2
      2.0
      0.0
      1.359590e-16

      3
      3.0
      0.0
      1.336821e-16

      4
      4.0
      0.0
      1.356337e-16

    EX         1.000000e+02
    est EX     1.000000e+02
    EX2        1.022340e+04
    est EX2    1.022340e+04
    EX3        1.067947e+06
    est EX3    1.067947e+06
    m          1.000000e+02
    v          2.234000e+02
    sd         1.494657e+01
    cv         1.494657e-01
    c3         9.270268e+02
    γ          2.776307e-01
    dtype: float64

# Solving for Sichel Parameters

```python
from scipy.optimize import newton_krylov, broyden2
from scipy.optimize.nonlin import NoConvergence
```

```python
def f(arrIn):
    μ, β, λ = arrIn
    target = np.array([100.0, 10123.400000000001, 1037476.8267999999])
    return np.array([μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]) - target
```

```python
guess = np.array((90, 1.2, -0.4)) ## 100 1.234 -0.5
target = np.array([100.0, 10123.400000000001, 1037476.8267999999])
```

```python
f((100, 1.23, -0.5))
```

    (100, 1.23, -0.5)

    array([   0.    ,   -0.4   , -122.9568])

```python
newton_krylov(f, (101, 1.23, -0.5), verbose=False, f_rtol=1e-12)
```

    array([ 99.99999995,   1.234     ,  -0.49999996])

```python
%timeit newton_krylov(f, (101, 1.23, -0.5), verbose=False, f_rtol=1e-12)
```

    227 ms ± 9.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

```python
broyden2(f, (101, 1.23, -0.5), verbose=False, f_rtol=1e-9)
```

    array([ 100.00000041,    1.23400001,   -0.50000033])

```python
%timeit broyden2(f, (101, 1.23, -0.5), verbose=False, f_rtol=1e-9)
```

    58.7 ms ± 938 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

```python
f([ 99.99472192,   1.23393579,  -0.49569666])
```

    array([ -2.61492758e-07,  -4.18807758e-07,  -7.69186299e-05])

```python
def sichel_solver(n, ν, γ):
    """
    find method of moments parameters for Sichel to match EN, CVN and EN^3 as given CV ν OF THE MIXING dist
    and skewness γ of RESULTING distribution
    """
    # starting parameters = PIG estimates in Panjer Willmot p. format
    c = ν ** 2
    λ = -0.5
    μ = n
    β = ν**2 * n

    # implied non-central moments
    en1 = n
    en2 = n * (1 + (1 + c) * n)
    vn = n * (1 + c * n)
    en3 = γ * vn**1.5 + 3 * en2 * en1 - 2 * en1**3
    # target factorial moments
    f1 = en1
    f2 = en2 - en1
    f3 = en3 - 3 * en2 + 2 * en1
    target = np.array((f1, f2, f3))

    pig_fact = tuple((μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1, 2,3)))
    mw = MomentWrangler()
    mw.noncentral = (en1, en2, en3)
    print(f'parameters(μ, β, λ) =   {(μ, β, λ)}')
    print(f'factorial mos of PIG  = {pig_fact}')
    print(f'target factorial =                {target}')
    print(f'mw factorial =                    {mw.factorial}')

    tf = mw.stats
    mw.factorial = pig_fact
    df0 = pd.DataFrame({'target': tf, 'pig': mw.stats})
    display(df0)

    def f(arrIn):
        μ, β, λ = arrIn
#         if f.counter % 100 == 0:
#             print(f.counter, arrIn)
#         f.counter += 1
        return np.array([μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]) - target
#     f.counter = 0
    try:
        ans = broyden2(f, (μ, β, λ), verbose=False, iter=10000, f_rtol=1e-11) # , f_rtol=1e-9)  , line_search='wolfe'
    except NoConvergence as e:
        print('ERROR: broyden did not converge')
        print(e)
#     try:
#         ans = newton_krylov(f, (μ, β, λ)) # , f_rtol=1e-9)
#     except NoConvergence as e:
#         print('ERROR: newton_krylov did not converge')
#         print(e)
        raise e
    return ans
```

```python
params = sichel_solver(100, 0.45, 1.6)  # 1.31887
params
```

    parameters(μ, β, λ) =   (100, 20.25, -0.5)
    factorial mos of PIG  = (100.0, 12025.0, 1730518.7500000002)
    target factorial =                [  1.00000000e+02   1.20250000e+04   1.75805726e+06]
    mw factorial =                    (100, 12025.000000000002, 1758057.2557739802)

      target
      pig

      ex
      100.000000
      100.000000

      var
      2125.000000
      2125.000000

      sd
      46.097722
      46.097722

      cv
      0.460977
      0.460977

      skew
      1.600000
      1.318873

    array([ 100.79241123,   21.70137996,   -0.56782587])

```python
def sichel_solver2(n, ν, γ_mult):
    """
    find method of moments parameters for Sichel mixed with GIG whose skewness = γ_mult x IG(n, ν)
    """
    # starting parameters = IG estimates in Panjer Willmot p.282 (8.3.12) and (8.3.13) format
    c = ν ** 2
    λ = -0.5
    μ = n
    β = ν**2 * n
    ig_param = (μ, β, λ)
    # EG^3
    ig_fact = tuple((μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1, 2,3)))
    mw = MomentWrangler()
    mw.factorial = ig_fact
    γ_base = mw.stats['skew']
    sd_base = mw.stats['sd']
    eg, eg2, eg3 = mw.noncentral
    print(eg, eg2, eg3)
    γ = γ_base * γ_mult
    # adjust the third moment (mean and std dev unchanged)
    eg3_target = γ * sd_base**3 + 3 * eg2 * eg - 2 * eg**3
    mw.noncentral = (eg, eg2, eg3_target)
    target = np.array(mw.factorial)
    print(target)

    def f(arrIn):
        μ, β, λ = arrIn
        # mu and beta are positive...
        μ = np.exp(μ)
        β = np.exp(β)
        return np.array([μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]) - target

    try:
        ans = broyden2(f, (np.log(μ), np.log(β), λ), verbose=False, iter=10000, f_rtol=1e-11) # , f_rtol=1e-9)  , line_search='wolfe'
    except NoConvergence as e:
        print('ERROR: broyden did not converge')
        print(e)
        raise e
    μ, β, λ = ans
    ans = (np.exp(μ), np.exp(β), λ)
    print('ig', ig_param, 'gig', ans)
    return
```

```python
params = sichel_solver2(40, 0.5, 2.25)
```

    40.0 2040.0 130040.0
    [  4.00000000e+01   2.00000000e+03   1.40550000e+05]

    C:\Users\steve\Anaconda3\lib\site-packages\scipy\optimize\nonlin.py:1038: RuntimeWarning: invalid value encountered in true_divide
      d = v / df_norm**2
    C:\Users\steve\Anaconda3\lib\site-packages\ipykernel_launcher.py:31: RuntimeWarning: invalid value encountered in double_scalars

    ig (40, 10.0, -0.5) gig (367.39216307700769, 79.860604017327717, -7.6659421537149264)

```python
a=1; b=2; c = (a, b)
```

```python
c
```

    (1, 2)

```python
b=15
```

```python
c
```

    (1, 2)

```python
params
```

    (42.377214598771914, 11.426137497976219, -0.77424253232753837)

```python
em, df = sichel(*params, 9)
display(df.head())
em
```

    params: 367.392163077 79.8606040173 -7.66594215371
    fact moms [113.14371414717861, 14513.756456394529, 2137268.7582834424]
    sum of probs error 0.0
    em manual
     EX         1.131437e+02
    est EX     1.131266e+02
    EX2        1.462690e+04
    est EX2    1.461623e+04
    EX3        2.180923e+06
    est EX3    2.174646e+06
    m          1.131437e+02
    v          1.825400e+03
    sd         4.272470e+01
    cv         3.776145e-01
    c3         1.129163e+05
    γ          1.447836e+00
    dtype: float64
    mw.stats
     ex       113.143714
    var     1825.400120
    sd        42.724702
    cv         0.377614
    skew       1.447836
    dtype: float64

      x
      p
      sichel

      0
      0.0
      0.0
      6.299091e-07

      1
      1.0
      0.0
      6.174707e-07

      2
      2.0
      0.0
      6.052903e-07

      3
      3.0
      0.0
      5.933621e-07

      4
      4.0
      0.0
      5.816809e-07

    EX         1.131437e+02
    est EX     1.131266e+02
    EX2        1.462690e+04
    est EX2    1.461623e+04
    EX3        2.180923e+06
    est EX3    2.174646e+06
    m          1.131437e+02
    v          1.825400e+03
    sd         4.272470e+01
    cv         3.776145e-01
    c3         1.129163e+05
    γ          1.447836e+00
    dtype: float64

```python

```

```python

```

```python

```

```python

```

```python
from scipy.optimize import newton
```

```python
def sichel_solver3(n, ν, λ1):
    """
    find parameters for Sichel matching n and ν but with λ1 in place of λ=-0.5 (PIG)
    """
    # starting parameters = IG estimates in Panjer Willmot p.282 (8.3.12) and (8.3.13) format
    c = ν ** 2
    λ = -0.5
    μ = n
    β = ν**2 * n
    ig_param = (μ, β, λ)
    print('initial params', ig_param)
    # need to find β so that K_(λ+1)(mu/beta) / K_(λ)(mu/beta) = value for lambda=-0.5, will use NR
    target = kv(λ + 1, μ / β) / kv(λ, μ / β)
    print('target value', target)
    def f(β1):
        x = kv(λ1 + 1, μ / np.exp(β1)) / kv(λ1, μ / np.exp(β1))
#         print(np.exp(β1), x, x-target)
        return x - target
#         return kv(λ1 + 1, μ / β1) / kv(λ1, μ / β1) - target

    print('should be close to zero: ', f(np.log(β) -2), f(np.log(β)), f(np.log(β) + 2))

    # newton(func, x0, fprime=None, args=(), tol=1.48e-08, maxiter=50, fprime2=None)[source]
    β1 = newton(f, np.log(β), maxiter=1000)

    print('ig', ig_param, 'gig', (μ, β1, λ1))
    return ans
```

```python
sichel_solver3(40, 0.5, -0.45)
```

```python
n = 50
ν = 0.4
c = ν ** 2
λ = -0.5
μ = n
β = c * n
base = tuple((μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1, 2,3)))
mw = MomentWrangler()
mw.factorial = base
print(base)
print(mw.stats)
df = pd.DataFrame(mw.stats, columns=[-0.5]).T
for l in np.linspace(-10, 10, 25):
    mw.factorial = tuple((μ**r * kv(l + r, μ / β) / kv(l, μ / β) for r in (1, 2,3)))
    df.loc[l, :] = mw.stats
df
```

    (50.0, 2900.0, 194600.00000000003)
    ex       50.000000
    var     450.000000
    sd       21.213203
    cv        0.424264
    skew      1.136609
    dtype: float64

      ex
      var
      sd
      cv
      skew

      -0.500000
      50.000000
      450.000000
      21.213203
      0.424264
      1.136609

      -10.000000
      15.525143
      38.874460
      6.234939
      0.401603
      0.942069

      -9.166667
      16.760971
      45.730620
      6.762442
      0.403464
      0.988131

      -8.333333
      18.180399
      54.486704
      7.381511
      0.406015
      1.035233

      -7.500000
      19.819528
      65.774898
      8.110173
      0.409201
      1.081656

      -6.666667
      21.721799
      80.442192
      8.968957
      0.412901
      1.125164

      -5.833333
      23.938791
      99.606586
      9.980310
      0.416910
      1.163101

      -5.000000
      26.530518
      124.709013
      11.167319
      0.420924
      1.192634

      -4.166667
      29.564757
      157.542226
      12.551583
      0.424545
      1.211132

      -3.333333
      33.114905
      200.228187
      14.150201
      0.427306
      1.216630

      -2.500000
      37.255910
      255.111283
      15.972203
      0.428716
      1.208245

      -1.666667
      42.058197
      324.545519
      18.015147
      0.428339
      1.186401

      -0.833333
      47.580180
      410.587153
      20.262950
      0.425870
      1.152785

      0.000000
      53.860731
      514.654095
      22.685989
      0.421197
      1.110018

      0.833333
      60.913513
      637.253820
      25.243887
      0.414422
      1.061173

      1.666667
      68.724863
      777.878852
      27.890480
      0.405828
      1.009272

      2.500000
      77.255910
      935.111283
      30.579589
      0.395822
      0.956913

      3.333333
      86.448238
      1106.894854
      33.270029
      0.384855
      0.906062

      4.166667
      96.231424
      1290.875559
      35.928757
      0.373358
      0.858027

      5.000000
      106.530518
      1484.709013
      38.531922
      0.361698
      0.813529

      5.833333
      117.272124
      1686.273252
      41.064258
      0.350162
      0.772847

      6.666667
      128.388465
      1893.775525
      43.517531
      0.338952
      0.735954

      7.500000
      139.819528
      2105.774898
      45.888723
      0.328200
      0.702640

      8.333333
      151.513732
      2321.153371
      48.178350
      0.317980
      0.672600

      9.166667
      163.427638
      2539.063954
      50.389125
      0.308327
      0.645496

      10.000000
      175.525143
      2758.874460
      52.524989
      0.299245
      0.620995

```python
sichel_solver3(40, 0.5, -0.5)
```

    ig (40, 10.0, -0.5) gig (40, -0.5, 10.00055)

    C:\Users\steve\Anaconda3\lib\site-packages\scipy\optimize\zeros.py:173: RuntimeWarning: Tolerance of 0.0010999999999992127 reached
      warnings.warn(msg, RuntimeWarning)

      EX
      EX2
      EX3
      mean
      CV
      Skew
      Asym Skew

      X
      type

      poisson
      empirical
      100
      10100
      1.0301e+06
      100
      0.1
      0.1
      NaN

      theoretical
      100
      10100
      1030100
      100
      0.1
      0.1
      inf

      diff
      -2.53131e-14
      -4.82947e-14
      -7.88258e-14
      -2.53131e-14
      1.16129e-13
      -9.80618e-11
      NaN

      neg bin
      empirical
      100
      12125
      1.72569e+06
      100
      0.460977
      0.900261
      NaN

      theoretical
      100
      12125
      1.72569e+06
      100
      0.460977
      0.900261
      0.9

      diff
      -2.4547e-13
      -1.38201e-12
      -7.6894e-12
      -2.4547e-13
      -2.54252e-12
      -9.16779e-11
      NaN

      delaporte
      empirical
      100
      12125
      1.8077e+06
      100
      0.460977
      1.73749
      NaN

      theoretical
      100
      12125
      1.8077e+06
      100
      0.460977
      1.73749
      1.8

      diff
      -4.77396e-13
      -4.42502e-12
      -3.8297e-11
      -4.77396e-13
      -9.90064e-12
      -2.87677e-10
      NaN

      pig
      empirical
      100
      12125
      1.76669e+06
      100
      0.460977
      1.31887
      NaN

      theoretical
      100
      12125
      1.76669e+06
      100
      0.460977
      1.31887
      1.35

      diff
      -4.43423e-13
      -3.82205e-12
      -3.14663e-11
      -4.43423e-13
      -8.37419e-12
      -3.0434e-10
      NaN

      shifted pig
      empirical
      100
      12125
      1.88971e+06
      100
      0.460977
      2.57471
      NaN

      theoretical
      100
      12125
      1.88971e+06
      100
      0.460977
      2.57471
      2.7

      diff
      -1.17995e-12
      -1.90861e-11
      -2.62924e-10
      -1.17995e-12
      -4.77191e-11
      -1.55906e-09
      NaN

      poisson pascal
      empirical
      100
      12025
      1.68508e+06
      100
      0.45
      0.851372
      NaN

      theoretical
      100
      12025
      1.68508e+06
      100
      0.45
      0.851372
      NaN

      diff
      -2.21934e-13
      -1.17428e-12
      -6.19738e-12
      -2.21934e-13
      -2.16827e-12
      -7.96727e-11
      NaN

      sichel
      empirical
      99.3606
      12123.8
      1.79883e+06
      99.3606
      0.477526
      1.37464
      NaN

      theoretical
      100
      12125
      1.835e+06
      100
      0.460977
      2.01621
      2.09962

      diff
      -0.00639415
      -0.000101381
      -0.0197139
      -0.00639415
      0.0358998
      -0.318204
      NaN

```python
params = sichel_solver2(50, 0.4, 1.25)
```

    50.0 2950.0 203350.0
    [  5.00000000e+01   2.90000000e+03   1.97312500e+05]
    ig (50, 8.000000000000002, -0.5) gig [ 49.84045196   8.52776965  -0.51055233]

```python
em, df = sichel(*params, 9)
display(df.head())
em
```

    params: 49.8404519587 8.52776964541 -0.510552325296
    fact moms [49.757170814717895, 2899.4332657554282, 197255.5974597774]
    sum of probs error -8.881784197e-16
    em manual
     EX             49.757171
    est EX         49.757171
    EX2          2949.190437
    est EX2      2949.190436
    EX3        206003.654428
    est EX3    206003.654346
    m              49.757171
    v             473.414389
    sd             21.758088
    cv              0.437285
    c3          12148.760863
    γ               1.179424
    dtype: float64
    mw.stats
     ex       49.757171
    var     473.414389
    sd       21.758088
    cv        0.437285
    skew      1.179424
    dtype: float64

      x
      p
      sichel

      0
      0.0
      0.0
      5.743692e-09

      1
      1.0
      0.0
      6.734179e-08

      2
      2.0
      0.0
      4.106721e-07

      3
      3.0
      0.0
      1.736739e-06

      4
      4.0
      0.0
      5.729351e-06

    EX             49.757171
    est EX         49.757171
    EX2          2949.190437
    est EX2      2949.190436
    EX3        206003.654428
    est EX3    206003.654346
    m              49.757171
    v             473.414389
    sd             21.758088
    cv              0.437285
    c3          12148.760863
    γ               1.179424
    dtype: float64

# Combine Sichel in with mixing function

```python
def mixing(n, ν, f, κ, γ_mult, log2, xmax=500, **kwds):
    """
    n = E(N) = expected claim count
    ν = CV(mixing) = asymptotic CV of any compound aggregate whose severity has a second moment
    f = proportion of certain claims, 0 <= f < 1, higher f corresponds to greater skewnesss
    κ = claims per occurrence
    γ_mult = adjust skewness of Sichel above/below standard PIG
    """

    def defuzz(x):
        x[np.abs(x) < 5e-16] = 0
        return x

    ma = lambda x : list(MomentAggregator._moments_to_mcvsk(*x))

    def row(ps):
        moms = [(x**k * ps).sum() for k in (1,2,3)]
        stats = ma(moms)
        return moms + stats + [np.nan]

    def Nmoms(n, c, g):
        """
        c=Var(G), g=E(G^3) return EN, EN2, EN3
        """
        return [n, n * (1 + (1 + c) * n), n * (1 + n * (3*(1+c) + n * g))]

    def asy_skew(g, c):
        """
        asymptotic skewnewss
        """
        return [(g - 3 * c - 1) / c**1.5]

    ans = pd.DataFrame(columns=['X', 'type', 'EX', 'EX2', 'EX3', 'mean', 'CV', 'Skew',  'Asym Skew'])
    ans = ans.set_index(['X', 'type'])

    N = 1 << log2
    x = np.arange(N, dtype=float)
    z = np.zeros(N)
    z[1] = 1
    fz = ft(z)

    # build poisson for comparison
    dist = 'poisson'
    kernel = n * (fz - 1)
    p = np.real(ift(np.exp(kernel)))
    p = defuzz(p)

    # for poisson c=0 and g=1 (the "mixing" distribution is G identically equal to 1)
    ans.loc[(dist, 'empirical'), :] = row(p)
    temp = Nmoms(n, 0, 1)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + [np.inf]
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # negative binomial
    # Var(G) = c, E(G)=1 so Var(G) = ν^2 = c
    # wikipedia / scipy stats use a and θ for shape and scale
    dist = 'neg bin'
    c = ν * ν   # contagion
    a = 1 / c
    θ = c
    # E(G^3): skew(G) = skew(G') = γ, so E(G-EG)^3 = γν^3, so EG^3 = γν^3 + 3(1+c) - 2 = γν^3 + 3c + 1
    # for Gamma skew = 2 / sqrt(a) = 2ν
    g = 1 + 3*c + 2*c*c
    nb = np.real(ift( (1 - θ * kernel) ** -a))
    nb = defuzz(nb)

    ans.loc[(dist, 'empirical'), :] = row(nb)
    # this row is generic: it applies to all distributions
    temp = Nmoms(n, c, g)   # [n, n * (1 + (1 + c) * n), n*(1 + n * (3*(1+c) + n * g))] # n**3*g + 3*n**2*(1+c)+n]
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # delaporte G = f + G'
    dist = 'delaporte'
    c = ν * ν   # contagion
    a = ((1 - f) / ν)**2
    θ = (1 - f) / a
    g = 2 * ν**4 / (1-f) + 3 * c + 1
    delaporte = np.real(ift( np.exp(f * kernel) *  (1 - θ * kernel) ** -a))
    delaporte = defuzz(delaporte)

    ans.loc[(dist, 'empirical'), :] = row(delaporte)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # pig
    dist = 'pig'
    c = ν * ν   # contagion
    μ = c
    λ = 1 / μ
    # our param (λ, μ) --> (λ, λμ) in Mathematica and hence skew = γ = 3 * sqrt(μ) in scip py parameterization
    γ = 3 * np.sqrt(μ)
    g = γ * ν**3 + 3 * c + 1
    pig = np.real(ift( np.exp( 1/μ * (1 - np.sqrt(1 - 2 * μ**2 * λ * kernel)) )))
    pig = defuzz(pig)
#     print(f'PIG parameters μ={μ} and λ={λ}, g={g}, γ={γ}')
    ans.loc[(dist, 'empirical'), :] = row(pig)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # shifted pig
    dist = 'shifted pig'
    c = ν * ν   # contagion
    μ = c / (1 - f)**2
    λ = (1 - f) / μ
    γ = 3 * np.sqrt(μ)
    g = γ * ν**3 + 3 * c + 1
    shifted_pig = np.real(ift(  np.exp(f * kernel) * np.exp( 1/μ * (1 - np.sqrt(1 - 2 * μ**2 * λ * kernel)))))
    shifted_pig = defuzz(shifted_pig)

    ans.loc[(dist, 'empirical'), :] = row(shifted_pig)
    temp = Nmoms(n, c, g)
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    #  poisson pascal
    # parameters
    dist = 'poisson pascal'
    # solve for local c to hit overall c=ν^2 value input
    c = (n * ν**2 - 1 - κ) / κ
    a = 1 / c
    θ = κ * c
    λ = n / κ   # poisson parameter for number of claims
    pois_pascal = np.real(ift( np.exp(λ * ((1 - θ * (fz - 1))**-a - 1))))
    pois_pascal = defuzz(pois_pascal)

    ans.loc[(dist, 'empirical'), :] = row(pois_pascal)
    # moments for the PP are different can't use Nmoms
    g = κ*λ*(2*c**2*κ**2 + 3*c*κ**2*λ + 3*c*κ**2 + 3*c*κ + κ**2*λ**2 + 3*κ**2*λ + κ**2 + 3*κ*λ + 3*κ + 1)
    g2 = n*(2*c**2*κ**2 + 3*c*n*κ + 3*c*κ**2 + 3*c*κ + n**2 + 3*n*κ + 3*n + κ**2 + 3*κ + 1)
    assert np.allclose(g, g2)
    temp = [λ * κ, n * (κ * (1 + c + λ) + 1), g]
    ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + [np.nan]  # note: hard to interpret this, but for FIXED cv of NB it tends to zero
    ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # sichel: find sichel to increase mixing skewness indicated amount from IG skewness
    dist = 'sichel'
    # starting parameters = IG estimates in Panjer Willmot p.282 (8.3.12) and (8.3.13) format
def sichel_solver2(n, ν, γ_mult):
    """
    find method of moments parameters for Sichel mixed with GIG whose skewness = γ_mult x IG(n, ν)
    """
    # starting parameters = IG estimates in Panjer Willmot p.282 (8.3.12) and (8.3.13) format
    c = ν ** 2
    λ = -0.5
    μ = n
    β = ν**2 * n
    ig_param = (μ, β, λ)
    # EG^3
    ig_fact = tuple((μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1, 2,3)))
    mw = MomentWrangler()
    mw.factorial = ig_fact
    γ_base = mw.stats['skew']
    sd_base = mw.stats['sd']
    eg, eg2, eg3 = mw.noncentral
    print(eg, eg2, eg3)
    γ = γ_base * γ_mult
    # adjust the third moment (mean and std dev unchanged)
    eg3_target = γ * sd_base**3 + 3 * eg2 * eg - 2 * eg**3
    mw.noncentral = (eg, eg2, eg3_target)
    target = np.array(mw.factorial)
    print(target)

    def f(arrIn):
        μ, β, λ = arrIn
        # mu and beta are positive...
        μ = np.exp(μ)
        β = np.exp(β)
        return np.array([μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]) - target

    try:
        ans = broyden2(f, (np.log(μ), np.log(β), λ), verbose=False, iter=10000, f_rtol=1e-11) # , f_rtol=1e-9)  , line_search='wolfe'
    except NoConvergence as e:
        print('ERROR: broyden did not converge')
        print(e)
        raise e
    μ, β, λ = ans
    ans = (np.exp(μ), np.exp(β), λ)
    print('ig', ig_param, 'gig', ans)
    return ans

    # if parameters found...
    if add_sichel:
        print('starting params:', μ, β, λ)
        print('\nfit params:    ', *params)
        μ, β, λ = params
        kernel = (fz - 1)  # not n*(fz-1)
        # theoretic factorial moments of the **mixing** distribution
        mw.factorial = [μ**r * kv(λ + r, μ / β) / kv(λ, μ / β) for r in (1,2,3)]
        print('Actual vs. target: ', mw.factorial, target)
        print('stats\n', mw.stats)
        # compute density
        inner = np.sqrt(1 - 2 * β * kernel)
        sichel = np.real(ift( inner**(-λ) * kv(λ, μ * inner / β) /  kv(λ, μ / β) ))
        sichel = defuzz(sichel)

        ans.loc[(dist, 'empirical'), :] = row(sichel)
        g = mw.noncentral[2] / n**3  # non central from factorial
        temp = Nmoms(n, c, g)
        ans.loc[(dist, 'theoretical'), :] = temp + ma(temp) + asy_skew(g, c)
        ans.loc[(dist, 'diff'), :] = ans.loc[(dist, 'empirical'), :] / ans.loc[(dist, 'theoretical'), :] - 1

    # ---------------------------------------------------------------------
    # sum of all errors is small
    print( ans.loc[(slice(None), 'diff'), :].abs().sum().sum() )
    # assert ans.loc[(slice(None), 'diff'), :].abs().sum().sum() < 1e-6

    # graphics
    df = pd.DataFrame(dict(x=x, poisson=p, pois_pascal=pois_pascal, negbin=nb,delaporte=delaporte,  pig=pig,
                           shifted_pig=shifted_pig, sichel=sichel))
    df = df.query(f' x < {xmax} ')
    df = df.set_index('x')
    axiter = agg.axiter_factory(None, 12, aspect=1.414, nr=6)
    all = ['poisson', 'negbin', 'delaporte', 'pig', 'shifted_pig', 'pois_pascal', 'sichel']
    for vars in [all,
                 ['poisson', 'negbin', 'pig', ],
                 ['poisson', 'negbin', 'delaporte', ],
                 ['poisson', 'pig', 'shifted_pig',  'sichel'],
                 ['poisson', 'negbin', 'pois_pascal'],
                 ['poisson', 'delaporte', 'shifted_pig', 'sichel'],
                ]:
        # keep the same colors
        pal = [sns.color_palette("Paired", 7)[i] for i in [all.index(j) for j in vars]]
        df[vars].plot(kind='line', ax=next(axiter), color=pal )
        axiter.ax.set_xlim(0, 4*n)
        df[vars].plot(kind='line', logy=True, ax=next(axiter), legend=None, color=pal )
    axiter.tidy()
    display(ans.unstack())
    return df, ans
```

```python
df, ans = mixing(n=100, ν=0.45, f=0.5, κ=1.25, γ_mult=1.25, log2=16, xmax=500)
```

    starting params: 100 20.25 -0.5

    fit params:     100.300742093 21.8457473324 -0.547552369405
    Actual vs. target:  [99.360585322225745, 12024.410166506066, 1762655.576483469] [  1.00000000e+02   1.20250000e+04   1.76281719e+06]
    stats
     ex        99.360585
    var     2251.244836
    sd        47.447285
    cv         0.477526
    skew       1.374644
    dtype: float64
    0.386707268469

      EX
      EX2
      EX3
      mean
      CV
      Skew
      Asym Skew

      type
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff
      empirical
      theoretical
      diff

      X

      poisson
      100
      100
      -2.53131e-14
      10100
      10100
      -4.82947e-14
      1.0301e+06
      1030100
      -7.88258e-14
      100
      100
      -2.53131e-14
      0.1
      0.1
      1.16129e-13
      0.1
      0.1
      -9.80618e-11
      NaN
      inf
      NaN

      neg bin
      100
      100
      -2.4547e-13
      12125
      12125
      -1.38201e-12
      1.72569e+06
      1.72569e+06
      -7.6894e-12
      100
      100
      -2.4547e-13
      0.460977
      0.460977
      -2.54252e-12
      0.900261
      0.900261
      -9.16779e-11
      NaN
      0.9
      NaN

      delaporte
      100
      100
      -4.77396e-13
      12125
      12125
      -4.42502e-12
      1.8077e+06
      1.8077e+06
      -3.8297e-11
      100
      100
      -4.77396e-13
      0.460977
      0.460977
      -9.90064e-12
      1.73749
      1.73749
      -2.87677e-10
      NaN
      1.8
      NaN

      pig
      100
      100
      -4.43423e-13
      12125
      12125
      -3.82205e-12
      1.76669e+06
      1.76669e+06
      -3.14663e-11
      100
      100
      -4.43423e-13
      0.460977
      0.460977
      -8.37419e-12
      1.31887
      1.31887
      -3.0434e-10
      NaN
      1.35
      NaN

      shifted pig
      100
      100
      -1.17995e-12
      12125
      12125
      -1.90861e-11
      1.88971e+06
      1.88971e+06
      -2.62924e-10
      100
      100
      -1.17995e-12
      0.460977
      0.460977
      -4.77191e-11
      2.57471
      2.57471
      -1.55906e-09
      NaN
      2.7
      NaN

      poisson pascal
      100
      100
      -2.21934e-13
      12025
      12025
      -1.17428e-12
      1.68508e+06
      1.68508e+06
      -6.19738e-12
      100
      100
      -2.21934e-13
      0.45
      0.45
      -2.16827e-12
      0.851372
      0.851372
      -7.96727e-11
      NaN
      NaN
      NaN

      sichel
      99.3606
      100
      -0.00639415
      12123.8
      12125
      -0.000101381
      1.79883e+06
      1.835e+06
      -0.0197139
      99.3606
      100
      -0.00639415
      0.477526
      0.460977
      0.0358998
      1.37464
      2.01621
      -0.318204
      NaN
      2.09962
      NaN

```python

```

```python

```

## Poisson Pascal Calculations with MGF for Moments

```python
import sympy as sym
sym.init_printing(use_unicode=True)
sym.init_printing(use_latex=True)
```

```python
x = sym.Symbol('x', real=True)
λ = sym.Symbol('λ', real=True)
κ = sym.Symbol('κ', real=True)
c = sym.Symbol('c', real=True)
n = sym.Symbol('n', real=True)
```

```python
sym.simplify(sym.diff(sym.exp((n/κ)*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 1).subs(x, 0))
```

```python
sym.simplify(sym.diff(sym.exp((n/κ)*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 2).subs(x, 0))
```

```python
sym.simplify(sym.diff(sym.exp(λ*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 3).subs(x, 0))
```

```python
print(sym.simplify(sym.diff(sym.exp(λ*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 3).subs(x, 0)))
```

    κ*λ*(2*c**2*κ**2 + 3*c*κ**2*λ + 3*c*κ**2 + 3*c*κ + κ**2*λ**2 + 3*κ**2*λ + κ**2 + 3*κ*λ + 3*κ + 1)

```python
sym.latex(sym.simplify(sym.diff(sym.exp(λ*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 3).subs(x, 0)))
```

    'κ λ \\left(2 c^{2} κ^{2} + 3 c κ^{2} λ + 3 c κ^{2} + 3 c κ + κ^{2} λ^{2} + 3 κ^{2} λ + κ^{2} + 3 κ λ + 3 κ + 1\\right)'

```python
en3 = sym.simplify(sym.diff(sym.exp((n/κ)*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 3).subs(x, 0))
en2 = sym.simplify(sym.diff(sym.exp((n/κ)*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 2).subs(x, 0))
en1 = sym.simplify(sym.diff(sym.exp((n/κ)*((1 - κ * c *(sym.exp(x) - 1))**(-1/c) - 1)), x, 1).subs(x, 0))
```

```python
en1, en2, en3
```

```python
cent = sym.simplify(en3 - 3 * en2 * en1 + 2 * en1**3)
cent
```

```python
sym.limit(cent / (en2 - en1*en1)**(3/2), n, sym.oo)
```

```python
sym.simplify(en2 - en1*en1)
```

```python
print(en3)
```

    n*(2*c**2*κ**2 + 3*c*n*κ + 3*c*κ**2 + 3*c*κ + n**2 + 3*n*κ + 3*n + κ**2 + 3*κ + 1)

```python
f = sym.Function('f')
t = sym.Symbol('t')
```

```python
display(sym.diff(f(sym.exp(t)), t, 2))
```

$$\left(e^{t} \frac{d^{2}}{d e^{t}^{2}}  f{\left (e^{t} \right )} + \left. \frac{d}{d \xi_{1}} f{\left (\xi_{1} \right )} \right|_{\substack{ \xi_{1}=e^{t} }}\right) e^{t}$$

```python
sym.diff(sym.exp(t), t)
```

# Moment Wrangler

```python
class MomentWrangler(object):

    def __init__(self):
        self._central = None
        self._noncentral = None
        self._factorial = None

    @property
    def central(self):
        return self._central

    @central.setter
    def central(self, value):
        self._central = value
        ex1, ex2, ex3 = value
        # p 43, 1.241
        self._noncentral = (ex1, ex2 + ex1**2, ex3 + 3 * ex2 * ex1 + ex1**3)
        self._make_factorial()

    @property
    def noncentral(self):
        return self._noncentral

    @noncentral.setter
    def noncentral(self, value):
        self._noncentral = value
        self._make_factorial()
        self._make_central()

    @property
    def factorial(self):
        return self._factorial

    @factorial.setter
    def factorial(self, value):
        self._factorial = value
        ex1, ex2, ex3 = value
        # p 44, 1.248
        self._noncentral = (ex1, ex2 + ex1, ex3 + 3 * ex2 + ex1)
        self._make_central()

    @property
    def stats(self):
        m, v, c3 = self._central
        sd = np.sqrt(v)
        if m == 0:
            cv = np.nan
        else:
            cv = sd / m
        if sd == 0:
            skew = np.nan
        else:
            skew = c3 / sd**3
#         return pd.Series((m, v, sd, cv, skew), index=('EX', 'Var(X)', 'SD(X)', 'CV(X)', 'Skew(X)'))
        # shorter names are better
        return pd.Series((m, v, sd, cv, skew), index=('ex', 'var', 'sd', 'cv', 'skew'))

    def _make_central(self):
        ex1, ex2, ex3 = self._noncentral
        # p 42, 1.240
        self._central = (ex1, ex2 - ex1**2, ex3 - 3 * ex2 * ex1 + 2* ex1**3)

    def _make_factorial(self):
        """ add factorial from central """
        ex1, ex2, ex3 = self._noncentral
        # p. 43, 1.245 (below)
        self._factorial = (ex1, ex2 - ex1, ex3 - 3 * ex2 + 2 * ex1)

```

```python
mw = MomentWrangler()
```

```python
mw.central = (1, 10, 100)
```

```python
mw.noncentral
```

    (1, 11, 131)

```python
mw.factorial
```

    (1, 10, 100)

```python
mw.central
```

    (1, 10, 100)

```python
mw.stats['cv']
```

    3.1622776601683795

```python
mw.stats = 12
```

```python

```


```python
import open3d as o3d
import open3d.core as o3c

if o3d.__DEVICE_API__ == 'cuda':
    import open3d.cuda.pybind.t.pipelines.registration as treg
else:
    import open3d.cpu.pybind.t.pipelines.registration as treg

import numpy as np
import sys
import os
import time

# monkey patches visualization and provides helpers to load geometries
sys.path.append('..')
import open3d_tutorial as o3dtut
# change to True if you want to interact with the visualization windows
o3dtut.interactive = not "CI" in os.environ
```

# Robust Kernel

This tutorial demonstrates the use of robust kernels in the context of outlier rejection. For this particular tutorial, we will be using the ICP (Iterative Closest Point) registration algorithm as the _target_ problem where we want to deal with outliers. Even so, the theory applies to any given optimization problem and not just for ICP. For the moment the robust kernels have been only implemented for the `PointToPlane` ICP.

The notation and some of the kernels implemented in `Open3D` has been inspired by the publication "Analysis of Robust Functions for Registration Algorithms" [\[Babin2019\]](../reference.html#babin2019)

**Note:**

This tutorial and the original implementation of the robust kernels in `Open3D` was contributed by **Ignacio Vizzo** and **Cyrill Stachniss** from the University of Bonn.

## Point-to-plane ICP using Robust Kernels

The standard point-to-plane ICP algorithm [\[ChenAndMedioni1992\]](../reference.html#chenandmedioni1992) minimizes this objective function:

\begin{equation}
E(\mathbf{T}) = \sum_{(\mathbf{p},\mathbf{q})\in\mathcal{K}}\big((\mathbf{p} - \mathbf{T}\mathbf{q})\cdot\mathbf{n}_{\mathbf{p}}\big)^{2},
\end{equation}

where $\mathbf{n}_{\mathbf{p}}$ is the normal of point $\mathbf{p}$ and $\mathcal{K}$ is the correspondence set between the target point cloud $\mathbf{P}$, and source point cloud $\mathbf{Q}$.

If we call $r_i(\mathbf{T})$ is $i^\mathrm{th}$ the residual, for a given pair of correspondences $(\mathbf{p},\mathbf{q})\in\mathcal{K}$ we can rewrite the objective function as.

\begin{equation}
E(\mathbf{T}) = \sum_{(\mathbf{p},\mathbf{q})\in\mathcal{K}}\big((\mathbf{p} - \mathbf{T}\mathbf{q})\cdot\mathbf{n}_{\mathbf{p}}\big)^{2} = \sum_{i=1}^{N} \big({r_i(\mathbf{T})}\big)^2
\end{equation}

The optimization problem above can also be solved by using the iteratively reweighted least-squares (IRLS) approach, which solves a sequence of weighted least squares problems:

\begin{equation}
E(\mathbf{T}) =  \sum_{i=1}^{N} w_i \big({r_i(\mathbf{T})}\big)^2
\end{equation}

### Outlier Rejection with Robust Kernels

The main idea of a robust loss is to downweight large residuals that are assumed to be caused from outliers such that their influence on the solution is reduced. This is achieved by optimizing $E(\mathbf{T})$ as:

\begin{equation}
E(\mathbf{T}) = \sum_{(\mathbf{p},\mathbf{q})\in\mathcal{K}}\rho\big((\mathbf{p} - \mathbf{T}\mathbf{q})\cdot\mathbf{n}_{\mathbf{p}}\big) = \sum_{i=1}^{N} \rho\big({r_i(\mathbf{T})}\big),
\end{equation}

where $\rho(r)$ is also called the robust loss or kernel.

We can see that exists a relation between the optimization formulation in IRLS and the one that uses the robust loss function. By setting the weight $w_i= \frac{1}{r_i(\mathbf{T})}\rho'(r_i(\mathbf{T}))$, we can solve the robust loss optimization problem using the existing techniques for weighted least-squares. Therefore, we can minimize the objective function using Gauss-Newton and determine increments by iteratively solving:

\begin{align}
\left(\mathbf{J}^\top \mathbf{W} \mathbf{J}\right)^{-1}\mathbf{J}^\top\mathbf{W}\vec{r},
\end{align}

where $\mathbf{W} \in \mathbb{R}^{N\times N}$ is a diagonal matrix containing weights $w_i$ for each residual $r_i$

### How to use Robust Kernels in Open3D

`icp`, `multi_scale_icp` can be called with a parameter `TransformationEstimationPointToPlane(loss)` or `TransformationEstimationForColoredICP(loss)`. Where `loss` is a given loss function(also called robust kernel).

Internally, `TransormationEstimationPointToPlane(loss)` implements a function to compute the weighted residuals and Jacobian matrices of the point-to-plane ICP objective according to the provided robust kernel.

## Vanilla ICP vs Robust ICP

To better show the advantages of using robust kernels in the registration, we add some artificially generated gaussian noise to the source point cloud.

```python
# Overriding visualization function, according to best camera view for colored-icp sample data.
def draw_registration_result(source, target, transformation):
    source_temp = source.clone()
    target_temp = target.clone()

    source_temp.transform(transformation)

    # This is patched version for tutorial rendering.
    # Use `draw` function for you application.
    o3d.visualization.draw_geometries(
        [source_temp.to_legacy(),
         target_temp.to_legacy()],
        zoom=0.4459,
        front=[0.9288, -0.2951, -0.2242],
        lookat=[1.6784, 2.0612, 1.4451],
        up=[-0.3402, -0.9189, -0.1996])
```

```python
demo_icp_pcds = o3d.data.DemoICPPointClouds()
source = o3d.t.io.read_point_cloud(demo_icp_pcds.paths[0])
target = o3d.t.io.read_point_cloud(demo_icp_pcds.paths[1])
```

```python
def apply_noise(pcd, mu, sigma):
    device = pcd.point["positions"].device
    noisy_pcd = pcd.cpu().clone()
    noisy_pcd.point["positions"] += o3d.core.Tensor(
        np.random.normal(mu, sigma, size=noisy_pcd.point["positions"].shape),
        noisy_pcd.point["positions"].dtype)
    return noisy_pcd

mu, sigma = 0, 0.1  # mean and standard deviation
source_noisy = apply_noise(source, mu, sigma)

print("Source PointCloud + noise:")
o3d.visualization.draw_geometries([source_noisy.to_legacy()],
                                  zoom=0.4459,
                                  front=[0.353, -0.469, -0.809],
                                  lookat=[2.343, 2.217, 1.809],
                                  up=[-0.097, -0.879, 0.467])
```

### Vanilla ICP

We inspect how the results will look if we use the exact same parameters that were used in the above examples of ICP.

```python
# Initial alignment or source to target transform.
init_source_to_target = np.asarray([[0.862, 0.011, -0.507, 0.5],
                                    [-0.139, 0.967, -0.215, 0.7],
                                    [0.487, 0.255, 0.835, -1.4],
                                    [0.0, 0.0, 0.0, 1.0]])

criteria = treg.ICPConvergenceCriteria(relative_fitness=0.000001,
                                       relative_rmse=0.000001,
                                       max_iteration=50)
```

```python
estimation = treg.TransformationEstimationPointToPlane()
```

```python
max_correspondence_distance = 0.02
```

```python
print("Vanilla point-to-plane ICP, max_correspondence_distance={}:".format(
    max_correspondence_distance))
s = time.time()

reg_point_to_plane = treg.icp(source_noisy, target, max_correspondence_distance,
                              init_source_to_target, estimation)

icp_time = time.time() - s

print("Time taken by Point-To-Plane ICP: ", icp_time)
print("Fitness: ", reg_point_to_plane.fitness)
print("Inlier RMSE: ", reg_point_to_plane.inlier_rmse)

draw_registration_result(source, target, reg_point_to_plane.transformation)
```

### Tuning Vanilla ICP

Given the fact that we are now dealing with gaussian noise, we might try to increase the threshold to search for nearest neighbors with the aim of improving the registration result.

We can see that under these condtions and without a robust kernel, the traditional ICP has no chance to deal with  outliers.

```python
estimation = treg.TransformationEstimationPointToPlane()
```

```python
max_correspondence_distance = 0.5

voxel_size = 0.02
```

```python
print("Vanilla point-to-plane ICP, max_correspondence_distance={}:".format(
    max_correspondence_distance))

s = time.time()

reg_point_to_plane = treg.icp(source, target, max_correspondence_distance,
                              init_source_to_target, estimation, criteria,
                              voxel_size)

icp_time = time.time() - s

print("Time taken by Point-To-Plane ICP: ", icp_time)
print("Fitness: ", reg_point_to_plane.fitness)
print("Inlier RMSE: ", reg_point_to_plane.inlier_rmse)

draw_registration_result(source, target, reg_point_to_plane.transformation)
```

### Robust ICP

Using the same `max_correspondence_distance=0.5` **and** a robust kernel, we can properly register the two point clouds:

```python
mu, sigma = 0, 0.1  # mean and standard deviation
estimation = treg.TransformationEstimationPointToPlane(
    treg.robust_kernel.RobustKernel(
        treg.robust_kernel.RobustKernelMethod.TukeyLoss, sigma))
```

```python
print("Vanilla point-to-plane ICP, max_correspondence_distance={}:".format(
    max_correspondence_distance))

s = time.time()

reg_point_to_plane = treg.icp(source, target, max_correspondence_distance,
                              init_source_to_target, estimation, criteria,
                              voxel_size)

icp_time = time.time() - s
print("Time taken by Point-To-Plane ICP: ", icp_time)
print("Fitness: ", reg_point_to_plane.fitness)
print("Inlier RMSE: ", reg_point_to_plane.inlier_rmse)

draw_registration_result(source, target, reg_point_to_plane.transformation)
```

Registration estimation method supports `Robust Kernels`:

`robust_kernel = o3d.t.pipelines.registration.robust_kernel.RobustKernel(method, scale, shape)`

Method options:

- robust_kernel.RobustKernelMethod.L2Loss
- robust_kernel.RobustKernelMethod.L1Loss
- robust_kernel.RobustKernelMethod.HuberLoss
- robust_kernel.RobustKernelMethod.CauchyLoss
- robust_kernel.RobustKernelMethod.GMLoss
- robust_kernel.RobustKernelMethod.TukeyLoss
- robust_kernel.RobustKernelMethod.GeneralizedLoss

**Note:**

For this example we use the **TukeyLoss**. For the parameter `k` we set it to match the std deviation of the noise model $k = \sigma$.
The parameter `k` used in the Robust Kernels it's usually pick to match the standard deviation of the noise model of the input data. In this sense, `k`, is the discriminator between **inlier**/**outlier**. Altough this is not always trivial to define in real world data, for synthetic one, it's easy to fix in order to illustrate the benefits of the robust kernels.

---


```python
from sympy.physics.mechanics import ReferenceFrame,Point,dynamicsymbols
from sympy.physics.mechanics import Point
from sympy import latex,pprint,symbols,init_printing
from sympy.algebras.quaternion import Quaternion
import numpy as np
import sys
sys.path.append("../tools")
from vis import Visualizer
%matplotlib notebook
init_printing() # Para visualizar simbolos

# Definicion del modelo para el ejemplo del robot scara

# Parametros del modelo
l1,l2,l3=symbols('l1,l2,l3')
# Variables de movimiento
q1,q2,q3=dynamicsymbols('q1,q2,q3')

# Defina los marcos y puntos de interes en el robot
a=ReferenceFrame('A')
o=Point('O')
b=a.orientnew('B','Axis',(q1,a.z))
p=o.locatenew('P',l1*a.z)
c=b.orientnew('C','Axis',(q2,b.z))
q=p.locatenew('Q',l2*b.x)
r=q.locatenew('R',l3*c.x)
s=r.locatenew('Q',-q3*c.z)

# Construya un objecto de visualizacion con el marco de referencia inercial y punto de origen
vis=Visualizer(a,o)
# Agrege marcos y puntos para ser visualizados (marco,punto,geometria)
vis.add(a,o,shape='assets/scara_base.stl')
vis.add(b,p,shape='assets/scara_brazo.stl')
vis.add(c,q,shape='assets/scara_brazo.stl')
vis.add(c,r,shape='assets/scara_cilindro.stl')
vis.add(c,s,shape='assets/Scara_effector.stl')

#vis.add(b,o,shape='assets/Atraccion_Parque_link1.stl')
#vis.add(c,p,shape='assets/Atraccion_Parque_disk.stl')
vis.add(a,o,frame_scale=100)
vis.add(b,p,frame_scale=100)
vis.add(c,q,frame_scale=100)
vis.add(c,r,frame_scale=100)
vis.add(c,s,frame_scale=100)

# Modifique las variables de movimiento y verifique el cambio en la posicion y
# orientacion de los marcos
vis.plot({l1:220,l2:210,l3:210,q1:0,q2:0,q3:100})
```

    <IPython.core.display.Javascript object>

```python
#Modifique las variables de movimiento para cambiar la configuracion cinematica del modelo
#Puede hacerlo desde esta celda para actualizar el modelo sin tener que volver a generar la figura.
vis.plot({l1:220,l2:210,l3:210,q1:-0.2,q2:0.5,q3:90})
```

```python
#Esto permite crear animaciones facilmente
#Ejecute esta celda y de click en la figura 1 para observar la animacion del movimiento.
import matplotlib
from matplotlib.animation import FuncAnimation
tt=np.linspace(0,10,250)
qq1=np.sin(2*np.pi*0.1*tt)
qq2=np.sin(2*np.pi*0.23*tt)
qq3=50+50*np.sin(np.pi*0.23*tt)

def animfunc(i,qq1,qq2,qq3):
    print(i)
    vis.plot({l1:220,l2:210,l3:210,q1:qq1[i],q2:qq2[i],q3:qq3[i]})

anim=FuncAnimation(vis.fig,animfunc,fargs=(qq1,qq2,qq3),interval=10)

```

```python
#Exporte la animacion como gif
anim.save('animation.gif', fps=16)
```

    MovieWriter imagemagick unavailable; using Pillow instead.

    0
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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
    58
    59
    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


```python
import sys
from importlib import reload
import sympy
from itertools import product
from collections import defaultdict
```

```python
sys.path.insert(0, '..')
```

```python
import multivector as mv
import examples as ex
import embedding as emb
```

```python
gens = sympy.symbols(['x%d' %x for x in range(10)])
p = mv.Mv(gens, {(g,):g**i for (i,g) in enumerate(gens)})
q = mv.Mv(gens, {(g,gens[i]):g**i for (i,g) in enumerate(gens[::-1])})
```

```
t*(x1,x2,y1,y2) = (t^p*x1, t^q*x2, t^-p*y1, t^-q*y2)

invariants:
x1*y1
x2*y2
x1^q*y2^p
x2^p*y1^q
```

```python
p, q = 7,9
```

```python
a = sympy.symbols('a0 a1 a2 a3')
x = sympy.symbols('x0 x1')
y = sympy.symbols('y0 y1')
```

```python
brac = mv.Mv(x+y, {(x[0],y[0]):-1, (x[1],y[1]):-1}).brac
```

```python
rmap = {
    a[0]: x[0]*y[0],
    a[1]: x[1]*y[1],
    a[2]: (x[0]**q)*(y[1]**p),
    a[3]: (x[1]**p)*(y[0]**q)
}

embcoords, invariants = list(zip(*rmap.items()))

```

```python
gb = sympy.polys.groebner(invariants)
```

```python
P = {}
for k1, k2 in product(rmap.keys(), rmap.keys()):
    if (k2, k1) not in P.keys():
        P[(k1, k2)] = brac(rmap[k1],rmap[k2])
```

```python
P
```

    {(a2, a2): 0,
     (a3, a2): 49*x0**9*x1**6*y0**9*y1**6 - 81*x0**8*x1**7*y0**8*y1**7,
     (a0, a2): -9*x0**9*y1**7,
     (a3, a1): 7*x1**7*y0**9,
     (a0, a1): 0,
     (a2, a1): -7*x0**9*y1**7,
     (a1, a1): 0,
     (a3, a0): -9*x1**7*y0**9,
     (a0, a0): 0,
     (a3, a3): 0}

```python
Pi = mv.Mv(embcoords, emb.lift_from_quotient(rmap, limit=20))
```

```python
# lifted bivector
Pi
```

    a0*(-49*a0**8*a1**6 + 81*a0**7*a1**7)*da3*da2 + 9*a2*da0*da2 + 7*a2*da2*da1 + 9*a3*da3*da0 - 7*a3*da3*da1

```python
# bivector is poisson
mv.sBr(Pi,Pi).sort()
```

    0

what cohomology class is it in?

d_Pi(X) = -[X, Pi]

is there a vector field X such that -[X, Pi] = Pi ? (No)

What is the poisson cohomology?
What are the poisson vector fields? (mod hamiltonians)
What are the poisson bivectors? (what are the coboundaries?)

```python
vf = mv.sBr(Pi,mv.Mv(embcoords, {():a[2]}))
```

```python
coeffs = sympy.symbols('f1 f2 f3 f4')
```

```python
cond = (mv.sBr(Pi, mv.Mv(embcoords, {(e,):c for e,c in zip(embcoords, coeffs)})) - Pi).sort()
```

```python
cond
```

    (-9*a2 - 9*f3)*da0*da2 + (7*a2 + 7*f3)*da1*da2 + (-7*a3 - 7*f1)*da1*da3 + (9*a3 + 9*f1)*da0*da3 + (a0*f4*(-294*a0**8*a1**5 + 567*a0**7*a1**6) + a0*(-49*a0**8*a1**6 + 81*a0**7*a1**7) + f2*(-49*a0**8*a1**6 + 81*a0**7*a1**7 + a0*(-392*a0**7*a1**6 + 567*a0**6*a1**7)))*da2*da3

```python

```


```python
%load_ext autoreload
%autoreload 2
```

```python
import jax
import jax.numpy as jnp
```

```python
import cr
```

```python
M = 10
p = 3
N = 5
```

```python
A = jnp.zeros([M, p])
A
```

    WARNING:absl:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)

    DeviceArray([[0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.],
                 [0., 0., 0.]], dtype=float32)

```python
B = jnp.ones([N, p])
B
```

    DeviceArray([[1., 1., 1.],
                 [1., 1., 1.],
                 [1., 1., 1.],
                 [1., 1., 1.],
                 [1., 1., 1.]], dtype=float32)

```python
import cr.sparse as crs
```

```python
crs.pairwise_sqr_l2_distances_rw(A, B)
```

    DeviceArray([[3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.],
                 [3., 3., 3., 3., 3.]], dtype=float32)

```python
A = 3*jnp.ones([M, p])
```

```python
B = 5*jnp.ones([N, p])
```

```python
crs.pairwise_sqr_l2_distances_rw(A, B)
```

    DeviceArray([[12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.],
                 [12., 12., 12., 12., 12.]], dtype=float32)

```python
A = jnp.arange(1, M+1) * jnp.ones((p, 1))
A
```

    DeviceArray([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.]],            dtype=float32)

```python
B = jnp.arange(2, N+2) * jnp.ones((p, 1))
B
```

    DeviceArray([[2., 3., 4., 5., 6.],
                 [2., 3., 4., 5., 6.],
                 [2., 3., 4., 5., 6.]], dtype=float32)

```python
crs.pairwise_sqr_l2_distances_rw(A.T, B.T)
```

    DeviceArray([[  3.,  12.,  27.,  48.,  75.],
                 [  0.,   3.,  12.,  27.,  48.],
                 [  3.,   0.,   3.,  12.,  27.],
                 [ 12.,   3.,   0.,   3.,  12.],
                 [ 27.,  12.,   3.,   0.,   3.],
                 [ 48.,  27.,  12.,   3.,   0.],
                 [ 75.,  48.,  27.,  12.,   3.],
                 [108.,  75.,  48.,  27.,  12.],
                 [147., 108.,  75.,  48.,  27.],
                 [192., 147., 108.,  75.,  48.]], dtype=float32)

```python
crs.pairwise_sqr_l2_distances_cw(A, B)
```

    DeviceArray([[  3.,  12.,  27.,  48.,  75.],
                 [  0.,   3.,  12.,  27.,  48.],
                 [  3.,   0.,   3.,  12.,  27.],
                 [ 12.,   3.,   0.,   3.,  12.],
                 [ 27.,  12.,   3.,   0.,   3.],
                 [ 48.,  27.,  12.,   3.,   0.],
                 [ 75.,  48.,  27.,  12.,   3.],
                 [108.,  75.,  48.,  27.,  12.],
                 [147., 108.,  75.,  48.,  27.],
                 [192., 147., 108.,  75.,  48.]], dtype=float32)

```python
cr.sparse.pairwise_sqr_l2_distances_cw(A, B)
```

    DeviceArray([[  3.,  12.,  27.,  48.,  75.],
                 [  0.,   3.,  12.,  27.,  48.],
                 [  3.,   0.,   3.,  12.,  27.],
                 [ 12.,   3.,   0.,   3.,  12.],
                 [ 27.,  12.,   3.,   0.,   3.],
                 [ 48.,  27.,  12.,   3.,   0.],
                 [ 75.,  48.,  27.,  12.,   3.],
                 [108.,  75.,  48.,  27.,  12.],
                 [147., 108.,  75.,  48.,  27.],
                 [192., 147., 108.,  75.,  48.]], dtype=float32)

```python
A
```

    DeviceArray([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.]],            dtype=float32)

```python
B
```

    DeviceArray([[2., 3., 4., 5., 6.],
                 [2., 3., 4., 5., 6.],
                 [2., 3., 4., 5., 6.]], dtype=float32)

```python
C = A.T
```

```python
C
```

    DeviceArray([[ 1.,  1.,  1.],
                 [ 2.,  2.,  2.],
                 [ 3.,  3.,  3.],
                 [ 4.,  4.,  4.],
                 [ 5.,  5.,  5.],
                 [ 6.,  6.,  6.],
                 [ 7.,  7.,  7.],
                 [ 8.,  8.,  8.],
                 [ 9.,  9.,  9.],
                 [10., 10., 10.]], dtype=float32)

```python
D = B.T
```

```python
D
```

    DeviceArray([[2., 2., 2.],
                 [3., 3., 3.],
                 [4., 4., 4.],
                 [5., 5., 5.],
                 [6., 6., 6.]], dtype=float32)

```python
crs.pairwise_l1_distances_rw(C, D)
```

    DeviceArray([[ 3.,  6.,  9., 12., 15.],
                 [ 0.,  3.,  6.,  9., 12.],
                 [ 3.,  0.,  3.,  6.,  9.],
                 [ 6.,  3.,  0.,  3.,  6.],
                 [ 9.,  6.,  3.,  0.,  3.],
                 [12.,  9.,  6.,  3.,  0.],
                 [15., 12.,  9.,  6.,  3.],
                 [18., 15., 12.,  9.,  6.],
                 [21., 18., 15., 12.,  9.],
                 [24., 21., 18., 15., 12.]], dtype=float32)

```python
crs.pairwise_l1_distances_cw(A, B)
```

    DeviceArray([[ 3.,  6.,  9., 12., 15.],
                 [ 0.,  3.,  6.,  9., 12.],
                 [ 3.,  0.,  3.,  6.,  9.],
                 [ 6.,  3.,  0.,  3.,  6.],
                 [ 9.,  6.,  3.,  0.,  3.],
                 [12.,  9.,  6.,  3.,  0.],
                 [15., 12.,  9.,  6.,  3.],
                 [18., 15., 12.,  9.,  6.],
                 [21., 18., 15., 12.,  9.],
                 [24., 21., 18., 15., 12.]], dtype=float32)

```python
crs.pdist_l1_rw(C)
```

    DeviceArray([[ 0.,  3.,  6.,  9., 12., 15., 18., 21., 24., 27.],
                 [ 3.,  0.,  3.,  6.,  9., 12., 15., 18., 21., 24.],
                 [ 6.,  3.,  0.,  3.,  6.,  9., 12., 15., 18., 21.],
                 [ 9.,  6.,  3.,  0.,  3.,  6.,  9., 12., 15., 18.],
                 [12.,  9.,  6.,  3.,  0.,  3.,  6.,  9., 12., 15.],
                 [15., 12.,  9.,  6.,  3.,  0.,  3.,  6.,  9., 12.],
                 [18., 15., 12.,  9.,  6.,  3.,  0.,  3.,  6.,  9.],
                 [21., 18., 15., 12.,  9.,  6.,  3.,  0.,  3.,  6.],
                 [24., 21., 18., 15., 12.,  9.,  6.,  3.,  0.,  3.],
                 [27., 24., 21., 18., 15., 12.,  9.,  6.,  3.,  0.]],            dtype=float32)

```python
crs.pdist_l1_cw(A)
```

    DeviceArray([[ 0.,  3.,  6.,  9., 12., 15., 18., 21., 24., 27.],
                 [ 3.,  0.,  3.,  6.,  9., 12., 15., 18., 21., 24.],
                 [ 6.,  3.,  0.,  3.,  6.,  9., 12., 15., 18., 21.],
                 [ 9.,  6.,  3.,  0.,  3.,  6.,  9., 12., 15., 18.],
                 [12.,  9.,  6.,  3.,  0.,  3.,  6.,  9., 12., 15.],
                 [15., 12.,  9.,  6.,  3.,  0.,  3.,  6.,  9., 12.],
                 [18., 15., 12.,  9.,  6.,  3.,  0.,  3.,  6.,  9.],
                 [21., 18., 15., 12.,  9.,  6.,  3.,  0.,  3.,  6.],
                 [24., 21., 18., 15., 12.,  9.,  6.,  3.,  0.,  3.],
                 [27., 24., 21., 18., 15., 12.,  9.,  6.,  3.,  0.]],            dtype=float32)

```python
A
```

    DeviceArray([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.],
                 [ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.]],            dtype=float32)

```python
crs.pairwise_linf_distances_rw(C, D)
```

    DeviceArray([[1., 2., 3., 4., 5.],
                 [0., 1., 2., 3., 4.],
                 [1., 0., 1., 2., 3.],
                 [2., 1., 0., 1., 2.],
                 [3., 2., 1., 0., 1.],
                 [4., 3., 2., 1., 0.],
                 [5., 4., 3., 2., 1.],
                 [6., 5., 4., 3., 2.],
                 [7., 6., 5., 4., 3.],
                 [8., 7., 6., 5., 4.]], dtype=float32)

```python
crs.pairwise_linf_distances_cw(A, B)
```

    DeviceArray([[1., 2., 3., 4., 5.],
                 [0., 1., 2., 3., 4.],
                 [1., 0., 1., 2., 3.],
                 [2., 1., 0., 1., 2.],
                 [3., 2., 1., 0., 1.],
                 [4., 3., 2., 1., 0.],
                 [5., 4., 3., 2., 1.],
                 [6., 5., 4., 3., 2.],
                 [7., 6., 5., 4., 3.],
                 [8., 7., 6., 5., 4.]], dtype=float32)

```python
crs.pdist_linf_rw(C)
```

    DeviceArray([[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.],
                 [1., 0., 1., 2., 3., 4., 5., 6., 7., 8.],
                 [2., 1., 0., 1., 2., 3., 4., 5., 6., 7.],
                 [3., 2., 1., 0., 1., 2., 3., 4., 5., 6.],
                 [4., 3., 2., 1., 0., 1., 2., 3., 4., 5.],
                 [5., 4., 3., 2., 1., 0., 1., 2., 3., 4.],
                 [6., 5., 4., 3., 2., 1., 0., 1., 2., 3.],
                 [7., 6., 5., 4., 3., 2., 1., 0., 1., 2.],
                 [8., 7., 6., 5., 4., 3., 2., 1., 0., 1.],
                 [9., 8., 7., 6., 5., 4., 3., 2., 1., 0.]], dtype=float32)

```python
crs.pdist_linf_cw(B)
```

    DeviceArray([[0., 1., 2., 3., 4.],
                 [1., 0., 1., 2., 3.],
                 [2., 1., 0., 1., 2.],
                 [3., 2., 1., 0., 1.],
                 [4., 3., 2., 1., 0.]], dtype=float32)

```python
jnp.mod(jnp.array([1, 2, 2.1, 2.3, 1.0, 3.0]), 1) == 0
```

    DeviceArray([ True,  True, False, False,  True,  True], dtype=bool)

```python
x = jnp.array([1, -2, -2.1, 2.3, 1.0, 3.0, 2.0, -1])
```

```python
jnp.logical_and(x > 0, jnp.mod(x, 1) == 0)
```

    DeviceArray([ True, False, False, False,  True,  True], dtype=bool)

```python
jnp.mod(x, 2) == 1
```

    DeviceArray([ True, False, False, False,  True,  True], dtype=bool)

```python
jnp.mod(x, 2) == 0
```

    DeviceArray([False,  True, False, False, False, False], dtype=bool)

```python
jnp.logical_and(x > 0, jnp.mod(x, 2) == 1)
```

    DeviceArray([ True, False, False, False,  True,  True, False, False], dtype=bool)

```python
x = 2
```

```python
jnp.logical_and(x > 0, jnp.mod(x, 2) == 1)
```

    DeviceArray(False, dtype=bool)

```python
jnp.logical_not(jnp.bitwise_and(x, x - 1))
```

    DeviceArray(True, dtype=bool)

```python
x = jnp.arange(1, 17)
```

```python
jnp.logical_not(jnp.bitwise_and(x, x - 1))
```

    DeviceArray([ True,  True, False,  True, False, False, False,  True,
                 False, False, False, False, False, False, False,  True],            dtype=bool)

```python
jnp.floor(x)
```

    DeviceArray([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.,
                 13., 14., 15., 16.], dtype=float32)

```python
jnp.mod
```

    <function jax._src.numpy.lax_numpy.remainder(x1, x2)>

```python
crs.is_integer(x)
```

    DeviceArray([ True,  True,  True,  True,  True,  True,  True,  True,
                  True,  True,  True,  True,  True,  True,  True,  True],            dtype=bool)

```python
crs.integer_factors_close_to_sqr_root(16)
```

    (DeviceArray(4, dtype=int32), DeviceArray(4, dtype=int32))

```python

```

    DeviceArray([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
                 16], dtype=int32)

```python
sqrt(2)
```

```python

```


# Raumluftqualität 3.0

In realen Gebäuden gibt es immer einen Luftaustausch mit der Umgebung. In modernen Gebäuden ist dieser aufgrund der hochwertigen Abdichtung nach einem Blowerdoortest sehr gering. Dann müssen Gebäude mechanisch belüftet werden, um Schadstoffe abzutransportieren und die Räume mit frischer Außenluft zu versorgen.

Wird ein Gebäude belüftet, so lässt sich bei bekannter Schadstoffproduktion und bei bekannten Konzentrationen der Raumluft und der Außenluft berechnen, welcher Volumenstrom zur Belüftung erforderlich ist.

Dazu diente die Gleichgewichtsbetrachtung, dass aus dem Raum genauso viel Schadstoff über die Abluft abtransportiert werden muss, wie über die Außenluft und durch die Schadstoffquellen zugeführt wird.

Der erforderliche Außenluftvolumenstrom ist:

$$
  \dot V_{\rm au} = \dfrac{\dot V_{\rm sch}}{k_{\rm zul} - k_{\rm au}}.
$$

Im nächsten Schritt wird die Frage untersucht, wie sich die Schadstoffkonzentration in einem belüfteten Raum im Laufe der Zeit verändert. Dazu gehen wir zunächst von einem gut durchlüfteten Raum aus. Die CO_2-Konzentration in einem gut durchlüfteten Raum wird mit der CO_2-Konzentration der Außenluft übereinstimmen. Zu Beginn ist also

$$
  k_0 = k_{\rm au}
$$

Zu einer beliebigen Zeit $t$ lässt sich die Schadstoffkonzentration $k(t)$ der Raumluft nach der folgenden Funktion berechnen:

$$
  k(t)= k_\infty + (k_0-k_\infty)\cdot{\mathrm e}^{-\beta\,(t-t_0)}
$$

Dabei ist $k_\infty$ der Endwert, der sich nach (theoretisch unendlich) langer Zeit einstellt. Das Zeichen $\infty$ wird mathematisch als "Unendlich" gelesen. $k_0$ ist der Startwert zur Zeit $t=t_0$, wobei meistens $t_0=0$ gesetzt wird. Manchmal ist es aber bequem, nicht bei $t_0=0$ beginnen zu müssen, sondern zu einer beliebigen Zeit.

In einem gut durchlüfteten Raum ist $k_0 = k_\rm{au} \approx 400\,\rm{ppM}$.

Der Wert $k_\infty$ ist die Schadstoffkonzentration, die sich im Gleichgewicht zwischen Schadstoffzufuhr und -abtransport ergibt. Sie ist bekannt, wenn der Außenluftvolumenstrom $\dot V_{\rm au}$ und die Schadstoffproduktion $\dot V_\rm{sch}$ bekannt sind. Es ist nämlich

$$
  \dot V_{\rm au} = \dfrac{\dot V_{\rm sch}}{k_\infty -k_{\rm au}}
$$

Nach $k_\infty$ umgestellt, ergibt sich

$$
  k_\infty =
  k_{\rm au} + \dfrac{\dot V_{\rm sch}}{\dot V_{\rm au}}
$$

### Beispiel

Ein Gebäude mit einer Grundfläche von $150\,{\rm m^2}$ und einer Geschosshöhe von $2.50\,{\rm m}$ wird mit
$180\,{\rm \dfrac{m^3}{h}}$ Außenluft belüftet. Zu Beginn ist die Schadstoffkonzentration $k_0=400\,\rm ppM$. Der Schadstoffvolumenstrom ist $540\,\rm\dfrac{\ell}{h}$ CO_2.

Berechnen Sie, welche Schadstoffkonzentration sich im Gebäude einstellt und stellen Sie den zeitlichenn Verlauf von $k(t)$ für einen 8-Stundentag dar.

#### Lösung

Zunächst wird das Raumvolumen berechnet. Es ergibt sich zu

$$
  V_\rm{ra} = 150\cdot 2.5\,\rm{m}^2 = 375\,\rm{m}^2
$$

oder, mittels Jupyter:

```python
# Raumvolumen
V_ra = 150*2.5 # m**3
V_ra
```

    375.0

Im nächsten Schritt wird daraus die CO_2-Konzentration $k_\infty$ ermittelt:

$$
  k_\infty = k_\rm{au} + \dfrac{\dot V_\rm{sch}}{\dot V_\rm{ra}}
  = 400\,\rm{ppM} + \dfrac{540\cdot 10^{-3}\,\rm{m}^3}{375\,\rm{m}^3}
  = 3400\,\rm{ppM}
$$

oder, mittels Jupyter:

```python
# Schadstoffvolumenstrom (CO_2):
dV_sch = 540e-3 # 180 l/h

# CO_2-Konzentration der Außenluft
k_0 = 400e-6 # 400 ppM

# bekannter Außenluftvolumenstrom
dV_au = 180 # m**3/h

# Daraus berechneter Endwert der Konzentration
k_inf = k_0 + dV_sch/dV_au

# Kontrollausgabe in ppM
k_inf*1e6 # ppM
```

    3400.0000000000005

Die Luftwechselzahl ergibt sich durch

$$
  \beta = \dfrac{\dot V_\rm{au}}{V_\rm{ra}} = 0.48\,\rm\dfrac{1}{h}
$$

oder, mittels Jupyter:

```python
# Luftwechselzahl beta:
beta = dV_au/V_ra # 1/h
beta
```

    0.48

Für das Plotten wird hier nur das Jupyter Notebook verwendet:

```python
# Plotten vorbereiten
from matplotlib import pyplot as plt
%config InlineBackend.figure_format='retina'

import pandas as pd
import numpy as np
```

```python
# Das Zeitintervall auf der x-Achse:
lt = np.linspace(0,4,17) # 0..4h

# Der Dataframe
df = pd.DataFrame(
    {
        't': lt,
        'k': 1e6*(k_inf + (k_0-k_inf)*np.exp(-beta*lt)) # k in ppM
    }
)

# Kontrollausdruck
df.head().T
```

      0
      1
      2
      3
      4

      t
      0.0
      0.25000
      0.500000
      0.750000
      1.000000

      k
      400.0
      739.23869
      1040.116417
      1306.971022
      1543.649825

```python
# Zeitliche Entwicklung der CO_2-Konzentragion im Diagramm
ax = df.plot(x='t',y='k',label="$k = k(t)$")
ax.axhline(1e6*k_inf,c='r')
ax.grid()
ax.set(
    ylim=(0,4000), ylabel=r'CO_2-Konzentration $k$ in $\rm ppM$',
    xlim=(0,4), xlabel=r'Zeit $t$ in Stunden'
);
```

Man erkennt, dass bereits nach etwa 30 Minuten die Grenze von 1000 ppM erreicht wird. Nach etwas über 90 Minuten ist die Raumluftqualität nicht mehr akzeptabel.

## Die Schadstoffbilanz des Raumes nach einem Zeitschritt

Herleitung der Lösung.

Im nächsten Schritt wird untersucht, wie sich $k$ verändert, nachdem der Raum eine bestimmte Zeit $\Delta t$ belüftet worden ist.

- Von den Personen (oder sonstigen Schadstoffquellen) im Raum wird eine bestimmte Schadstoffmenge abgegeben,
  nämlich
  $$
    \dot V{\rm sch}\cdot\Delta t
  $$
- Mit der Außenluft wird eine bestimmte Schadstoffmenge in den Raum hineingetragen, nämlich
  $$
    k_{\rm au}\cdot\dot V_{\rm au}\cdot\Delta t
  $$
- Mit der Abluft wird eine bestimmte Schadstoffmenge aus dem Raum herausgetragen, nämlich
  $$
    k_0\cdot\dot V_{\rm ab}\cdot\Delta t
  $$

Damit ergibt sich:

$$
  k_1 \cdot V_{\rm ra} =
    k_0 \cdot V_{\rm ra}
  + \dot V{\rm sch}\cdot\Delta t
  + k_{\rm au}\cdot\dot V_{\rm au}\cdot\Delta t
  - k_0\cdot\dot V_{\rm ab}\cdot\Delta t
$$

Oder, wenn durch das Raumvolumen dividiert wird:

$$
  k_1 =
    k_0
  + \dfrac{\dot V{\rm sch}}{V_{\rm ra}}\cdot\Delta t
  + k_{\rm au}\cdot\dfrac{\dot V_{\rm au}}{V_{\rm ra}}\cdot\Delta t
  - k_0\cdot\dfrac{\dot V_{\rm ab}}{V_{\rm ra}}\cdot\Delta t
$$

Nun ist $\dfrac{\dot V_{\rm au}}{V_{\rm ra}} = \dfrac{\dot V_{\rm ab}}{V_{\rm ra}} = \beta$ gerade die Luftwechselzahl des Raumes. Deshalb ist

$$
 k_1 =
    k_0
 + \dfrac{\dot V{\rm sch}}{V_{\rm ra}}\cdot\Delta t
 + k_{\rm au}\cdot\beta\cdot\Delta t
 - k_0\cdot\beta\cdot\Delta t
$$

oder, zusammengefasst:

$$
  k_1 =
     k_0\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
$$

Genau so, wie $k_1$ aus $k_0$ berechnet wurde, lässt sich $k_2$ aus $k_1$ berechnen, u.s.w. Das führt der Reihe nach auf

\begin{align}
    k_1 &=
     k_0\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
  \\[2ex]
  k_2 &=
     k_1\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
  \\[2ex]
  k_3 &=
     k_2\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
  \\[2ex]
  k_4 &=
     k_3\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
  \\[2ex]
   &\,\,\vdots
  \\
   &=
   \\
   &\,\,\vdots
   \\[2ex]
  k_{n} &=
     k_{n-1}\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
\end{align}

Da der Unterschied zwischen $k_{n}$ und $k_{n-1}$ immer kleiner wird, gilt schließlich

\begin{align}
  k_{\infty} &=
     k_{\infty}\cdot\left(1 -\beta\cdot\Delta t \right)
  + \left(\dfrac{\dot V{\rm sch}}{V_{\rm ra}}
  + k_{\rm au}\cdot\beta\right)\cdot\Delta t
\end{align}

wobei der Wert $k_\infty$ nach (theoretisch) unendlich langer Zeit erreicht wird.

Bildet man nun die Differenzen $k_1 - k_\infty$, $k_2-k_\infty$, $\ldots$ $k_{n+1} - k_\infty$, so ergeben sich die einfacheren Formeln

\begin{align}
  k_1 - k_\infty &= (k_0 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right) \\
  k_2 - k_\infty &= (k_1 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)
                  = (k_0 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)^2\\
  k_3 - k_\infty &= (k_2 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)
                  = (k_0 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)^3\\
  k_4 - k_\infty &= (k_3 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)
                  = (k_0 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)^4\\
                 &\,\,\vdots\\
                 &= \\
                 &\,\,\vdots\\
  k_n - k_\infty &= (k_{n-1} - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)
                  = (k_0 - k_\infty)\cdot\left(1 -\beta\cdot\Delta t \right)^n\\
\end{align}

Lässt man nun das Zeitintervall $\Delta t$ immer kleiner werden, so werden immer mehr Zeitschritte benötigt, um den Zeitpunkt $t = n\cdot\Delta t$ zu erreichen. Es ergibt sich

$$
  t = n\cdot\Delta t \implies \Delta t = \dfrac{t}{n}
$$

und, mit der Eulerschen Formel für die Funktion ${\rm e}^x$, nach der

$$
  \lim_{n\to\infty}
  \left(
    1 + \dfrac{x}{n}
  \right)^n
  = {\rm e}^x
$$

ist:

$$
  (1-\beta\cdot\Delta t)^n = \left(1-\dfrac{\beta\,t}{n}\right)^n \to {\mathrm e}^{-\beta\,t}
  \quad\text{für}\quad n\to\infty
$$

Wegen $n\cdot\Delta t = t$ kann man $k_n$ durch $k(t)$ ersetzen. Damit ergibt sich schließlich die Formel

$$
  k(t) - k_\infty = (k_0 - k_\infty)\cdot{\mathrm e}^{-\beta\,t}
$$

Dies Ergebnis schreibt man auch in der Form

$$
  k(t) = k_\infty + (k_0 - k_\infty)\cdot{\mathrm e}^{-\beta\,t}
$$