```python
import holoviews as hv
hv.extension('bokeh')
hv.opts.defaults(hv.opts.Curve(width=500),
                 hv.opts.Image(width=500, colorbar=True, cmap='Viridis'))
```

```python
import numpy as np
import scipy.signal
import scipy.fft
```

# Diseño de sistemas y filtros FIR

En la lección anterior definimos un sistema FIR que transforma una obtiene una salida $y$ a partir de una entrada $x$ como

$$
y[n] = (h * x)[n]
$$

donde $h$ es un vector de largo $L+1$ que tiene los coeficientes del sistema y $*$ es la operación de convolución

En esta lección veremos

- La respuesta al impulso y respuesta en frecuencia de un sistema
- La definición de filtro y los tipos básicos de filtros
- Como diseñar un filtro FIR, es decir como decidir los valores del vector $h$

## Respuesta al impulso de un sistema

Sea el impulso unitario o delta de Kronecker

$$
\delta[n-m] = \begin{cases} 1 & n=m \\ 0 & n \neq m \end{cases}
$$

La **respuesta al impulso de un sistema discreto** es la salida obtenida cuando la entrada es un impulso unitario

Para un sistema FIR arbitrario tenemos

$$
y[n]|_{x=\delta} = (h * \delta)[n] = \sum_{j=0}^L h[j] \delta[n-j] = \begin{cases} h[n] & n \in [0, L] \\ 0 & \text{en otro caso} \end{cases} \\
$$

es decir que la respuesta al impulso:

-  tiene **una duración finita y luego decae a zero**
-  recupera los coeficientes $h[j]$ del sistema

En un sistema causal se tiene que $h[n] = 0 \quad \forall n < 0$

Llamamos **soporte** del sistema a todos aquellos valores de $n$ tal que  $h[n] \neq 0$

### Ejemplo: Respuesta al impulso del sistema reverberante

Para el sistema reverberante

$$
y[n] = x[n] + A x[n-m]
$$

la respuesta al impulso es

$$
y[n] = \delta[n] + A \delta[n-m] = \begin{cases} 1 & n=0\\ A& n=m \\ 0 & \text{en otro caso} \end{cases}
$$

La respuesta al impulse me permite a recuperar los coeficientes del sistema en caso de que no los conociera

## Respuesta en frecuencia de un sistema

Sea un sistema lineal cuyos coeficientes no cambian en el tiempo, como el sistema FIR que hemos estado estudiando

Por propiedad de la transformada de Fourier sabemos que

$$
\begin{align}
\text{DFT}_N [y[n]] & = \text{DFT}_N [(h * x)[n]] \nonumber \\
\text{DFT}_N [y[n]] & = \text{DFT}_N [h[n]]  \cdot \text{DFT}_N [x[n]] \nonumber \\
Y[k] &= H[k] \cdot X[k] ,
\end{align}
$$

donde llamamos a $H[k]$ la **respuesta en frecuencia del sistema**

La respuesta en frecuencia es **la transformada de Fourier de la respuesta al impulso**

### Respuesta en frecuencia utilizando Python

Podemos calcular la respuesta en frecuencia de un filtro a partir de su respuesta al impulso $h$ usando la función

```python
scipy.signal.freqz(b, # Coeficientes en el numerador h
                   a=1, # Coeficientes en el denominador de h
                   fs=6.28318 # Frecuencia de muestreo
                   ...
                  )
```

Para el caso de un filtro FIR solo existen coeficientes en el numerador por lo que no utilizamos el argumento $a$

La función retorna

```python
freq, H = scipy.signal.freqz(b=h)
```

un arreglo de frecuencias y la respuesta en frecuencia (compleja)

### Ejemplo: Respuesta en frecuencia del sistema promediador

El sistema promediador que vimos la lección anterior tiene respuesta al impulso

$$
h[i] = \begin{cases} 1/L & i < L \\ 0 & i > L \end{cases}
$$

El valor absoluto de su respuesta en frecuencia es

```python
p1, p2 = [], []

for L in [10, 20, 50]:
    h = np.zeros(shape=(100,))
    h[:L] = 1/L
    freq, H = scipy.signal.freqz(b=h, fs=1)
    p1.append(h)
    p2.append(np.abs(H))
```

