Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Equipment Class #1098

Merged
merged 125 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
5209b33
TODO: some modules use equipment when talking about consumables
EvaJanouskova Sep 1, 2023
5158aa2
breast_cancer: dummy used_equipment added where Andrew requested
EvaJanouskova Sep 4, 2023
e554dd7
co: dummy used_equipment added for methods where Emi listed some
EvaJanouskova Sep 4, 2023
e4935e3
healthsystem: annual equipment summary log by fac. level
EvaJanouskova Sep 4, 2023
8b455bd
breast_cancer: mastectomy dummy equipment fixed
EvaJanouskova Sep 5, 2023
9f44e85
equipment_catalogue & utils: new script + a change in utils.py - to c…
EvaJanouskova Sep 6, 2023
0ff003b
equipment_catalogue: PEP8
EvaJanouskova Sep 7, 2023
f736939
healthsystem: sort equipment for log
EvaJanouskova Sep 12, 2023
745d4ab
equipment_catalogue: comment updated
EvaJanouskova Sep 12, 2023
e7c52d8
rti: unified use of consumables/equipment terms
EvaJanouskova Sep 12, 2023
b523d58
hs, brc, co: used_equipment renamed to EQUIPMENT; if equip always sam…
EvaJanouskova Sep 12, 2023
5396269
brc: comment updated
EvaJanouskova Sep 18, 2023
2791c18
brc & co: rm the dummy examples of equipment from modules
EvaJanouskova Sep 20, 2023
07e1ae9
RF_Equipment: equipment catalogue - first draft (from Sakshi)
EvaJanouskova Sep 24, 2023
00270c1
RF_Equipment: equipment catalogue - merge duplicates (round 1)
EvaJanouskova Sep 24, 2023
d486c4d
RF_Equipment: equipment catalogue - merge duplicates (round 2)
EvaJanouskova Sep 27, 2023
860d952
hs: debugging Equipment log - rm sorted
EvaJanouskova Sep 27, 2023
c573fc8
hs: fix sorting of _equip_by_level
EvaJanouskova Sep 27, 2023
373018c
RF_Equipment: equipment catalogue - merge duplicates (round 3)
EvaJanouskova Sep 27, 2023
307efab
RF_Equipment: equipment catalogue - merge duplicates (round 4)
EvaJanouskova Sep 27, 2023
4f2ce6c
RF_Equipment: equipment catalogue - merge duplicates (round 5)
EvaJanouskova Sep 29, 2023
10b9dde
RF_Equipment: equipment catalogue - merge duplicates (round 6) + col …
EvaJanouskova Nov 15, 2023
1f2ff79
RF_Equip: equip item codes added
EvaJanouskova Nov 15, 2023
adde374
codes_to_items_list: new script created
EvaJanouskova Nov 15, 2023
e6013d9
codes_to_items_list: script generalised + PEP 8
EvaJanouskova Nov 15, 2023
a214a88
hs: equipment added to HSIEventDetails
EvaJanouskova Nov 16, 2023
32145b6
equip_catalogue: make catalogues from new logging (equip included in …
EvaJanouskova Nov 17, 2023
d8e299f
hs: sort equipment for logging
EvaJanouskova Nov 19, 2023
598368d
test_hs: assert equipment logging within detailed_hsi_event
EvaJanouskova Nov 19, 2023
e2fdafb
[no ci] hs: typo; Equipment logging removed
EvaJanouskova Nov 23, 2023
acf3a63
equip_catalogue: fix the keys mapping to be done for each run
EvaJanouskova Dec 1, 2023
97f0f3f
equip_catalogue: input hsi event details by which to catalog equipment
EvaJanouskova Dec 2, 2023
1435d22
equip_catalogue: input time period by which to catalog equipment
EvaJanouskova Dec 2, 2023
14a11a7
equip_catalogue: list of requested details can be empty
EvaJanouskova Dec 2, 2023
6d85fe1
equip_catalogue: verify inputs as expected & set output file names in…
EvaJanouskova Dec 2, 2023
934e52d
equip_catalogue: (1) detailed - equip set as string in one row, modul…
EvaJanouskova Dec 6, 2023
bdbd8f7
equip_catalogue: suffix (as input) added for output file names
EvaJanouskova Dec 6, 2023
070454c
hs: structure v2; alri+co: examples to test new structure
EvaJanouskova Jan 13, 2024
99be34a
equip_catalogue: TODO added
EvaJanouskova Jan 13, 2024
5076e3b
equip_catalogue: typo
EvaJanouskova Dec 6, 2023
d47b53e
equip_catalogue: TODO added
EvaJanouskova Jan 13, 2024
057fb44
equip_catalogue: bug fixed
EvaJanouskova Jan 13, 2024
2bf3e8a
brc: TODO added
EvaJanouskova Jan 13, 2024
fa3380f
hs: rm prints
EvaJanouskova Jan 13, 2024
4b7904a
hs: rm old code
EvaJanouskova Jan 13, 2024
8c8bcc5
hs: rm/add accidentally added/rmd commas
EvaJanouskova Jan 13, 2024
6b03edd
hs & alri+co: rename and correct return of fncs related to equipment
EvaJanouskova Jan 15, 2024
c37e1de
hs: log equip item codes instead of names
EvaJanouskova Jan 16, 2024
99ad138
equip_catalogue: updated for logged equip item codes
EvaJanouskova Jan 17, 2024
a281e56
brc: change Andrew suggested
EvaJanouskova Jan 19, 2024
1787ffe
hs: updates for better readability; rm unused code
EvaJanouskova Jan 19, 2024
f961640
hs: PEP8
EvaJanouskova Jan 19, 2024
12142b0
hs: get_equip_item_code_from_item_name fnc updated; ESS.EQUIP as codes
EvaJanouskova Jan 19, 2024
36f0523
equip_catalogue: add item codes to catalogue by requested details (1 …
EvaJanouskova Jan 21, 2024
b40f955
hs: allow adding equip by pkg name(s)
EvaJanouskova Jan 21, 2024
9c8b6ad
co & RF_Equip: an example of usage of equipment pkg
EvaJanouskova Jan 21, 2024
0552e48
utils: use pandas fnc (instead of make one)
EvaJanouskova Jan 24, 2024
0106e20
hs: ESS_EQUIP as HSI_Event's attribute; if settings of ESS_EQUIP forg…
EvaJanouskova Jan 24, 2024
455f4e0
hs: fixed saving _hsi_event_names_missing_ess_equip
EvaJanouskova Jan 24, 2024
503036d
hs: fixed updating _hsi_event_names_missing_ess_equip
EvaJanouskova Jan 29, 2024
8ea0b27
hs: TODO smt odd going on with hsi_event_names_missing_ess_equip warning
EvaJanouskova Jan 29, 2024
7ad13cc
hs: sort hsi_event_names_missing_ess_equip warning
EvaJanouskova Jan 29, 2024
39a5d21
hs: equip_item_and_package_code_lookups renamed to equip_item_and_pac…
EvaJanouskova Jan 29, 2024
9dcf46b
hs: ignore_unknown_equip_names
EvaJanouskova Jan 29, 2024
0bda4d0
hs: warning messages shortened
EvaJanouskova Jan 30, 2024
0d387a4
co: example of setting ess. equip based on condition
EvaJanouskova Mar 21, 2024
b87781e
co: comment updated; TODO added
EvaJanouskova Mar 21, 2024
88632f6
co, hs, RF_Equip, RF_HS_params, test_alri, test_co, test_hs: checking…
EvaJanouskova Mar 21, 2024
d71107a
Merge branch 'master' into EvaJ/equipment/structure_ToRunSim
EvaJanouskova Mar 21, 2024
2518c49
Merge branch 'master' into EvaJ/equipment/structure_ToRunSim
tbhallett Mar 25, 2024
a6fffc9
example test suite
tbhallett Mar 25, 2024
8428d8a
typo and add todo
tbhallett Mar 25, 2024
2a3c8f8
further tests
tbhallett Mar 25, 2024
b1aa9ae
[no_ci] RF_Equip: availabilities changed from probs to True/False val…
EvaJanouskova Mar 25, 2024
ffd5178
ac: rm comments
EvaJanouskova Mar 26, 2024
7c8ea54
hs: rm equip_availability before sim default
EvaJanouskova Mar 26, 2024
6ddd11e
labour: rm comments
EvaJanouskova Mar 26, 2024
4f4d858
hs: rm extra line
EvaJanouskova Mar 26, 2024
f8b0ac1
hs: raise error if 1) ess equip not a set of ints, 2) invalid equip_a…
EvaJanouskova Mar 26, 2024
55772ed
Merge branch 'refs/heads/master' into EvaJ/equipment/structure_ToRunSim
tbhallett May 9, 2024
16752ae
roll back incidental changes
tbhallett May 9, 2024
cd2774e
move `codes_to_items_list` to scripts/data-file-processing
tbhallett May 9, 2024
7a5d991
roll back parsing script
tbhallett May 9, 2024
4781555
squash - basic outline of Equipment class
tbhallett May 9, 2024
27779e6
squash - basic outline of Equipment class
tbhallett May 10, 2024
1988ae2
linting
tbhallett May 10, 2024
13d5a3c
update call in bed-days
tbhallett May 10, 2024
3d4b80d
use hashable type in HSIEventDetails
tbhallett May 12, 2024
3bb2581
update logic and add docstring
tbhallett May 12, 2024
3c16898
linting
tbhallett May 13, 2024
5bcdb38
linting
tbhallett May 13, 2024
3bea20f
Merge branch 'master' into EvaJ/equipment/structure_ToRunSim
tbhallett May 13, 2024
3a8d3da
linting
tbhallett May 13, 2024
dc9d9b9
linting
tbhallett May 13, 2024
ea4027b
provide default for 'equip_availability' in test_alri:get_sim()
tbhallett May 13, 2024
55e3352
Prevent recomputing of probabilities every time availability is updated
willGraham01 May 13, 2024
eba5e39
Remove commentted-out code and pass back return value
willGraham01 May 13, 2024
d3f926f
small updates following chnages from Will
tbhallett May 13, 2024
092a417
Update src/tlo/methods/hsi_event.py
tbhallett May 13, 2024
af05d26
Update src/tlo/methods/hsi_event.py
tbhallett May 13, 2024
8be7782
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
e0d2d00
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
1a6fdc9
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
2579b86
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
f2ea9cf
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
cf1d8a5
Update src/tlo/methods/equipment.py
tbhallett May 13, 2024
8e0a353
use property syntax and a setter for `availability` rather than `upda…
tbhallett May 13, 2024
6176413
update typehint and docstring on `parse_items` to be more accurate
tbhallett May 13, 2024
fa214a7
fix typo in `test_HealthSystemChangeParameters`
tbhallett May 13, 2024
3be8363
linting
tbhallett May 14, 2024
959c243
remove update_availability
tbhallett May 14, 2024
cc56948
use continue rather than nested if/else statment
tbhallett May 14, 2024
69e28ca
initialise self._EQUIPMENT in the __init__ so that it specific to the…
tbhallett May 14, 2024
d340e09
Update src/tlo/methods/hsi_event.py
tbhallett May 14, 2024
45b3b41
Update src/tlo/methods/hsi_event.py
tbhallett May 14, 2024
da5222e
Update src/tlo/methods/hsi_event.py
tbhallett May 14, 2024
e7b40b0
Update src/tlo/methods/hsi_event.py
tbhallett May 14, 2024
204877e
Update src/tlo/methods/hsi_event.py
tbhallett May 14, 2024
55f9c88
make checks in `test_core_functionality_of_equipment_class` test for …
tbhallett May 14, 2024
5f52b7b
Merge remote-tracking branch 'origin/EvaJ/equipment/structure_ToRunSi…
tbhallett May 14, 2024
75e02a9
check logging from multiple HSI_events in test_logging_of_equipment_f…
tbhallett May 14, 2024
8549403
Merge remote-tracking branch 'refs/remotes/origin/master' into EvaJ/e…
tbhallett May 14, 2024
81b8c58
roll back incidental changes
tbhallett May 14, 2024
e42e660
remove `codes_to_items_list.py` from this PR
tbhallett May 14, 2024
9370931
Merge branch 'master' into EvaJ/equipment/structure_ToRunSim
matt-graham May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
1 change: 1 addition & 0 deletions src/tlo/analysis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,7 @@ def get_parameters_for_status_quo() -> Dict:
"mode_appt_constraints": 1,
"cons_availability": "default",
"beds_availability": "default",
"equip_availability": "all", # <--- NB. Existing calibration is assuming all equipment is available
},
}

Expand Down
270 changes: 270 additions & 0 deletions src/tlo/methods/equipment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import warnings
from collections import defaultdict
from typing import Counter, Iterable, Literal, Set, Union

import numpy as np
import pandas as pd

from tlo import logging

logger_summary = logging.getLogger("tlo.methods.healthsystem.summary")


class Equipment:
"""This is the equipment class. It maintains a current record of the availability of equipment in the
health system. It is expected that this is instantiated by the :py:class:`~.HealthSystem` module.

The basic paradigm is that an :py:class:`~.HSI_Event` can declare equipment that is required for delivering the healthcare
service that the ``HSI_Event`` represents. The ``HSI_Event`` uses :py:meth:`HSI_event.add_equipment` to make these declarations,
with reference to the items of equipment that are defined in ``ResourceFile_EquipmentCatalogue.csv``. (These
declaration can be in the form of the descriptor or the equipment item code). These declarations can be used when
the ``HSI_Event`` is created but before it is run (in ``__init__``), or during execution of the ``HSI_Event`` (in :py:meth:`.HSI_Event.apply`).

As the ``HSI_Event`` can declare equipment that is required before it is run, the HealthSystem *can* use this to
prevent an ``HSI_Event`` running if the equipment declared is not available. Note that for equipment that is declared
whilst the ``HSI_Event`` is running, there are no checks on availability, and the ``HSI_Event`` is allowed to continue
running even if equipment is declared is not available. For this reason, the ``HSI_Event`` should declare equipment
that is *essential* for the healthcare service in its ``__init__`` method. If the logic inside the ``apply`` method
of the ``HSI_Event`` depends on the availability of equipment, then it can find the probability with which
item(s) will be available using :py:meth:`.HSI_Event.probability_equipment_available`.

The data on the availability of equipment data refers to the proportion of facilities in a district of a
particular level (i.e., the ``Facility_ID``) that do have that piece of equipment. In the model, we do not know
which actual facility the person is attending (there are many actual facilities grouped together into one
``Facility_ID`` in the model). Therefore, the determination of whether equipment is available is made
probabilistically for the ``HSI_Event`` (i.e., the probability that the actual facility being attended by the
person has the equipment is represented by the proportion of such facilities that do have that equipment). It is
assumed that the probabilities of each item being available are independent of one other (so that the
probability of all items being available is the product of the probabilities for each item). This probabilistic
determination of availability is only done _once_ for the ``HSI_Event``: i.e., if the equipment is determined to
not be available for the instance of the ``HSI_Event``, then it will remain not available if the same event is
re-scheduled / re-entered into the ``HealthSystem`` queue. This represents that if the facility that a particular
person attends for the ``HSI_Event`` does not have the equipment available, then it will still not be available on
another day.

Where data on availability is not provided for an item, the probability of availability is inferred from the
average availability of other items in that facility ID. Likewise, the probability of an item being available
at a facility ID is inferred from the average availability of that item at other facilities. If an item code is
referred in ``add_equipment`` that is not recognised (not included in :py:attr:`catalogue`), a :py:exc:`UserWarning` is issued, but
that item is then silently ignored. If a facility ID is ever referred that is not recognised (not included in
:py:attr:`master_facilities_list`), an :py:exc:`AssertionError` is raised.

:param catalogue: The database of all recognised item_codes.
:param data_availability: Specifies the probability with which each equipment (identified by an ``item_code``) is
available at a facility level. Note that information is not necessarily provided for every item in the :py:attr`catalogue`
or every facility ID in the :py:attr`master_facilities_list`.
:param: rng: The random number generator object to use for random numbers.
:param availability: Determines the mode availability of the equipment. If 'default' then use the availability
specified in :py:attr:`data_availability`; if 'none', then let no equipment be ever be available; if 'all', then all
equipment is always available.
:param master_facilities_list: The :py:class:`~pandas.DataFrame` with the line-list of all the facilities in the health system.
"""

def __init__(
self,
catalogue: pd.DataFrame,
data_availability: pd.DataFrame,
rng: np.random.RandomState,
master_facilities_list: pd.DataFrame,
availability: Literal["all", "default", "none"] = "default",
) -> None:
# - Store arguments
self.catalogue = catalogue
self.rng = rng
self.data_availability = data_availability
self.availability = availability
self.master_facilities_list = master_facilities_list

# - Data structures for quick look-ups for items and descriptors
self._item_code_lookup = self.catalogue.set_index('Item_Description')['Item_Code'].to_dict()
self._all_item_descriptors = set(self._item_code_lookup.keys())
self._all_item_codes = set(self._item_code_lookup.values())
self._all_fac_ids = self.master_facilities_list['Facility_ID'].unique()

# - Probabilities of items being available at each facility_id
self._probabilities_of_items_available = self._calculate_equipment_availability_probabilities()

# - Internal store of which items have been used at each facility_id This is of the form
# {facility_id: {item_code: count}}.
self._record_of_equipment_used_by_facility_id = defaultdict(Counter)


def on_simulation_end(self):
"""Things to do when the simulation ends:
* Log (to the summary logger) the equipment that has been used.
"""
self.write_to_log()

@property
def availability(self):
return self._availability

@availability.setter
def availability(self, value: Literal["all", "default", "none"]):
assert value in {"all", "none", "default"}, f"New availability value {value} not recognised."
self._availability = value

def _calculate_equipment_availability_probabilities(self) -> pd.Series:
"""
Compute the probabilities that each equipment item is available (at a given
facility), for use when the equipment availability is set to "default".

The probabilities computed in this method are constant throughout the simulation,
however they will not be used when the equipment availability is "all" or "none".
Computing them once and storing the result allows us to avoid repeating this
calculation if the equipment availability change event occurs during the simulation.
"""
# Create "full" dataset, where we force that there is probability of availability for every item_code at every
# observed facility
dat = pd.Series(
index=pd.MultiIndex.from_product(
[self._all_fac_ids, self._all_item_codes], names=["Facility_ID", "Item_Code"]
),
data=float("nan"),
).combine_first(
self.data_availability.set_index(["Facility_ID", "Item_Code"])[
"Pr_Available"
]
)

# Merge in original dataset and use the mean in that facility_id to impute availability of missing item_codes
dat = dat.groupby("Facility_ID").transform(lambda x: x.fillna(x.mean()))
# ... and also impute availability for any facility_ids for which no data, based on all other facilities
dat = dat.groupby("Item_Code").transform(lambda x: x.fillna(x.mean()))

# Check no missing values
assert not dat.isnull().any()

return dat

def parse_items(self, items: Union[int, str, Iterable[int], Iterable[str]]) -> Set[int]:
"""Parse equipment items specified as an item_code (integer), an item descriptor (string), or an iterable of
item_codes or descriptors (but not a mix of the two), and return as a set of item_code (integers). For any
item_code/descriptor not recognised, a ``UserWarning`` is issued."""

def check_item_codes_recognised(item_codes: set[int]):
if not item_codes.issubset(self._all_item_codes):
warnings.warn(f'Item code(s) "{item_codes}" not recognised.')

def check_item_descriptors_recognised(item_descriptors: set[str]):
if not item_descriptors.issubset(self._all_item_descriptors):
warnings.warn(f'Item descriptor(s) "{item_descriptors}" not recognised.')

# Make into a set if it is not one already
if isinstance(items, (str, int)):
items = set([items])
else:
items = set(items)

items_are_ints = all(isinstance(element, int) for element in items)

if items_are_ints:
check_item_codes_recognised(items)
# In the return, any unrecognised item_codes are silently ignored.
return items.intersection(self._all_item_codes)
else:
check_item_descriptors_recognised(items) # Warn for any unrecognised descriptors
# In the return, any unrecognised descriptors are silently ignored.
return set(self._item_code_lookup[i] for i in items if i in self._item_code_lookup)

def probability_all_equipment_available(
self, facility_id: int, item_codes: Set[int]
) -> float:
"""
Returns the probability that all the equipment item_codes are available
at the given facility.

It does so by looking at the probabilities of each equipment item being
available and multiplying these together to find the probability that *all*
are available.

NOTE: This will error if the facility ID or any of the item codes is not recognised.

:param facility_id: Facility at which to check for the equipment.
:param item_codes: Integer item codes corresponding to the equipment to check.
"""

assert facility_id in self._all_fac_ids, f"Unrecognised facility ID: {facility_id=}"
assert item_codes.issubset(self._all_item_codes), f"At least one item code was unrecognised: {item_codes=}"

if self.availability == "all":
return 1.0
elif self.availability == "none":
return 0.0
return self._probabilities_of_items_available.loc[
(facility_id, list(item_codes))
tbhallett marked this conversation as resolved.
Show resolved Hide resolved
].prod()

def is_all_items_available(
self, item_codes: Set[int], facility_id: int
) -> bool:
"""
Determine if all equipment items are available at the given facility_id.
Returns True only if all items are available at the facility_id,
otherwise returns False.
"""
if item_codes:
return self.rng.random_sample() < self.probability_all_equipment_available(
facility_id=facility_id,
item_codes=item_codes,
)
else:
# In the case of an empty set, default to True without doing anything else ('no equipment' is always
# "available"). This is the most common case, so optimising for speed.
return True

def record_use_of_equipment(
self, item_codes: Set[int], facility_id: int
) -> None:
"""Update internal record of the usage of items at equipment at the specified facility_id."""
self._record_of_equipment_used_by_facility_id[facility_id].update(item_codes)

def write_to_log(self) -> None:
"""Write to the log:
* Summary of the equipment that was _ever_ used at each district/facility level.
Note that the info-level health system logger (key: `hsi_event_counts`) contains logging of the equipment used
in each HSI event (if finer splits are needed). Alternatively, different aggregations could be created here for
the summary logger, using the same pattern as used here.
"""

mfl = self.master_facilities_list

def set_of_keys_or_empty_set(x: Union[set, dict]):
if isinstance(x, set):
return x
elif isinstance(x, dict):
return set(x.keys())
else:
return set()

set_of_equipment_ever_used_at_each_facility_id = pd.Series({
fac_id: set_of_keys_or_empty_set(self._record_of_equipment_used_by_facility_id.get(fac_id, set()))
for fac_id in mfl['Facility_ID']
}, name='EquipmentEverUsed').astype(str)

output = mfl.merge(
set_of_equipment_ever_used_at_each_facility_id,
left_on='Facility_ID',
right_index=True,
how='left',
).drop(columns=['Facility_ID', 'Facility_Name'])

# Log multi-row data-frame
for _, row in output.iterrows():
logger_summary.info(
key='EquipmentEverUsed_ByFacilityID',
description='For each facility_id (the set of facilities of the same level in a district), the set of'
'equipment items that are ever used.',
data=row.to_dict(),
)

def lookup_item_codes_from_pkg_name(self, pkg_name: str) -> Set[int]:
"""Convenience function to find the set of item_codes that are grouped under a package name in the catalogue.
It is expected that this is used by the disease module once and then the resulting equipment item_codes are
saved on the module."""
df = self.catalogue

if pkg_name not in df['Pkg_Name'].unique():
raise ValueError(f'That Pkg_Name is not in the catalogue: {pkg_name=}')

return set(df.loc[df['Pkg_Name'] == pkg_name, 'Item_Code'].values)
Loading