Skip to content

PrEP

Bases: BaseFeature

Source code in titan/features/prep.py
 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
class Prep(base_feature.BaseFeature):
    name = "prep"
    stats = ["prep", "prep_new", "prep_injectable", "prep_oral"]
    """
        PrEP collects the following stats:

        * prep - number of agents with active PrEP
        * prep_new - number of agents who became active PrEP this time step
        * prep_injectable - number of agents on injectable PrEP
        * prep_oral - number of agents on oral PrEP
    """

    # class level attributes to track all Prep agents
    counts: ClassVar[Dict[str, int]] = {}

    def __init__(self, agent: "agent.Agent"):
        super().__init__(agent)
        # agent level attributes
        self.active = False
        self.adherent = False
        self.type = ""
        self.time = None
        self.last_dose_time: Optional[int] = None

    @classmethod
    def init_class(cls, params: "ObjMap"):
        """
        Initialize the counts dictionary for the races in the model.

        args:
            params: the population params
        """
        cls.counts = {race: 0 for race in params.classes.races}

    def init_agent(self, pop: "population.Population", time: int):
        """
        Initialize the agent for this feature during population initialization (`Population.create_agent`).  Called on only features that are enabled per the params.

        If an agent does not have HIV, is PrEP eligible, and time is at least the prep start time, they are randomly asigned to enroll in PrEP.

        args:
            pop: the population this agent is a part of
            time: the current time step
        """
        params = self.agent.location.params
        if self.eligible(time):
            if "Racial" in params.prep.target_model:
                if (
                    pop.pop_random.random()
                    < params.demographics[self.agent.race]
                    .sex_type[self.agent.sex_type]
                    .prep.init
                ):
                    self.enroll(pop.pop_random, time)
            elif pop.pop_random.random() < params.prep.init:
                self.enroll(pop.pop_random, time)

    def update_agent(self, model: "model.TITAN"):
        """
        Update the agent for this feature for a time step.  Called once per time step in `TITAN.update_all_agents`. Agent level updates are done after population level updates.   Called on only features that are enabled per the params.

        If the agent is not hiv and time is at least the prep start time, if the agent is already on PrEP update their PrEP attributes, if the agent isn't on PrEP and is eleigible, initiate PrEP.

        args:
            model: the instance of TITAN currently being run
        """
        if (
            not self.agent.hiv.active  # type: ignore[attr-defined]
            and model.time >= self.agent.location.params.prep.start_time
        ):
            if self.active:
                self.progress(model)
            elif self.eligible(model.time):
                self.initiate(model)

    @classmethod
    def add_agent(cls, agent: "agent.Agent"):
        """
        Add an agent to the class (not instance).

        Add agent to the PrEP counts by race, and add the agent to the set of new agents.

        args:
            agent: the agent to add to the class attributes
        """
        # set up if this is the first time being called
        cls.counts[agent.race] += 1

    @classmethod
    def remove_agent(cls, agent):
        """
        Remove an agent from the class (not instance).

        Decrement the prep counts by race.

        args:
            agent: the agent to remove from the class attributes
        """
        cls.counts[agent.race] -= 1

    def set_stats(self, stats: Dict[str, int], time: int):
        if self.active:
            stats["prep"] += 1

            if self.time == time:
                stats["prep_new"] += 1

            if self.type == "Inj":
                stats["prep_injectable"] += 1
            elif self.type == "Oral":
                stats["prep_oral"] += 1

    def get_acquisition_risk_multiplier(self, time: int, interaction_type: str):
        """
        Get a multiplier for how prep reduces risk of HIV acquisition.

        By default, returns 1.0

        args:
            time: the current model time step
            interaction_type: The type of interaction where the agent could acquire HIV (e.g. 'sex', 'injection' - from [params.classes.interaction_types])
        """
        if self.active and self.last_dose_time is not None:
            params = self.agent.location.params
            if self.type == "Oral":
                adherence = "adherent" if self.adherent else "non_adherent"
                return 1.0 - params.prep.efficacy[adherence]
            elif self.type == "Inj":
                annualized_last_dose_time = (
                    time - self.last_dose_time
                ) / params.model.time.steps_per_year
                annualized_half_life = params.prep.half_life / 365
                load = params.prep.peak_load * (
                    (0.5) ** (annualized_last_dose_time / annualized_half_life)
                )
                return np.exp(-5.528636721 * load)

        return 1.0

    # =============== HELPER METHODS ===================

    def initiate(self, model: "model.TITAN", force: bool = False):
        """
        Place agents onto PrEP treatment. PrEP treatment assumes that the agent knows their HIV status is negative.

        args:
            model : instance of TITAN being run
            force : whether to force the agent to enroll instead of using the appropriate algorithm per the prep params
        """
        # Prep only valid for agents not on prep and are HIV negative
        if self.active or self.agent.hiv.active:  # type: ignore[attr-defined]
            return

        params = self.agent.location.params

        if force:
            self.enroll(model.run_random, model.time)
        elif params.prep.cap_as_prob:
            if "Racial" in params.prep.target_model:
                if (
                    model.run_random.random()
                    <= params.demographics[self.agent.race]
                    .sex_type[self.agent.sex_type]
                    .prep.cap
                ):
                    self.enroll(model.run_random, model.time)
            else:
                if model.run_random.random() <= params.prep.cap:
                    self.enroll(model.run_random, model.time)
        else:
            if "Racial" in params.prep.target_model:
                num_prep_agents = self.counts[self.agent.race]
                all_hiv_agents = exposures.HIV.agents
                all_race = {
                    a for a in model.pop.all_agents if a.race == self.agent.race
                }

                num_hiv_agents = len(all_hiv_agents & all_race)
                target_prep = (len(all_race) - num_hiv_agents) * params.demographics[
                    self.agent.race
                ].sex_type[self.agent.sex_type].prep.cap
            else:
                num_prep_agents = sum(self.counts.values())
                target_prep = int(
                    (model.pop.all_agents.num_members() - len(exposures.HIV.agents))
                    * params.prep.cap
                )

            if num_prep_agents < target_prep:
                self.enroll(model.run_random, model.time)

    def enroll(self, rand_gen, time):
        """
        Enroll an agent in PrEP

        args:
            rand_gen: random number generator
        """
        params = self.agent.location.params

        self.active = True
        self.time = time
        self.last_dose_time = time

        self.adherent = (
            rand_gen.random()
            < params.demographics[self.agent.race]
            .sex_type[self.agent.sex_type]
            .prep.adherence
        )

        if "Inj" in params.prep.type and "Oral" in params.prep.type:
            if rand_gen.random() < params.prep.lai.prob:
                self.type = "Inj"
            else:
                self.type = "Oral"

        else:
            self.type = params.prep.type[0]

        self.add_agent(self.agent)

    def progress(self, model: "model.TITAN", force: bool = False):
        """
        Update agent's PrEP status and discontinue stochastically or if `force` is True

        args:
            model: instance of the TITAN being run
            force: whether to force discontinuation of PrEP
        """
        if force:
            self.discontinue()  # TO_REVIEW should this just remove the agent from counts, or discontinue? does it depend on type?
            return

        if self.type == "Oral":
            if (
                model.run_random.random()
                < self.agent.location.params.demographics[self.agent.race]
                .sex_type[self.agent.sex_type]
                .prep.discontinue
            ):
                self.discontinue()
            else:
                self.last_dose_time = model.time

        # TO_REVIEW should inj prep have a way to continue at the year mark (besides maybe getting prep again through the normal channels of enrollment)?
        if (
            self.type == "Inj"
            and self.last_dose_time
            + self.agent.location.params.model.time.steps_per_year
            == model.time
        ):
            self.discontinue()

    def discontinue(self):
        """
        Discontinue PrEP usage
        """
        self.active = False
        self.type = ""
        self.time = None
        self.last_dose_time = None

        self.remove_agent(self.agent)

    def eligible(self, time) -> bool:
        """
        Determine if an agent is eligible for PrEP

        returns:
            whether the agent is eligible
        """
        params = self.agent.location.params
        if self.agent.hiv.active or time < params.prep.start_time:  # type: ignore[attr-defined]
            return False

        target_model = params.prep.target_model
        gender = params.classes.sex_types[self.agent.sex_type].gender

        if (
            self.active
            or self.agent.vaccine.active  # type: ignore[attr-defined]
            or params.features.random_trial
        ):
            return False

        all_eligible_models = {"Allcomers", "Racial"}

        if all_eligible_models.intersection(target_model):
            return True

        if "cdc_women" in target_model:
            if gender == "F":
                if self.cdc_eligible():
                    return True

        if "cdc_msm" in target_model:
            if gender == "M" and self.cdc_eligible():
                return True

        if "pwid_sex" in target_model:
            if self.agent.drug_type == "Inj" and self.cdc_eligible():
                return True

        if "pwid" in target_model:
            if self.agent.drug_type == "Inj":
                return True

        if "ssp_sex" in target_model:
            if self.agent.syringe_services.active and self.cdc_eligible():  # type: ignore[attr-defined]
                return True

        if "ssp" in target_model:
            if self.agent.syringe_services.active:  # type: ignore[attr-defined]
                return True

        return False

    def cdc_eligible(self) -> bool:
        """
        Determine agent eligibility for PrEP under CDC criteria

        returns:
            cdc eligibility
        """
        if self.agent.is_msm():
            return True

        ongoing_duration = self.agent.location.params.partnership.ongoing_duration
        for rel in self.agent.relationships:
            partner = rel.get_partner(self.agent)
            if rel.duration > ongoing_duration and partner.hiv.dx:  # type: ignore[attr-defined]
                return True

            if partner.drug_type == "Inj" or partner.is_msm():
                return True

        return False