```python
ph = hv.Overlay([hv.Curve((range(100), p), 'Tiempo', 'Respuesta al impulso') for p in p1])
pH = hv.Overlay([hv.Curve((freq, p), 'Frecuencia', 'Respuesta en frecuencia') for p in p2])
hv.Layout([ph, pH]).cols(1).opts(hv.opts.Curve(height=200))
```

:::{note}

Mientras más ancho es el sistema en el dominio del tiempo ($L$ grande), más concentrada se vuelve su respuesta en frecuencia

:::

Si multiplicamos $H$ con el espectro de una señal, lo que estamos haciendo es atenuar las frecuencias altas

Formalizaremos este concepto a continuación

## Filtros digitales

Un **filtro** es un sistema cuyo objetivo es reducir o resaltar un aspecto específico de una señal

Por ejemplo

- Disminuir el nivel de ruido
- Separar dos o más señales que están mezcladas
- Ecualizar la señal
- Restaurar la señal (eliminar desenfoque o artefactos de grabación)

Llamamos **filtro digital** a los filtros aplicados a señales digitales y hablamos de **señal filtrada** para referirnos a la salida del filtro

En esta unidad nos enfocaremos en filtros cuyos coeficientes son fijos y no se modifican en el tiempo. En la próxima unidad veremos filtros que se adaptan continuamente a los cambios de la entrada

### Tipos básicos de filtro

Como vimos el filtro lineal e invariante en el tiempo puede estudiarse en frecuencia usando

$$
Y[k] = H[k] X[k] ,
$$

donde $H[k]$ es la DFT del filtro (respuesta en frecuencia)

El filtro actua como una **máscara multiplicativa** que modifica el espectro de la señal entrada

:::{important}

Esto significa que el filtro sólo puede acentuar, atenuar o remover ciertas frecuencias pero **nunca crear nuevas frecuencias**

:::

Consideremos los siguienes filtros o máscaras ideales

De izquierda a derecha tenemos:

- Filtro pasa bajo: Anula las frecuencias altas. Sirve para suavizar
- Filtro pasa alto: Anula las frecuencias bajas. Sirve para detectar cambios
- Filtro pasa banda: Anula todo excepto una banda continua de frecuencias
- Filtro rechaza banda: Anula sólo una banda continua de frecuencias

Las llamamos "ideales" por que en general los cortes de los filtros no pueden ser tan abruptos como se muestra en la figura

A continuación veremos un método para diseñar filtros FIR partiendo desde el dominio de la frecuencia

## Diseño de un filtro FIR: Método de la ventana

Diseñar un filtro consisten en definir

- L: El largo de la respuesta al impulso
- h: Los valores de la respuesta al impulso

El siguiente algoritmo de diseño de filtro se llama el "método de la ventana" y parte de la base de una respuesta en frecuencia ideal

1. Especificar una **respuesta en frecuencia** ideal $H_d[k]$ dependiendo de los requerimientos
1. Usar la transformada de Fourier inversa para obtener la **respuesta al impulso ideal** $h_d[n]$
1. Truncar la respuesta al impulso ideal usando **una ventana** tal que $h[n] = h_d[n] w[n]$

Finalmente $h[n]$ nos da los coeficientes del filtro FIR y $w[n]$ nos da el largo del filtro

La ventana $w[n]$ puede ser cualquiera de las funciones vistas en la unidad anterior, por ejemplo una ventana rectangular

$$
w[n] = \begin{cases} 1 & n \leq L \\ 0 & n > L \end{cases}
$$

o la ventana de Hann

$$
w[n] = 0.5 - 0.5 \cos \left( \frac{2\pi n}{L-1} \right)
$$

A continuación veremos paso a paso como se crea un filtro pasa-bajo utilizando este método. Más adelante veremos unas funciones de `scipy` que facilitan este proceso

### Diseño de un filtro pasa bajo (LPF)

Un filtro pasa bajo es aquel que sólo deja pasar las **bajas** frecuencias

Sus usos son:

- Recuperar una tendencia o comportamiento lento en la señal
- Suavizar la señal y disminuir la influencia del ruido aditivo

