Location
Locations can be used in TITAN to differentiate agents by "geography". The primary features of locations are:
- Differentiated parameters via location scaling/overrides
- Allows different demographics or interventions by location
- See params app for details
- Location based assorting (including based on neighboring locations)
- Can have agents assort with agents from their own location vs neighbors vs all others
- Neighboring locations are determined by the
edges
defined inparams.location
- See params app for details on assorting rules
- It is also possible to define edges via a geography CSV (see utils). This is also exposed via the
grid2edges
command line utility (rungrid2edges --help
for usage).
- Migration between locations
- When an agent migrates locations, they adopt the parameters of their new location
- Migration can cause the population numbers in a location to drift over time
- See params app for details
Location
__init__(self, name, defn, params)
special
This class constructs and represents a location within the model. A location can have an arbitrary geographic granularity.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
name of the location |
required |
defn |
ObjMap |
definition for this location |
required |
params |
ObjMap |
model parameters |
required |
Source code in titan/location.py
def __init__(self, name: str, defn: ObjMap, params: ObjMap):
"""
This class constructs and represents a location within the model. A location
can have an arbitrary geographic granularity.
args:
name: name of the location
defn: definition for this location
params: model parameters
"""
# location properties
self.name = name
self.params = self.create_params(params)
self.ppl = defn.ppl # percent of overall population assigned to this location
self.category = defn.category # arbitrary category, can be used for migration
# value/weight maps needed for creating new agents in this location
self.pop_weights: Dict[str, Dict[str, List[Any]]] = {}
self.role_weights: Dict[str, Dict] = {}
self.drug_weights: Dict[str, Dict] = {}
self.init_weights()
self.migration_weights: Dict[str, Any] = {}
self.neighbors: Set[str] = set() # or maybe edges instead
create_params(self, params)
Scale or override the generic parameters with any location based scaling from params.location.scaling
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params |
ObjMap |
model parameters |
required |
Returns:
Type | Description |
---|---|
ObjMap |
new parameter object with scaled values for this location |
Source code in titan/location.py
def create_params(self, params: ObjMap) -> ObjMap:
"""
Scale or override the generic parameters with any location based scaling from params.location.scaling
args:
params: model parameters
returns:
new parameter object with scaled values for this location
"""
new_params = deepcopy(params)
defns = new_params.location.scaling[self.name]
for param_path, defn in defns.items():
if param_path != "ls_default":
if defn.field == "scalar":
utils.scale_param(new_params, param_path, defn.scalar)
elif defn.field == "override":
utils.override_param(new_params, param_path, defn.override)
return new_params
init_weights(self)
Create the containers to hold values and weights for randomly selecting:
- sex_role
- drug_type
- race
- sex_type
Source code in titan/location.py
def init_weights(self):
"""
Create the containers to hold values and weights for randomly selecting:
* sex_role
* drug_type
* race
* sex_type
"""
def init_weight_dict(d, item):
d[item] = {}
d[item]["values"] = []
d[item]["weights"] = []
def add_weight(d, v, w):
d["values"].append(v)
d["weights"].append(w)
total_ppl = 0
for race, race_param in self.params.demographics.items():
self.role_weights[race] = {}
self.drug_weights[race] = {}
init_weight_dict(self.pop_weights, race)
total_ppl += race_param.ppl
for st, st_param in race_param.sex_type.items():
add_weight(self.pop_weights[race], st, st_param.ppl)
init_weight_dict(self.role_weights[race], st)
init_weight_dict(self.drug_weights[race], st)
for role, prob in st_param.sex_role.init.items():
add_weight(self.role_weights[race][st], role, prob)
for dt, dt_param in st_param.drug_type.items():
add_weight(self.drug_weights[race][st], dt, dt_param.ppl)
assert math.isclose(
sum(self.role_weights[race][st]["weights"]), 1, abs_tol=0.001
), f"{self.name}'s' {race} {st} role weights must add to 1"
assert math.isclose(
sum(self.drug_weights[race][st]["weights"]), 1, abs_tol=0.001
), f"ppl of {self.name}'s' {race} {st} drug_types must add to 1"
assert math.isclose(
sum(self.pop_weights[race]["weights"]), 1, abs_tol=0.001
), f"ppl of {self.name}'s' {race} sex_types must add to 1"
assert math.isclose(
total_ppl, 1, abs_tol=0.001
), f"ppl of {self.name}'s' races must add to 1"
LocationEdge
__init__(self, loc1, loc2, distance, id=None)
special
Construct a location edge, which holds attributes that relate two Locations.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
loc1 |
Location |
the first location |
required |
loc2 |
Location |
the other location |
required |
distance |
float |
a measure of distance between the locations |
required |
id |
Optional[int] |
a unique identifier for this edge |
None |
Source code in titan/location.py
def __init__(
self, loc1: Location, loc2: Location, distance: float, id: Optional[int] = None
):
"""
Construct a location edge, which holds attributes that relate two Locations.
args:
loc1: the first location
loc2: the other location
distance: a measure of distance between the locations
id: a unique identifier for this edge
"""
assert loc1 != loc2, "can't have a location self-edge"
# self.id is unique ID number used to track each edge.
if id is not None:
self.id = id
else:
self.id = self.next_edge_id
self.update_id_counter(self.id)
self.edge = set({loc1, loc2})
self.distance = distance
loc1.neighbors.add(loc2.name)
loc2.neighbors.add(loc1.name)
Geography
__init__(self, params)
special
Umbrella class to initialize/store locations and location edges for a population
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params |
ObjMap |
model parameters |
required |
Source code in titan/location.py
def __init__(self, params: ObjMap):
"""
Umbrella class to initialize/store locations and location edges for a population
args:
params: model parameters
"""
self.locations: Dict[str, Location] = {
location: Location(location, defn, params)
for location, defn in params.classes.locations.items()
}
self.categories: Dict[str, List[Location]] = {}
for location in self.locations.values():
if location.category in self.categories:
self.categories[location.category].append(location)
else:
self.categories[location.category] = [location]
if params.location.migration.enabled:
with open(params.location.migration.probs_file, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
from_loc = row.pop("")
prob = float(row.pop("prob", 1))
values = list(row.keys())
weights = list(map(float, row.values()))
assert math.isclose(
sum(weights), 1, abs_tol=0.001
), f"Migration weights for {from_loc} must add to 1"
if params.location.migration.attribute == "name":
self.locations[from_loc].migration_weights["prob"] = prob
self.locations[from_loc].migration_weights["weights"] = weights
self.locations[from_loc].migration_weights["values"] = values
elif params.location.migration.attribute == "category":
for location in self.categories[from_loc]:
location.migration_weights["prob"] = prob
location.migration_weights["weights"] = weights
location.migration_weights["values"] = values
else:
raise ValueError("Unknown migration attribute")
self.edges: Set[LocationEdge] = set()
for name, defn in params.location.edges.items():
if name != "edge_default":
loc1 = self.locations[defn.location_1]
loc2 = self.locations[defn.location_2]
self.edges.add(LocationEdge(loc1, loc2, defn.distance))