stats = ['prep', 'prep_new', 'prep_injectable', 'prep_oral'] class-attribute instance-attribute

PrEP collects the following stats:

  • prep - number of agents with active PrEP
  • prep_new - number of agents who became active PrEP this time step
  • prep_injectable - number of agents on injectable PrEP
  • prep_oral - number of agents on oral PrEP

add_agent(agent) classmethod

Add an agent to the class (not instance).

Add agent to the PrEP counts by race, and add the agent to the set of new agents.

Parameters:

Name Type Description Default
agent Agent

the agent to add to the class attributes

required
Source code in titan/features/prep.py
88
89
90
91
92
93
94
95
96
97
98
99
@classmethod
def add_agent(cls, agent: "agent.Agent"):
    """
    Add an agent to the class (not instance).

    Add agent to the PrEP counts by race, and add the agent to the set of new agents.

    args:
        agent: the agent to add to the class attributes
    """
    # set up if this is the first time being called
    cls.counts[agent.race] += 1

cdc_eligible()

Determine agent eligibility for PrEP under CDC criteria

Returns:

Type Description
bool

cdc eligibility

Source code in titan/features/prep.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def cdc_eligible(self) -> bool:
    """
    Determine agent eligibility for PrEP under CDC criteria

    returns:
        cdc eligibility
    """
    if self.agent.is_msm():
        return True

    ongoing_duration = self.agent.location.params.partnership.ongoing_duration
    for rel in self.agent.relationships:
        partner = rel.get_partner(self.agent)
        if rel.duration > ongoing_duration and partner.hiv.dx:  # type: ignore[attr-defined]
            return True

        if partner.drug_type == "Inj" or partner.is_msm():
            return True

    return False