Diseñemos un filtro que elimine todas las frecuencias mayores a $f_c$ [Hz] de una señal $x[n]$ muestreada con frecuencia $F_s$

**Paso 1: Respuesta en frecuencia ideal**

Propongamos la siguiente respuesta en frecuencia que solo deja pasar las frecuencias menores a $f_c$, es decir que sólo es distinta de cero en el rango $[-f_c, f_c]$

$$
\begin{align}
H_d(\omega) &= \begin{cases} 1 & |f| < f_c\\ 0 & |f| > f_c \end{cases} \nonumber \\
&= \text{rect}(f/f_c) \nonumber
\end{align}
$$

```python
fc = 0.1 # Frecuencia de corte
Fs = 1 # Frecuencia de muestreo
n = np.arange(-50, 50, step=1/Fs);
f = scipy.fft.fftshift(scipy.fft.fftfreq(n=len(n), d=1/Fs))

# Diseño de la respuesta en frecuencia ideal
kc = int(len(n)*fc)
Hd = np.zeros_like(n, dtype=np.float64);
Hd[:kc] = 1.
Hd[len(Hd)-kc+1:] = 1.
```

**Paso 2: Respuesta al impulso ideal**

Obtenemos la transformada de Fourier inversa de la respuesta en frecuencia

$$
\begin{align}
h_d(t) &=  \int_{-f_c}^{f_c} e^{j 2 \pi f t} df \nonumber \\
& = \frac{2j  f_c}{2 j \pi f_c t} \sin(2 \pi f_c t) = 2  f_c \text{sinc}(2 \pi f_c t) \nonumber
\end{align}
$$

donde la versión en tiempo discreto sería

$$
h_d[n] = 2 f_c\text{sinc}(2 \pi f_c n/ F_s)/F_s
$$

Notemos que es una función infinitamente larga

```python
# Cálculo de la respuesta al impulso ideal
#hd = np.real(sfft.ifftshift(sfft.ifft(Hd)))
hd = 2*fc*np.sinc(2*fc*n/Fs)/Fs # Se omite Pi por que está incluido en np.sinc
```

```python
p1 = hv.Curve((f, scipy.fft.fftshift(Hd)), 'Frecuencia', 'Respuesta en\n frecuencia ideal')
p2 = hv.Curve((n, hd), 'Tiempo', 'Respuesta al\n impulso ideal')
hv.Layout([p1, p2]).opts(hv.opts.Curve(width=300, height=200))
```

**Paso 3: Truncar la respuesta al impulso ideal**

Para obtener una respuesta al impulso finita multiplicamos por una ventana finita de largo $L+1$

$$
h[n] = 2  f_c \text{sinc}(2 \pi f_c n /F_s) \cdot \text{rect}(n/(L+1))
$$

```python
# Cálculo de la respuesta al impulso truncada
def truncar(hd, L=100):
    w = np.zeros_like(hd);
    w[len(w)//2-L//2:len(w)//2+L//2+1] = 1.
    return w*hd

# Cálculo de la respuesta en frecuencia truncada
h = truncar(hd)
H = scipy.fft.fft(h)
```

Comparemos la respuesta en frecuencia ideal con la que en realidad aplicamos a la señal

```python
p = []
p.append(hv.Curve((f, scipy.fft.fftshift(Hd)), 'Frecuencia', 'Respuesta en frecuencia', label='Ideal'))

for L in [20, 40]:
    H = scipy.fft.fft(truncar(hd, L))
    p.append(hv.Curve((f, scipy.fft.fftshift(np.abs(H))), label=f'Truncada L={L}'))

hv.Overlay(p).opts(hv.opts.Curve(line_width=3, alpha=0.75))
```

La respuesta en frecuencia "ideal" $H_d[k]$ es plana y tiene discontinuidades fuertes

La respuesta en frecuencia "truncada" $H[k]$ busca aproximar a $H_d[k]$. Pero observando $H[k]$ notamos que

- Aparecen ondulaciones (ripple). No tiene zonas de paso y/o rechazo perfectamente planas
- La frecuencia de corte es una transición. La discontinuidad no es abrupta como el caso ideal

