Domain: Biology — Evolutionary Biology

Problem:
We consider a viral quasispecies composed of four competing genomic variants $V_1,V_2,V_3,V_4$, corresponding to haplotypes (AB, Ab, aB, ab) at two linked loci. The population frequency vector is
\[\mathbf{P}(t) = \bigl(p_1(t),\; p_2(t),\; p_3(t),\; p_4(t)\bigr),\]
and evolution is simulated for $G=500$ discrete replicative cycles ($t=0,1,\dots,499$). Each cycle consists of two sequential phases:

1. Antagonistic frequency-dependent selection (Phase 1), and
2. Genomic reassortment / recombination (Phase 2).

Initial frequencies at $t=0$ are
\[\mathbf{P}(0) = \bigl(0.35,\;0.15,\;0.15,\;0.35\bigr).\]

## Phase 1: Antagonistic frequency-dependent selection

For each variant $i\in\{1,2,3,4\}$ define its instantaneous fitness
\[\Phi_i(t) = \Phi_{\mathrm{base},i} + \beta_i(t),\]
where $\Phi_{\mathrm{base},i}$ is the base fitness of variant $i$ and $\beta_i(t)$ is a *resonance bonus* that depends negatively on the frequency of its antagonist. The antagonist pairs are $V_1\leftrightarrow V_4$ and $V_2\leftrightarrow V_3$, and the bonuses are
\[\beta_1(t) = \sigma\bigl(1 - p_4(t)\bigr)^2,\]
\[\beta_2(t) = \sigma\bigl(1 - p_3(t)\bigr)^2,\]
\[\beta_3(t) = \sigma\bigl(1 - p_2(t)\bigr)^2,\]
\[\beta_4(t) = \sigma\bigl(1 - p_1(t)\bigr)^2.\]

The mean population fitness is
\[\bar{\Phi}(t) = \sum_{i=1}^4 p_i(t)\,\Phi_i(t).\]
After selection, the post-selection (intermediate) frequencies are
\[p_i^*(t) = \frac{p_i(t)\,\Phi_i(t)}{\bar{\Phi}(t)},\qquad i=1,\dots,4.\]

## Phase 2: Reassortment / Recombination

Compute linkage disequilibrium from the post-selection frequencies:
\[
D(t) = p_1^*(t)\,p_4^*(t) - p_2^*(t)\,p_3^*(t).
\]
Recombination with rate $r$ transforms the post-selection vector $\mathbf{P}^*(t)$ into the next-generation frequencies $\mathbf{P}(t+1)$ by
\[
\begin{array}{rcl}
p_1(t+1) & = & p_1^*(t) - r\,D(t) \\
p_2(t+1) & = & p_2^*(t) + r\,D(t) \\
p_3(t+1) & = & p_3^*(t) + r\,D(t) \\
p_4(t+1) & = & p_4^*(t) - r\,D(t)
\end{array}
\]

## Task

Run the simulation for $G=500$ cycles (i.e. iterate the two-phase process from $t=0$ up to $t=499$) and compute the final frequency vector $\mathbf{P}(500)$. Using $\mathbf{P}(500)$, report the following terminal metrics (rounded to three decimal places):

1. Final mean fitness
\[\bar{\Phi}_{\text{final}} = \sum_{i=1}^4 p_i(500)\,\Phi_i(500),\]
where $\Phi_i(500)=\Phi_{\mathrm{base},i}+\beta_i(500)$ computed using $\mathbf{P}(500)$; and

2. Final linkage disequilibrium
\[D_{\text{final}} = p_1(500)p_4(500) - p_2(500)p_3(500).\]

Return the result as a two-element list
\[ \bigl[\bar{\Phi}_{\text{final}},\; D_{\text{final}}\bigr],\]
with both values rounded to three decimal places.

## Model parameters and constants

- Number of cycles: $G=500$.
- Reassortment rate: $r=0.03$.
- Antagonistic selection coefficient: $\sigma=0.20$.
- Initial frequencies: $\mathbf{P}(0) = [0.35,\;0.15,\;0.15,\;0.35]$.
- Base fitness vector: $\Phi_{\mathrm{base}} = [1.00,\;1.05,\;1.05,\;1.00]$.

## Implementation note

Implement the simulation using only the `numpy`, `pandas`, and `scipy` libraries. Ensure numerical stability (e.g. avoid division by zero) when computing $\bar{\Phi}(t)$.

Answer: $[1.229, -0.162]$

Solution code:
import numpy as np
import pandas as pd
import scipy

def run_viral_simulation():
    
    G = 500              
    r = 0.03             
    sigma = 0.20         
    P = np.array([0.35, 0.15, 0.15, 0.35], dtype=np.float64)
    
    
    Phi_base = np.array([1.00, 1.05, 1.05, 1.00], dtype=np.float64)

    
    for _ in range(G):
        
        
        p1, p2, p3, p4 = P[0], P[1], P[2], P[3]
        
        
        beta = np.array([
            sigma * (1 - p4)**2,  
            sigma * (1 - p3)**2,  
            sigma * (1 - p2)**2,  
            sigma * (1 - p1)**2   
        ])
        
        
        Phi = Phi_base + beta
        
        
        Phi_bar = np.dot(P, Phi)
        
        
        P_star = (P * Phi) / Phi_bar
        
        
        D = P_star[0] * P_star[3] - P_star[1] * P_star[2]
        
        
        P = np.array([
            P_star[0] - r * D,
            P_star[1] + r * D,
            P_star[2] + r * D,
            P_star[3] - r * D
        ])

    
    p1_final, p2_final, p3_final, p4_final = P[0], P[1], P[2], P[3]
    
    
    beta_final = np.array([
        sigma * (1 - p4_final)**2,
        sigma * (1 - p3_final)**2,
        sigma * (1 - p2_final)**2,
        sigma * (1 - p1_final)**2
    ])
    
    
    Phi_final = Phi_base + beta_final
    
    
    Phi_bar_final = np.dot(P, Phi_final)
    
    
    D_final = P[0] * P[3] - P[1] * P[2]
    
    
    result = [round(Phi_bar_final, 3), round(D_final, 3)]
    return result

if __name__ == "__main__":
    final_metrics = run_viral_simulation()
    print(final_metrics)