discontinue()

Discontinue PrEP usage

Source code in titan/features/prep.py
267
268
269
270
271
272
273
274
275
276
def discontinue(self):
    """
    Discontinue PrEP usage
    """
    self.active = False
    self.type = ""
    self.time = None
    self.last_dose_time = None

    self.remove_agent(self.agent)

eligible(time)

Determine if an agent is eligible for PrEP

Returns:

Type Description
bool

whether the agent is eligible

Source code in titan/features/prep.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def eligible(self, time) -> bool:
    """
    Determine if an agent is eligible for PrEP

    returns:
        whether the agent is eligible
    """
    params = self.agent.location.params
    if self.agent.hiv.active or time < params.prep.start_time:  # type: ignore[attr-defined]
        return False

    target_model = params.prep.target_model
    gender = params.classes.sex_types[self.agent.sex_type].gender

    if (
        self.active
        or self.agent.vaccine.active  # type: ignore[attr-defined]
        or params.features.random_trial
    ):
        return False

    all_eligible_models = {"Allcomers", "Racial"}

    if all_eligible_models.intersection(target_model):
        return True

    if "cdc_women" in target_model:
        if gender == "F":
            if self.cdc_eligible():
                return True

    if "cdc_msm" in target_model:
        if gender == "M" and self.cdc_eligible():
            return True

    if "pwid_sex" in target_model:
        if self.agent.drug_type == "Inj" and self.cdc_eligible():
            return True

    if "pwid" in target_model:
        if self.agent.drug_type == "Inj":
            return True

    if "ssp_sex" in target_model:
        if self.agent.syringe_services.active and self.cdc_eligible():  # type: ignore[attr-defined]
            return True

    if "ssp" in target_model:
        if self.agent.syringe_services.active:  # type: ignore[attr-defined]
            return True

    return False