El siguiente esquema muestra estos comportamientos

La función de ventana que se ocupa para truncar la respuesta ideal influye en el trade-off entre cuan abrupta es la caida del filtro y las ondulaciones (ripple) que aparece en las zonas planas.

En general

- más larga es la ventana ($L$) más fiel será la respuesta en frecuencia
- mientras más suave es la ventana más lenta será la transición en la respuesta en frecuencia y habrán menos ondulaciones en las bandas de paso y rechazo

## Diseño de filtro FIR usando scipy

Podemos diseñar un filtro usando la técnica de enventando con la función de scipy

```python
scipy.signal.firwin(numtaps, # Largo del filtro
                    cutoff, # Frecuencia(s) de corte
                    window='hamming', # Función de ventana
                    pass_zero=True, # Se explicará a continuación
                    fs=None # Frecuencia de muestreo
                    ...
                    )
```

El argumento `pass_zero` es un booleano que indica si la frecuencia cero pasa o se rechaza por el filtro. Se darán más detalles en los ejemplos que se muestran a continuación.

La función `firwin` retorna un arreglo con $h$ que corresponde a la respuesta al impulso del filtro FIR. Luego podemos usar el arreglo $h$ para convolucionar con nuestra señal de entrada.

### Diseño de un filtro pasa bajo (LPF)

Veamos como se ocupa esta función para diseñar el filtro pasa-bajo que creamos manualmente en la sección anterior

```python
fc = 0.1 # Frecuencia de corte
Fs = 1 # Frecuencia de muestreo
L = 100+1 # Largo del filtro

h = scipy.signal.firwin(L, fc, window='boxcar', pass_zero=True, fs=Fs)
freq, H = scipy.signal.freqz(h, fs=Fs)
```

El argumento `pass_zero` debe ser `True` para diseñar un filtro pasa-bajo

```python
p1 = hv.Curve((range(len(h)), h), 'Tiempo', 'Respuesta al\n impulso')
p2 = hv.Curve((freq, np.absolute(H)), 'Frecuencia', 'Respuesta en\n frecuencia')

hv.Layout([p1, p2 * hv.VLine(fc).opts(color='r', alpha=0.25)]).opts(hv.opts.Curve(width=300, height=200))
```

El resultado es equivalente al proceso manual que mostramos antes

Podemos cambiar el compromiso (trade-off) entre la velocidad de la transición y las ondulaciones si utilizamos otra ventana. Por ejemplo para la ventana de *Hamming* tenemos

```python
fc = 0.1 # Frecuencia de corte
Fs = 1 # Frecuencia de muestreo
L = 100+1 # Largo del filtro

h = scipy.signal.firwin(L, fc, window='hamming', pass_zero=True, fs=Fs)
freq, H = scipy.signal.freqz(h, fs=Fs)
```

```python
p1 = hv.Curve((range(len(h)), h), 'Tiempo', 'Respuesta al\n impulso')
p2 = hv.Curve((freq, np.absolute(H)), 'Frecuencia', 'Respuesta en\n frecuencia')

hv.Layout([p1, p2 * hv.VLine(fc).opts(color='r', alpha=0.25)]).opts(hv.opts.Curve(width=300, height=200))
```

:::{note}

Las ondulaciones disminuye pero el corte en frecuencia es ahora más lento

:::

### Diseño de un filtro pasa alto (HPF)

Un filtro pasa alto es aquel que sólo deja pasar las **altas** frecuencias

Sus usos son:

- Identificar cambios/detalles, es decir comportamientos rápidos en una señal
- Eliminar tendencias

Con respecto al ejemplo anterior, para diseñar un filtro pasa alto con `firwin` sólo debemos cambiar el valor del argumento `pass_zero`

```python
Fs = 1 # Frecuencia de muestreo
fc = 0.2 # Frecuencia de corte
L = 100+1 # Largo del filtro

h = scipy.signal.firwin(L, fc, window='hamming', pass_zero=False, fs=Fs)
freq, H = scipy.signal.freqz(h, fs=Fs)
```

