#!/usr/bin/env python3
import numpy as np
import starsim as ss
from enum import IntEnum
__all__ = ['HIVState', 'HIV']
# Define HIV states as an enumeration.
[docs]
class HIVState(IntEnum):
"""
Enum representing the possible HIV states an agent can be in.
States:
- ATRISK: Agent is HIV-negative but at risk.
- ACUTE: Recently infected with HIV.
- LATENT: Chronic HIV infection.
- AIDS: Advanced stage of HIV infection.
"""
ATRISK = 0 # Uninfected
ACUTE = 1 # Newly infected (early state)
LATENT = 2 # Chronic infection
AIDS = 3 # Advanced disease
def __str__(self):
return {0: 'ATRISK', 1: 'ACUTE', 2: 'LATENT', 3: 'AIDS'}[self.value]
def __repr__(self):
return {0: 'ATRISK', 1: 'ACUTE', 2: 'LATENT', 3: 'AIDS'}[self.value]
[docs]
class HIV(ss.Disease):
"""
A simplified agent-based HIV disease model for use with the Starsim framework.
This model tracks HIV state progression through ACUTE, LATENT, and AIDS phases,
influenced by whether the agent is receiving ART (antiretroviral therapy).
Key Features:
- Initial infection and ART status are assigned during the first timestep,
unless a high-level intervention labeled 'hivinterventions' is present.
- Disease progression is stochastic and modified by ART presence.
- ART reduces the probability of progression from ACUTE → LATENT and LATENT → AIDS.
- AIDS → DEAD transition is defined but not applied in this model.
Parameters:
- init_prev: Initial probability of infection (ACUTE).
- init_onart: Probability of being on ART at initialization (if infected).
- acute_to_latent: Daily transition probability from ACUTE to LATENT.
- latent_to_aids: Daily transition probability from LATENT to AIDS.
- aids_to_dead: Daily transition probability from AIDS to DEAD (unused).
- art_progression_factor: Multiplier applied to progression probabilities for agents on ART.
States:
- state: HIV progression state (ATRISK, ACUTE, LATENT, AIDS, DEAD).
- on_ART: Boolean indicating whether agent is on ART.
Results Tracked:
- hiv_prevalence: Proportion of total agents with HIV.
- infected: Total number of HIV-positive agents.
- on_art: Number of agents on ART.
- atrisk, acute, latent, aids: Percent of population in each state.
- n_active: Total number of agents in ACUTE, LATENT, or AIDS states.
"""
[docs]
def __init__(self, pars=None, **kwargs):
super().__init__(**kwargs)
# Define progression parameters (using a time step in weeks).
self.define_pars(
init_prev = ss.bernoulli(p=0.00), # Initial prevalence of HIV
init_onart = ss.bernoulli(p=0.00), # Initial probability of being on ART (if infected).
art_progression_factor = 0.1, # Multiplier to reduce progression rates if on ART.
acute_to_latent = ss.perday(1/(7*12)), # 1-np.exp(-1/8), # 8 weeks
latent_to_aids = ss.perday(1/(365*8)), # 1-np.exp(-1/416), # 416 weeks
)
self.update_pars(pars, **kwargs)
# Define extra attributes for Agents of this disease.
self.define_states(
ss.FloatArr('state', default=HIVState.ATRISK), # Column name to store HIV state.
ss.BoolArr('on_ART', default=False), # Column name to store Whether agent is on ART.
)
return
[docs]
def set_prognoses(self ):
"""
Initialize HIV infection and ART status for agents in the simulation.
This method is called at the beginning of the simulation (time index 0)
to assign initial disease states and treatment (ART) status.
Behavior:
- If a high-level intervention labeled 'hivinterventions' is present in the simulation,
initialization is skipped entirely, assuming that the intervention will handle infection
and ART assignments dynamically at the appropriate time.
- If no HIV states are currently set to ACUTE, a subset of agents is randomly assigned to
ACUTE based on the `init_prev` parameter.
- If no agents are currently on ART, a subset of those in the ACUTE state is randomly
assigned to be on ART based on the `init_onart` parameter.
Notes:
- This check ensures the model does not reinitialize infected or ART states
if they've already been set or will be handled externally.
- It also avoids reapplying ART if ART status was assigned previously.
Returns:
None
"""
if hasattr(self.sim, 'interventions'):
import tbsim as mtb
for i in self.sim.interventions:
if i=='hivinterventions': # Check if the intervention label is present among the interventions
print('HIV intervention present, skipping initialization.')
return
uids = self.sim.people.auids
if len(self.state[self.state == HIVState.ACUTE])==0:
initial_infected= self.pars.init_prev.filter(uids)
self.state[initial_infected] = HIVState.ACUTE
current = self.state[uids].copy()
if len(self.on_ART[self.on_ART == True])==0:
# apply ART only to those who are in the ACUTE state
infected =uids[current == HIVState.ACUTE]
initial_onart = self.pars.init_onart.filter(infected)
self.on_ART[initial_onart] = True
return
[docs]
def step(self):
"""
Update state transitions based solely on state and ART.
If an agent is on ART, progression probabilities are reduced.
"""
if self.sim.ti == 0:
# Set initial prognoses for all agents
self.set_prognoses()
return
dt = self.sim.t.dt if hasattr(self.sim.t, 'dt') else 1.0 # dt in weeks (default=1)
uids = self.sim.people.auids
current = self.state[uids].copy()
# ART factor for progression:
art_factor = self.pars['art_progression_factor']
# HIV → LATENT:
acute_to_latent = self.pars.acute_to_latent.to_prob() # Convert to probability for Starsim 3.0
hiv_ids = uids[current == HIVState.ACUTE]
art_multiplier = np.where(self.on_ART[hiv_ids], art_factor, 1.0) # Apply ART factor
effective_p = acute_to_latent*art_multiplier
rand_vals = np.random.rand(hiv_ids.size)
self.state[hiv_ids[rand_vals < effective_p]] = HIVState.LATENT
# LATENT → AIDS:
latent_to_aids = self.pars.latent_to_aids.to_prob() # Convert to probability for Starsim 3.0
latent_ids = uids[current == HIVState.LATENT]
art_multiplier = np.where(self.on_ART[latent_ids], art_factor, 1.0) # Apply ART factor
effective_p = latent_to_aids*art_multiplier
rand_vals = np.random.rand(latent_ids.size)
self.state[latent_ids[rand_vals<effective_p]] = HIVState.AIDS
[docs]
def init_results(self):
super().init_results()
self.define_results(
ss.Result(name='hiv_prevalence', dtype=float, label='Prevalence (% Infected)'),
ss.Result(name='infected', dtype=int, label='Infected'),
ss.Result(name='on_art', dtype=float, label='On ART'),
ss.Result(name='atrisk', dtype=float, label='% ATRISK (Alive)'),
ss.Result(name='acute', dtype=float, label='% ACUTE'),
ss.Result(name='latent', dtype=float, label='% LATENT'),
ss.Result(name='aids', dtype=float, label='% AIDS'),
ss.Result(name='n_active', dtype=int, label='Active (Combined)'),
)
[docs]
def update_results(self):
super().update_results()
ti = self.sim.ti
uids = self.sim.people.auids
n_alive = np.count_nonzero(self.sim.people.alive)
res = self.results
n = len(uids)
states = self.state[uids]
if n_alive > 0:
res.hiv_prevalence[ti] = np.count_nonzero(np.isin(self.state, [HIVState.ACUTE, HIVState.LATENT, HIVState.AIDS])) / n_alive
else:
res.hiv_prevalence[ti] = 0.0
res.infected[ti] = np.count_nonzero(np.isin(self.state, [HIVState.ACUTE, HIVState.LATENT, HIVState.AIDS]))
res.atrisk[ti] = np.count_nonzero(self.state == HIVState.ATRISK)/n_alive
res.acute[ti] = np.count_nonzero(self.state == HIVState.ACUTE)/n_alive
res.latent[ti] = np.count_nonzero(self.state == HIVState.LATENT)/n_alive
res.aids[ti] = np.count_nonzero(self.state == HIVState.AIDS)/n_alive
res.n_active[ti] = np.count_nonzero(np.isin(self.state, [HIVState.ACUTE, HIVState.LATENT, HIVState.AIDS]))
res.on_art[ti] = np.count_nonzero(self.on_ART == True)
# if n_alive > 0:
# res.hiv_prevalence[ti] = res.n_active[ti] / n_alive