enroll(rand_gen, time)

Enroll an agent in PrEP

Parameters:

Name Type Description Default
rand_gen

random number generator

required
Source code in titan/features/prep.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def enroll(self, rand_gen, time):
    """
    Enroll an agent in PrEP

    args:
        rand_gen: random number generator
    """
    params = self.agent.location.params

    self.active = True
    self.time = time
    self.last_dose_time = time

    self.adherent = (
        rand_gen.random()
        < params.demographics[self.agent.race]
        .sex_type[self.agent.sex_type]
        .prep.adherence
    )

    if "Inj" in params.prep.type and "Oral" in params.prep.type:
        if rand_gen.random() < params.prep.lai.prob:
            self.type = "Inj"
        else:
            self.type = "Oral"

    else:
        self.type = params.prep.type[0]

    self.add_agent(self.agent)

get_acquisition_risk_multiplier(time, interaction_type)

Get a multiplier for how prep reduces risk of HIV acquisition.

By default, returns 1.0

Parameters:

Name Type Description Default
time int

the current model time step

required
interaction_type str

The type of interaction where the agent could acquire HIV (e.g. 'sex', 'injection' - from [params.classes.interaction_types])

required
Source code in titan/features/prep.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def get_acquisition_risk_multiplier(self, time: int, interaction_type: str):
    """
    Get a multiplier for how prep reduces risk of HIV acquisition.

    By default, returns 1.0

    args:
        time: the current model time step
        interaction_type: The type of interaction where the agent could acquire HIV (e.g. 'sex', 'injection' - from [params.classes.interaction_types])
    """
    if self.active and self.last_dose_time is not None:
        params = self.agent.location.params
        if self.type == "Oral":
            adherence = "adherent" if self.adherent else "non_adherent"
            return 1.0 - params.prep.efficacy[adherence]
        elif self.type == "Inj":
            annualized_last_dose_time = (
                time - self.last_dose_time
            ) / params.model.time.steps_per_year
            annualized_half_life = params.prep.half_life / 365
            load = params.prep.peak_load * (
                (0.5) ** (annualized_last_dose_time / annualized_half_life)
            )
            return np.exp(-5.528636721 * load)

    return 1.0

init_agent(pop, time)

Initialize the agent for this feature during population initialization (Population.create_agent). Called on only features that are enabled per the params.

If an agent does not have HIV, is PrEP eligible, and time is at least the prep start time, they are randomly asigned to enroll in PrEP.

Parameters:

Name Type Description Default
pop Population

the population this agent is a part of

required
time int

the current time step

required
Source code in titan/features/prep.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def init_agent(self, pop: "population.Population", time: int):
    """
    Initialize the agent for this feature during population initialization (`Population.create_agent`).  Called on only features that are enabled per the params.

    If an agent does not have HIV, is PrEP eligible, and time is at least the prep start time, they are randomly asigned to enroll in PrEP.

    args:
        pop: the population this agent is a part of
        time: the current time step
    """
    params = self.agent.location.params
    if self.eligible(time):
        if "Racial" in params.prep.target_model:
            if (
                pop.pop_random.random()
                < params.demographics[self.agent.race]
                .sex_type[self.agent.sex_type]
                .prep.init
            ):
                self.enroll(pop.pop_random, time)
        elif pop.pop_random.random() < params.prep.init:
            self.enroll(pop.pop_random, time)

init_class(params) classmethod

Initialize the counts dictionary for the races in the model.

Parameters:

Name Type Description Default
params ObjMap

the population params

required
Source code in titan/features/prep.py
37
38
39
40
41
42
43
44
45
@classmethod
def init_class(cls, params: "ObjMap"):
    """
    Initialize the counts dictionary for the races in the model.

    args:
        params: the population params
    """
    cls.counts = {race: 0 for race in params.classes.races}

initiate(model, force=False)

Place agents onto PrEP treatment. PrEP treatment assumes that the agent knows their HIV status is negative.

Parameters:

Name Type Description Default
model

instance of TITAN being run

required
force

whether to force the agent to enroll instead of using the appropriate algorithm per the prep params