```python
p1 = hv.Curve((range(L), h), 'Tiempo', 'Respuesta al\n impulso')
p2 = hv.Curve((freq, np.absolute(H)), 'Frecuencia', 'Respuesta en\n frecuencia')

hv.Layout([p1, p2]).opts(hv.opts.Curve(width=300, height=200))
```

:::{note}

La respuesta en frecuecia muestra que este filtro anula las frecuencias bajas de la señal

:::

### Diseño de un filtro pasa banda (BPF) y rechaza banda (BRF)

Como sus nombres lo indican estos filtros

- BPF: Dejan pasar sólo una cierta banda de frecuencia
- BRF: Dejan pasar todas las frecuencias excepto una banda determinada

La banda de frecuencia está definida por sus frecuencias de corte mínima y máxima $f_{c1} < f_{c2}$

Para crear un filtro BPF o BRF con `firwin` debemos entregar una tupla o lista con estas frecuencias. Por ejemplo para un filtro BPF

```python
Fs = 1 # Frecuencia de muestreo
fc1, fc2 = 0.2, 0.3 # Frecuencias de cortes
L = 100+1 # Largo del filtro

h = scipy.signal.firwin(L, (fc1, fc2), window='hamming', pass_zero=False, fs=Fs)
freq, H = scipy.signal.freqz(h, fs=Fs)
```

```python
p1 = hv.Curve((range(L), h), 'Tiempo', 'Respuesta al\n impulso')
p2 = hv.Curve((freq, np.absolute(H)), 'Frecuencia', 'Respuesta en\n frecuencia')

hv.Layout([p1, p2]).opts(hv.opts.Curve(width=300, height=200))
```

:::{note}

La respuesta en frecuecia muestra que este filtro anula las frecuencias **fuera** del rango definido

:::

Un filtro rechaza-banda se crea con el argumento `pass_zero=True`

```python
h = scipy.signal.firwin(L, (fc1, fc2), window='hamming', pass_zero=True, fs=Fs)
freq, H = scipy.signal.freqz(h, fs=Fs)
```

```python
p1 = hv.Curve((range(L), h), 'Tiempo', 'Respuesta al\n impulso')
p2 = hv.Curve((freq, np.absolute(H)), 'Frecuencia', 'Respuesta en\n frecuencia')

hv.Layout([p1, p2]).opts(hv.opts.Curve(width=300, height=200))
```

:::{note}

La respuesta en frecuecia muestra que este filtro anula las frecuencias **dentro** del rango definido

:::

## Ejemplo: Filtro FIR para remover una tendencia

En la lección anterior vimos el caso de una señal de interés que está montada en una tendencia

```python
np.random.seed(0);
n = np.arange(0, 150, step=1)
C = np.exp(-0.5*(n[:, np.newaxis] - n[:, np.newaxis].T)**2/30**2)
x_tendencia = 3*np.random.multivariate_normal(np.zeros_like(n), C)+2.5
x_deseada = np.sin(2.0*np.pi*0.125*n)
x = x_deseada + x_tendencia
```

```python
p3=hv.Curve((n, x_deseada), 'Tiempo', 'Señal', label='Deseada (s)').opts(color='k', alpha=0.75)
p2=hv.Curve((n, x_tendencia), 'Tiempo', 'Señal', label='Tendencia').opts(alpha=0.75)
p1=hv.Curve((n, x), 'Tiempo', 'Señal', label='Observada (x)').opts(height=250)
hv.Overlay([p1,p2,p3]).opts(legend_position='bottom_right')
```

Podemos diseñar un filtro FIR para separar la señal deseada de la tendencia

Para eso necesitamos definir una frecuencia de corte. Podemos encontrar una frecuencia de corte apropiada en base al espectro de amplitud de la señal observada

```python
freq = scipy.fft.rfftfreq(n=len(x), d=1)
SA = np.absolute(scipy.fft.rfft(x-np.mean(x)))
hv.Curve((freq, SA), 'Frecuencia [Hz]', 'Espectro').opts(height=250)
```

El espectro nos indica que existe un componente con frecuencia cercana a 0.13 Hz y otro más lento con frecuencia cercana a 0.01 Hz

