def covid_psi(I, K_F, K_S, delta=0, EPS=1E-15, reflect=False): """ COVID delay ratio function: psi(t, delta T) = (I * K_F)(t + delta T) / (I * K_S)(t), where * means convolution. Convolution is calculated here via discrete convolution. Args: I : cumulative infections distribution K_F : sampled delay kernel function in the numerator K_S : sampled delay kernel function in the denominator delta : numerator time shift [indices of t] Returns: Ratio function """ N = len(I) # Handle the long tail by zero padding. # Kernel array should be a long enough, because it provides reference. I_zp = tools.zeropad_after(x=I, reference=K_F) # Compute via discrete convolutions # Kernel normalization to sum=1 guarantees count normalization IKF = tools.conv_(I_zp, K_F / np.sum(K_F))[0:N] IKS = tools.conv_(I_zp, K_S / np.sum(K_S))[0:N] # Numerator is read out at t + deltaT psi = IKF[delta:] / np.maximum(IKS[0:N - delta], EPS) out = np.zeros(N) out[0:len(psi)] = psi if reflect: # Continue with the boundary value out[len(psi):] = psi[-1] return out
# ** Normalize discretized kernel to sum to one # => count conservation with discrete convolutions ** kernel_REV = copy.deepcopy(kernel_C_REV) kernel_REV /= np.sum(kernel_REV) # Plot kernels fig, ax = plt.subplots() plt.plot(t, kernel_C) plt.plot(t, kernel_C_REV) #plt.show() # ------------------------------------------------------------------------ # Discrete convolution # Seroconversion counts dI_conv = tools.conv_(dI, kernel) # Seroreversion decay of converted counts dI_conv_REV = tools.conv_(dI_conv, kernel_REV) # Cumulative sum I_S = tools.conv_(I, kernel) I_RS = tools.conv_(I_S, kernel_REV) # Observed counts I_tilde = I_S - I_RS # ------------------------------------------------------------------------ # Plots XLIM = 300
# ======================================================================== # Capture ratios # True IFR value IFR_true = 0.5 * 1e-2 # Synthetic input I = 1 / (1 + np.exp(-0.25*(t-20))) # ** Unit normalized kernel for a discrete convolution ** KF_kernel = copy.deepcopy(K['F']); KF_kernel /= np.sum(KF_kernel) # Discrete convolution F = IFR_true * tools.conv_(I, KF_kernel) # ----------------------------------------- fig,ax = plt.subplots() plt.plot(t, I, label='$I$') plt.plot(t, F / IFR_true, label='$(I \\ast K_F)(t) / \\langle IFR \\rangle$') plt.xlabel('$t$ [days]') plt.title('diagnostics') plt.ylim([0, 1.2]) plt.xlim([0, None]) plt.legend() os.makedirs(f'{plotfolder}', exist_ok = True) plt.savefig(f'{plotfolder}/conv_diagnostic.pdf', bbox_inches='tight')
def covid_deconvolve(Cdiff, Fdiff, kp, t=None, mode='C', alpha=1, BS=1000, TSPAN=200, data_poisson=True, kernel_syst=True): """ COVID time-series deconvolution. Args: Cdiff: observed daily cases array Fdiff: observed daily fatalities array kp: kernel parameters dictionary t: time points array, default None (constructed automatically) mode: use 'C' for invertion based on cases or 'F' for fatality based alpha: regularization strength for the deconvolution BS: number of bootstrap/MC samples TSPAN: minimum convolution domain span of the kernel data_poisson: measurement statistical bootstrap fluctuation on / off kernel_syst: kernel systematic uncertainties on / off Returns: Id_hat: daily infections estimate obtained via deconvolution Fd_hat: daily fatalities from push-forward of the estimate: (K_F * dI/dt)(t) """ print(__name__ + '.covid_deconvolve: Running ...') if len(Cdiff) != len(Fdiff): raise Exception('covid_deconvolve: input C length != F length') if t is None: # Construct convolution domain t = np.arange(0, len(Fdiff) + TSPAN) # Monte Carlo re-sampling of perturbed kernels Id_hat = np.zeros((BS, len(Cdiff))) Fd_hat = np.zeros((BS, len(Fdiff))) for i in tqdm(range(BS)): # Generate new kernel if kernel_syst: K = covid_kernels(t=t, mu=kp['mu'], sigma=kp['sigma'], mu_std=kp['mu_std'], sigma_std=kp['sigma_std']) else: K = covid_kernels(t=t, mu=kp['mu'], sigma=kp['sigma'], mu_std=None, sigma_std=None) # Deconvolution if mode == 'C': if data_poisson: y = np.random.poisson(Cdiff) # Poisson fluctuate measurement else: y = Cdiff y_zp = tools.zeropad_after( x=y, reference=K['C']) # Take care of the tail unrolling output, _, _ = tools.nneg_tikhonov_deconv(y=y_zp, kernel=K['C'], alpha=alpha, mass_conserve=True) Id_hat[i, :] = output[0:len(y)] elif mode == 'F': if data_poisson: y = np.random.poisson(Fdiff) # Poisson fluctuate measurement else: y = Fdiff y_zp = tools.zeropad_after( x=y, reference=K['F']) # Take care the tail unrolling output, _, _ = tools.nneg_tikhonov_deconv(y=y_zp, kernel=K['F'], alpha=alpha, mass_conserve=True) Id_hat[i, :] = output[0:len(y)] else: raise Except(__name__ + '.covid_deconvolve: Error: unknown mode.') # Push-forward, take care of the tail unrolling by zeropad N = len(Id_hat[i, :]) I_zp = tools.zeropad_after(x=Id_hat[i, :], reference=K['F']) F_diff_hat = tools.conv_(I_zp, K['F'])[0:N] # Normalization for visualization and comparisons # (absolute normalization not obtained by convolution alone) if np.sum(Id_hat[i, :]) > 0: Id_hat[i, :] /= np.sum(Id_hat[i, :]) if np.sum(F_diff_hat) > 0: F_diff_hat /= np.sum(F_diff_hat) Id_hat[i, :] *= np.sum(Cdiff) F_diff_hat *= np.sum(Fdiff) Fd_hat[i, :] = F_diff_hat return Id_hat, Fd_hat
qval = [1.35, 1.15, 0.95, 0.75, 0.5, 0.25] k = 0 for key in tqdm(I.keys()): fig,ax = plt.subplots() for q in qval: # Unit normalization of the kernel for discrete convolution kernelfunc = copy.deepcopy(K['F']); kernelfunc /= np.sum(kernelfunc) # Discrete convolution I_del = tools.conv_(I[key], kernelfunc) print(f'max(I) = {np.max(I[key])}, max(I_del) = {np.max(I_del)}') y = tools.find_delay(t=t, F=I[key], Fd=I_del, rho=q) plt.plot(t, y, label=f'$\\epsilon = {q:0.2f}$', color=cm.RdBu(1-q/np.max(qval))) plt.legend(loc=1) plt.ylim([0,None]) plt.xlim([0,100]) plt.xticks(np.arange(0,110,10)) plt.ylabel('$\\Delta t$ [days] | $\\frac{(K \\ast I)(t + \\Delta t)}{I(t)} = \\epsilon$') plt.xlabel('$t$ [days]') plt.title(f'${key}$')