False
Source code in titan/features/prep.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def initiate(self, model: "model.TITAN", force: bool = False):
    """
    Place agents onto PrEP treatment. PrEP treatment assumes that the agent knows their HIV status is negative.

    args:
        model : instance of TITAN being run
        force : whether to force the agent to enroll instead of using the appropriate algorithm per the prep params
    """
    # Prep only valid for agents not on prep and are HIV negative
    if self.active or self.agent.hiv.active:  # type: ignore[attr-defined]
        return

    params = self.agent.location.params

    if force:
        self.enroll(model.run_random, model.time)
    elif params.prep.cap_as_prob:
        if "Racial" in params.prep.target_model:
            if (
                model.run_random.random()
                <= params.demographics[self.agent.race]
                .sex_type[self.agent.sex_type]
                .prep.cap
            ):
                self.enroll(model.run_random, model.time)
        else:
            if model.run_random.random() <= params.prep.cap:
                self.enroll(model.run_random, model.time)
    else:
        if "Racial" in params.prep.target_model:
            num_prep_agents = self.counts[self.agent.race]
            all_hiv_agents = exposures.HIV.agents
            all_race = {
                a for a in model.pop.all_agents if a.race == self.agent.race
            }

            num_hiv_agents = len(all_hiv_agents & all_race)
            target_prep = (len(all_race) - num_hiv_agents) * params.demographics[
                self.agent.race
            ].sex_type[self.agent.sex_type].prep.cap
        else:
            num_prep_agents = sum(self.counts.values())
            target_prep = int(
                (model.pop.all_agents.num_members() - len(exposures.HIV.agents))
                * params.prep.cap
            )

        if num_prep_agents < target_prep:
            self.enroll(model.run_random, model.time)

progress(model, force=False)

Update agent's PrEP status and discontinue stochastically or if force is True

Parameters:

Name Type Description Default
model TITAN

instance of the TITAN being run

required
force bool

whether to force discontinuation of PrEP

False
Source code in titan/features/prep.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def progress(self, model: "model.TITAN", force: bool = False):
    """
    Update agent's PrEP status and discontinue stochastically or if `force` is True

    args:
        model: instance of the TITAN being run
        force: whether to force discontinuation of PrEP
    """
    if force:
        self.discontinue()  # TO_REVIEW should this just remove the agent from counts, or discontinue? does it depend on type?
        return

    if self.type == "Oral":
        if (
            model.run_random.random()
            < self.agent.location.params.demographics[self.agent.race]
            .sex_type[self.agent.sex_type]
            .prep.discontinue
        ):
            self.discontinue()
        else:
            self.last_dose_time = model.time

    # TO_REVIEW should inj prep have a way to continue at the year mark (besides maybe getting prep again through the normal channels of enrollment)?
    if (
        self.type == "Inj"
        and self.last_dose_time
        + self.agent.location.params.model.time.steps_per_year
        == model.time
    ):
        self.discontinue()

remove_agent(agent) classmethod

Remove an agent from the class (not instance).

Decrement the prep counts by race.

Parameters:

Name Type Description Default
agent

the agent to remove from the class attributes

required
Source code in titan/features/prep.py
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def remove_agent(cls, agent):
    """
    Remove an agent from the class (not instance).

    Decrement the prep counts by race.

    args:
        agent: the agent to remove from the class attributes
    """
    cls.counts[agent.race] -= 1

update_agent(model)

Update the agent for this feature for a time step. Called once per time step in TITAN.update_all_agents. Agent level updates are done after population level updates. Called on only features that are enabled per the params.

If the agent is not hiv and time is at least the prep start time, if the agent is already on PrEP update their PrEP attributes, if the agent isn't on PrEP and is eleigible, initiate PrEP.

Parameters:

Name Type Description Default
model TITAN

the instance of TITAN currently being run

required
Source code in titan/features/prep.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def update_agent(self, model: "model.TITAN"):
    """
    Update the agent for this feature for a time step.  Called once per time step in `TITAN.update_all_agents`. Agent level updates are done after population level updates.   Called on only features that are enabled per the params.

    If the agent is not hiv and time is at least the prep start time, if the agent is already on PrEP update their PrEP attributes, if the agent isn't on PrEP and is eleigible, initiate PrEP.

    args:
        model: the instance of TITAN currently being run
    """
    if (
        not self.agent.hiv.active  # type: ignore[attr-defined]
        and model.time >= self.agent.location.params.prep.start_time
    ):
        if self.active:
            self.progress(model)
        elif self.eligible(model.time):
            self.initiate(model)