Si queremos el componente más rápido podemos diseñar un filtro pasa-alto con una frecuencia de corte entre estos dos valores

Veamos como cambia la señal filtrada y su espectro de amplitud ante distintas frecuencias de corte en torno al rango anteriormente mencionado. Se visualiza también la respuesta en frecuencia del filtro diseñado (en rojo) sobre el espectro resultante

```python
L = 51

y, Y, H = {}, {}, {}
for fc in np.arange(0.01, 0.17, step=0.01):
    # Diseñar filtro
    h = scipy.signal.firwin(L, fc, window='hamming', pass_zero=False, fs=1)
    # Filtrar señal
    y[fc] = scipy.signal.convolve(x, h, mode='same', method='auto')
    # Obtener espectro de la señal filtrada
    Y[fc] = np.absolute(scipy.fft.rfft(y[fc]-np.mean(y[fc]), norm='forward'))
    # Obtener respuesta en frecuencia del filtro
    freqH, H[fc] = scipy.signal.freqz(h, fs=1)
    H[fc] = np.abs(H[fc])

freq = scipy.fft.rfftfreq(n=len(x), d=1)
```

```python
hMap1 = hv.HoloMap(kdims='Frecuencia de corte')
hMap2 = hv.HoloMap(kdims='Frecuencia de corte')

for fc, y_ in y.items():
    hMap1[fc] = hv.Curve((n, y_), 'Tiempo [s]', 'Salida', label='y')
for (fc, Y_),(fc, H_) in zip(Y.items(), H.items()):
    p1 = hv.Curve((freq, Y_), 'Frecuencia [Hz]', 'Espectro', label='Y')
    p2 = hv.Curve((freqH, H_), 'Frecuencia [Hz]', 'Espectro', label='H')
    hMap2[fc] =  p1 * p2

p_target = hv.Curve((n, x_deseada), 'Tiempo', 'Salida', label='s').opts(color='k', alpha=0.5, width=4)
hv.Layout([hMap1 * p_target, hMap2]).cols(1).opts(hv.opts.Curve(height=250),
                                                  hv.opts.Overlay(legend_position='bottom_right'))
```

## Resumen

El diseño del filtro está dado entonces por su

- **Aplicación:** Definida por la respuesta en frecuencia ideal , por ejemplo: Pasa-bajo, Pasa-alto, etc
- **Fidelidad:** El error tolerable entre la respuesta en frecuencia ideal y la real

El tipo de filtro y sus frecuencias de corte definen su Aplicación. Esto es un requisito del problema que buscamos resolver.

El parámetro $L$ nos da un trade-off para la fidelidad. Si agrandamos $L$ tendremos mayor fidelidad pero más costo computacional.

El tipo de función de ventana que se ocupa para truncar también afecta la fidelidad entregando un segundo trade-off entre artefactos (ondulaciones) en las zonas de paso/rechazo y que tan abrupto es el corte en frecuencia

```python

```


# Comparison of W'balance algorithms

## Initialization

```python
from athletic_pandas.models import Athlete, WorkoutDataFrame

%matplotlib inline
```

## Introduction

In this article a comparison is made of the different W'balance algorithms that are circulating. It is assumed that the reader is familiar with power duration models, the 2-parameter critical power model and the W'balance concept. If you are not familiar with these, Jones et al. (2017)1 is a good primer. If that article is too complicated then this article will probably be too complicated as well, sorry about that... The article probably is probably not complete and also not intended to judge or validate any of the algorithms. The algorithms that are discussed are the Skiba2 algorithm, an optimization (approximation) of the Skiba algorithm by Dave Waterworth3 and the Froncioni/Clarke algorithm4. All the algorithms are implemented in the Python library [athletic_pandas](https://github.com/aartgoossens/athletic_pandas).

## Skiba

The Skiba W'balance algorithm is the best known algorithm to calculate W'balance. It is the only algorithm that has been scientifically validated5. The formula's for the algorithm are:

\begin{equation}
W'_{bal}(t) = W' - \sum_{u=0}^t W'_{exp}(u) \cdot e^{-\frac{t-u}{\tau_{W'}}}
\end{equation}

