File size: 4,545 Bytes
7b1174f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
039d001
7b1174f
039d001
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b1174f
 
 
 
 
 
 
039d001
7b1174f
 
 
039d001
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b1174f
 
 
 
 
 
 
 
 
 
039d001
7b1174f
 
 
039d001
 
 
 
 
 
 
 
 
 
 
 
7b1174f
 
 
 
 
039d001
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b1174f
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Tuple

import numpy as np
import pandas as pd

from .sexual_debut import SexualDebutModel


@dataclass(slots=True)
class Campaign:
    age_from: int
    age_to: int
    coverage: float


def age_coverage(
    ages: Iterable[int],
    routine_coverage: float,
    vaccine_efficacy_before: float,
    vaccine_efficacy_after: float,
    campaigns: Optional[List[Campaign]],
    sexual_debut_model: SexualDebutModel,
    age_at_vaccination: int,
    protection_duration_years: Optional[int] = None,
) -> pd.DataFrame:
    """
    Calculate age-specific vaccination coverage.
    
    Args:
        ages: Iterable of ages to calculate coverage for
        routine_coverage: Routine vaccination coverage rate
        vaccine_efficacy_before: Efficacy for those not yet sexually active
        vaccine_efficacy_after: Efficacy for those already sexually active
        campaigns: Optional list of catch-up campaigns
        sexual_debut_model: Model for sexual debut probabilities
        age_at_vaccination: Target age for routine vaccination
        protection_duration_years: Years of protection (None = lifelong)
        
    Returns:
        DataFrame with 'age' and 'coverage' columns
    """
    ages_arr = np.asarray(list(ages), dtype=int)
    cov = np.zeros_like(ages_arr, dtype=float)

    # Routine at target age
    idx = np.where(ages_arr == age_at_vaccination)[0]
    if idx.size:
        p_debut = sexual_debut_model.cdf(age_at_vaccination)
        base_cov = (
            routine_coverage * vaccine_efficacy_before * (1.0 - p_debut)
            + routine_coverage * vaccine_efficacy_after * p_debut
        )
        cov[idx[0]] = base_cov
        
        # Apply coverage to all ages after vaccination, with waning if specified
        for i, age in enumerate(ages_arr):
            if age > age_at_vaccination:
                years_since_vac = age - age_at_vaccination
                
                # Calculate waning multiplier (1.0 if unlimited protection)
                if protection_duration_years is None:
                    waning_multiplier = 1.0  # Lifelong protection
                else:
                    waning_multiplier = calculate_waning_multiplier(
                        years_since_vac, protection_duration_years
                    )
                
                # Base coverage remains the same as at vaccination age
                # (coverage is set at vaccination and persists, modulated by waning)
                cov[i] = base_cov * waning_multiplier

    # Campaigns
    if campaigns:
        for camp in campaigns:
            mask = (ages_arr >= camp.age_from) & (ages_arr <= camp.age_to)
            if not np.any(mask):
                continue
            # Use debut at midpoint age
            mid_age = float((camp.age_from + camp.age_to) / 2.0)
            p_debut = sexual_debut_model.cdf(mid_age)
            base_camp_cov = (
                camp.coverage * vaccine_efficacy_before * (1.0 - p_debut)
                + camp.coverage * vaccine_efficacy_after * p_debut
            )
            
            # Apply waning for campaign coverage
            if protection_duration_years is not None:
                for i, age in enumerate(ages_arr):
                    if mask[i]:
                        years_since_vac = max(0, age - mid_age)
                        waning_multiplier = calculate_waning_multiplier(
                            years_since_vac, protection_duration_years
                        )
                        cov[i] += base_camp_cov * waning_multiplier
            else:
                cov[mask] += base_camp_cov

    cov = np.clip(cov, 0.0, 1.0)
    return pd.DataFrame({"age": ages_arr, "coverage": cov})


def calculate_waning_multiplier(
    years_since_vaccination: float, protection_duration_years: int
) -> float:
    """
    Calculate the waning multiplier based on years since vaccination.
    
    Uses a step function model:
    - Full protection (multiplier = 1.0) for years < protection_duration_years
    - No protection (multiplier = 0.0) for years >= protection_duration_years
    
    Args:
        years_since_vaccination: Years elapsed since vaccination
        protection_duration_years: Duration of vaccine protection in years
        
    Returns:
        Waning multiplier (0.0 to 1.0)
    """
    if years_since_vaccination < protection_duration_years:
        return 1.0
    else:
        return 0.0