\begin{equation}
\tau_{W'} = 546 \cdot e^{-0.01 \cdot D_{CP}}+316
\end{equation}

\begin{equation}
W'_{exp}(t) =
    \begin{cases}
    0 & \text{where } P(t) < CP \\
    P(t) - CP & \text{else}
    \end{cases}
\end{equation}

Where $W’_{bal}(t)$ is equal to $W'_bal$ at time *t*, **W’**  is the amount of available energy above CP , **t**  the time for which $W'_{bal}$ is calculated, **u** the iterator of the summation, $W’_{exp}(t)$  amount of energy above CP that is used at time u (*expended*), **e**  the Euler number and **$\tau_{W’}$** a time constant that describes the recovery speed. The numbers 546, -0.01 and 316 are determined in Skiba's original article and do not change between persons. $D_{CP}$ is the difference between CP and the average power of the intervals in which the power was below CP. $D_{CP}$ can be calculated dynamically (the average until time t) or calculated once for the entire workout and used as a static value. Skiba recommends using a static value for $D_{CP}${citation needed}.

### Waterworth optimization of Skiba

The original algorithm of Skiba is quite expensive to compute, even on fast computers, since the summation has to be run for every time t again. Mathematician Dave Waterworth helped core developer of GoldenCheetah Mark Liversedge to develop an optimization of the Skiba algorithm3. This reformulation is an approximation of the Skiba algorithm so results can vary a little in extreme cases, especially when $\tau_{W'}$ is very low. I will show a comparison of these two algorithms later in this article.

\begin{equation}
S(t) = \sum_0^t W'_{exp}(t) \cdot e^{\frac{t}{\tau_{W'}}}
\end{equation}
\begin{equation}
W'_{bal}(t) = S(t) \cdot e^\frac{t}{\tau_{W'}}
\end{equation}

## Froncioni/Clarke

The second algorithm is only implemented in [GoldenCheetah](http://www.goldencheetah.org/) and apart from the source code of this software I could not find any references to this algorithm anywhere. Nevertheless, this algorithm is the default algorithm for the calculation of W'balance in GoldenCheetah (The "W'bal formula" is by default set to "Differential" which corresponds to this algorithm, instead of "Integral", which corresponds to the Waterworth optimization of the Skiba algorithm).

\begin{equation}
W'_{bal}(t) =
    \sum_0^t (CP - P(t)) \cdot
    \begin{cases}
    1 & \text{where } P(t)>CP\\
    {\frac{W' - W'_{bal}(t-1)}{W'_{bal}(t-1)}} & \text{else}
    \end{cases}
\end{equation}

## Comparison

As an example I will create a workout for an athlete (CP=300 Watt, W'=20 kJ) which starts with a warming up of 10 minutes at 100 Watt, followed by 2 times 5 times 1 minute at 400 Watt with 1 minute rest at 100 Watt and 4 minutes rest between the two sets and ending with a cooling down of 10 minutes at 100 Watt. The power is plotted below.

```python
warming_up = [100] * 600
intervals = ([400]*60 + [100]*60)*5 + [100]*240 + ([400]*60 + [100]*60)*5
cooling_down = [100] * 600

wdf = WorkoutDataFrame(dict(
    power=warming_up + intervals + cooling_down
))
wdf.athlete = Athlete(cp=350, w_prime=20000)
```

```python
fig_power = wdf.power.plot.area()
fig_power.set_ylabel('power (Watt)')
fig_power.set_xlabel('time (seconds)');
```

```python
wdf = wdf.assign(
    w_balance_skiba=wdf.compute_w_prime_balance('skiba'),
    w_balance_waterworth=wdf.compute_w_prime_balance('waterworth'),
    w_balance_froncioni=wdf.compute_w_prime_balance('froncioni'),
)
```

I will start with comparing the two versions of the Skiba algorithm. First the plot showing $W'_{bal}$ for both algorithms. This plot shows that the difference between these to algorithms is very small, with the Waterworth plot covering the Skiba plot almost completely.

```python
fig_skiba_waterworth = wdf.loc[:, ['w_balance_skiba', 'w_balance_waterworth']].plot(ylim=0)
fig_skiba_waterworth.set_ylabel('W\'balance (Joule)')
fig_skiba_waterworth.set_xlabel('time (seconds)');
```

When the residual plot for these two algorithms is plotted it is obvious that the algorithms are almost identical with the maximum difference equal to 5.46 nanojoule...

```python
residuals = wdf.w_balance_skiba - wdf.w_balance_waterworth
fig_residuals = residuals.plot()
fig_residuals.set_xlabel('time (seconds)')
residuals.max()
```

Comparison of Skiba with Froncioni reveals large differences between these two algorithms in the plot below. Both the expenditure and the recovery of $W'_{bal}$ are faster for the Froncioni algorithm. The difference in the expenditure is especially striking because one would expect that it is linear and therefore identical for both algorithms. However, the difference can be explained by the fact that the Skiba algorithm assumes that although there is a netto decrease in $W'_{bal}$ for intervals where $P>CP$, there is also recovery of previously expended $W'$ during those intervals.

The difference in the recovery of $W'_{bal}$ is a consequence of how the recovery is modeled. For Skiba this is done with the term $e^{-\frac{t-u}{\tau_{W'}}}$ in the equation, where $\tau_{W'}$ regulates the speed of recovery. My expectation is that for different values of $\tau_{W'}$ (or different values for the constants numbers 546, -0.01 and 316) the recovery can be changed to similar speed as the Froncioni model. Similarly, the recovery speed in the Froncioni model is regulated by the term $\frac{W' - W'_{bal}(t-1)}{W'_{bal}(t-1)}$ and it is possible to change the recovery speed by multiplying this term by e.g. a factor $\alpha$: $\alpha*\frac{W' - W'_{bal}(t-1)}{W'_{bal}(t-1)}$. I will try to fit this modified algorithm to the Skiba algorithm in a different article.

```python
fig_skiba_froncioni = wdf.loc[:, ['w_balance_skiba', 'w_balance_froncioni']].plot()
fig_skiba_froncioni.set_ylabel('W\'balance (Joule)')
fig_skiba_froncioni.set_xlabel('time (seconds)');
```

## Conclusion

From the three algorithms that were compared, there were no differences between the original Skiba algorithm and the optimization of this algorithm by Waterworth. There were however big differences between both versions of the Skiba algorithm and the Froncioni/Clarke algorithm, with the Froncioni/Clarke algorithm showing faster expenditure and recovery of $W_{bal}$. It would be interesting to see if these differences can be minimized by adding a model parameter to the Froncioni algorithm and fitting it to experimental data comparable to Skiba et al. (2014)5.

## References

1. Jones, A. M., & Vanhatalo, A. (2017). The ‘Critical Power’Concept: Applications to Sports Performance with a Focus on Intermittent High-Intensity Exercise. Sports Medicine, 47(1), 65-78.
2. Skiba, P. F., Chidnok, W., Vanhatalo, A., & Jones, A. M. (2012). Modeling the expenditure and reconstitution of work capacity above critical power. Medicine and science in sports and exercise, 44(8), 1526-1532.
3. http://markliversedge.blogspot.nl/2014/10/wbal-optimisation-by-mathematician.html
4. GoldenCheetah source code: https://github.com/GoldenCheetah/GoldenCheetah/blob/160eb74cab712bfaa9eab64dd19310e5d42ed193/src/Metrics/WPrime.cpp#L257
5. Skiba, P. F., Jackman, S., Clarke, D., Vanhatalo, A., & Jones, A. M. (2014). Effect of work and recovery durations on W'reconstitution during intermittent exercise. Medicine and science in sports and exercise, 46(7), 1433-1440.

## Exporting images

```python
import numpy as np
figures = [fig_power, fig_skiba_waterworth, fig_residuals, fig_skiba_froncioni]
figure_names = ['fig_power', 'fig_skiba_waterworth', 'fig_residuals', 'fig_skiba_froncioni']
for fig, name in zip(figures, figure_names):
    if hasattr(fig, 'savefig'):
        f = fig
    elif isinstance(fig, np.ndarray):
        f = fig[0].get_figure()
    else:
        f = fig.get_figure()

    f.savefig(f'{name}.png', dpi=600)

```