Skip to content

Commit 58a2a41

Browse files
willGraham01mnjowe
andauthored
Fix over-allocation of Bed Days in #1399 (#1437)
* 1st pass writing combination method * Write test that checks failure case in issue * Expand on docstring for combining footprints method * Force-cast to int when returning footprint since pd.datetime.timedelta doesn't know how to handle np.int64's * Catch bug when determining priority on each day, write test to cover this case with a 3-bed types resolution --------- Co-authored-by: Emmanuel Mnjowe <[email protected]>
1 parent d9d3f62 commit 58a2a41

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

src/tlo/methods/bed_days.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,60 @@ def issue_bed_days_according_to_availability(self, facility_id: int, footprint:
318318

319319
return available_footprint
320320

321+
def combine_footprints_for_same_patient(
322+
self, fp1: Dict[str, int], fp2: Dict[str, int]
323+
) -> Dict[str, int]:
324+
"""
325+
Given two footprints that are due to start on the same day, combine the two footprints by
326+
overlaying the higher-priority bed over the lower-priority beds.
327+
328+
As an example, given the footprints,
329+
fp1 = {"bedtype1": 2, "bedtype2": 0}
330+
fp2 = {"bedtype1": 1, "bedtype2": 6}
331+
332+
where bedtype1 is higher priority than bedtype2, we expect the combined allocation to be
333+
{"bedtype1": 2, "bedtype2": 5}.
334+
335+
This is because footprints are assumed to run in the order of the bedtypes priority; so
336+
fp2's second day of being allocated to bedtype2 is overwritten by the higher-priority
337+
allocation to bedtype1 from fp1. The remaining 5 days are allocated to bedtype2 since
338+
fp1 does not require a bed after the first 2 days, but fp2 does.
339+
340+
:param fp1: Footprint, to be combined with the other argument.
341+
:param pf2: Footprint, to be combined with the other argument.
342+
"""
343+
fp1_length = sum(days for days in fp1.values())
344+
fp2_length = sum(days for days in fp2.values())
345+
max_length = max(fp1_length, fp2_length)
346+
347+
# np arrays where each entry is the priority of bed allocated by the footprint
348+
# on that day. fp_priority[i] = priority of the bed allocated by the footprint on
349+
# day i (where the current day is day 0).
350+
# By default, fill with priority equal to the lowest bed priority; though all
351+
# the values will have been explicitly overwritten after the next loop completes.
352+
fp1_priority = np.ones((max_length,), dtype=int) * (len(self.bed_types) - 1)
353+
fp2_priority = fp1_priority.copy()
354+
355+
fp1_at = 0
356+
fp2_at = 0
357+
for priority, bed_type in enumerate(self.bed_types):
358+
# Bed type priority is dictated by list order, so it is safe to loop here.
359+
# We will start with the highest-priority bed type and work to the lowest
360+
fp1_priority[fp1_at:fp1_at + fp1[bed_type]] = priority
361+
fp1_at += fp1[bed_type]
362+
fp2_priority[fp2_at:fp2_at + fp2[bed_type]] = priority
363+
fp2_at += fp2[bed_type]
364+
365+
# Element-wise minimum of the two priority arrays is then the bed to assign
366+
final_priorities = np.minimum(fp1_priority, fp2_priority)
367+
# Final footprint is then formed by converting the priorities into blocks of days
368+
return {
369+
# Cast to int here since pd.datetime.timedelta doesn't know what to do with
370+
# np.int64 types
371+
bed_type: int(sum(final_priorities == priority))
372+
for priority, bed_type in enumerate(self.bed_types)
373+
}
374+
321375
def impose_beddays_footprint(self, person_id, footprint):
322376
"""This is called to reflect that a new occupancy of bed-days should be recorded:
323377
* Cause to be reflected in the bed_tracker that an hsi_event is being run that will cause bed to be
@@ -345,9 +399,7 @@ def impose_beddays_footprint(self, person_id, footprint):
345399
remaining_footprint = self.get_remaining_footprint(person_id)
346400

347401
# combine the remaining footprint with the new footprint, with days in each bed-type running concurrently:
348-
combo_footprint = {bed_type: max(footprint[bed_type], remaining_footprint[bed_type])
349-
for bed_type in self.bed_types
350-
}
402+
combo_footprint = self.combine_footprints_for_same_patient(footprint, remaining_footprint)
351403

352404
# remove the old footprint and apply the combined footprint
353405
self.remove_beddays_footprint(person_id)

tests/test_beddays.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import copy
33
import os
44
from pathlib import Path
5+
from typing import Dict
56

67
import pandas as pd
78
import pytest
@@ -83,6 +84,88 @@ def test_beddays_in_isolation(tmpdir, seed):
8384
assert ([cap_bedtype1] * days_sim == tracker.values).all()
8485

8586

87+
def test_beddays_allocation_resolution(tmpdir, seed):
88+
sim = Simulation(start_date=start_date, seed=seed)
89+
sim.register(
90+
demography.Demography(resourcefilepath=resourcefilepath),
91+
healthsystem.HealthSystem(resourcefilepath=resourcefilepath),
92+
)
93+
94+
# Update BedCapacity data with a simple table:
95+
level2_facility_ids = [128, 129, 130] # <-- the level 2 facilities for each region
96+
# This ensures over-allocations have to be properly resolved
97+
cap_bedtype1 = 10
98+
cap_bedtype2 = 10
99+
cap_bedtype3 = 10
100+
101+
# create a simple bed capacity dataframe
102+
hs = sim.modules["HealthSystem"]
103+
hs.parameters["BedCapacity"] = pd.DataFrame(
104+
data={
105+
"Facility_ID": level2_facility_ids,
106+
"bedtype1": cap_bedtype1,
107+
"bedtype2": cap_bedtype2,
108+
"bedtype3": cap_bedtype3,
109+
}
110+
)
111+
112+
sim.make_initial_population(n=100)
113+
sim.simulate(end_date=start_date)
114+
115+
# reset bed days tracker to the start_date of the simulation
116+
hs.bed_days.initialise_beddays_tracker()
117+
118+
def assert_footprint_matches_expected(
119+
footprint: Dict[str, int], expected_footprint: Dict[str, int]
120+
):
121+
"""
122+
Asserts that two footprints are identical.
123+
The footprint provided as the 2nd argument is assumed to be the footprint
124+
that we want to match, and the 1st as the result of the program attempting
125+
to resolve over-allocations.
126+
"""
127+
assert len(footprint) == len(
128+
expected_footprint
129+
), "Bed type footprints did not return same allocations."
130+
for bed_type, expected_days in expected_footprint.items():
131+
allocated_days = footprint[bed_type]
132+
assert expected_days == allocated_days, (
133+
f"Bed type {bed_type} was allocated {allocated_days} upon combining, "
134+
f"but expected it to get {expected_days}."
135+
)
136+
137+
# Check that combining footprints for a person returns the expected output
138+
139+
# SIMPLE 2-bed days case
140+
# Test uses example fail case given in https://github.com/UCL/TLOmodel/issues/1399
141+
# Person p has: bedtyp1 for 2 days, bedtype2 for 0 days.
142+
# Person p then assigned: bedtype1 for 1 days, bedtype2 for 6 days.
143+
# EXPECT: p's footprints are combined into bedtype1 for 2 days, bedtype2 for 5 days.
144+
existing_footprint = {"bedtype1": 2, "bedtype2": 0, "bedtype3": 0}
145+
incoming_footprint = {"bedtype1": 1, "bedtype2": 6, "bedtype3": 0}
146+
expected_resolution = {"bedtype1": 2, "bedtype2": 5, "bedtype3": 0}
147+
allocated_footprint = hs.bed_days.combine_footprints_for_same_patient(
148+
existing_footprint, incoming_footprint
149+
)
150+
assert_footprint_matches_expected(allocated_footprint, expected_resolution)
151+
152+
# TEST case involve 3 different bed-types.
153+
# Person p has: bedtype1 for 2 days, then bedtype3 for 4 days.
154+
# p is assigned: bedtype1 for 1 day, bedtype2 for 3 days, and bedtype3 for 1 day.
155+
# EXPECT: p spends 2 days in each bedtype;
156+
# - Day 1 needs bedtype1 for both footprints
157+
# - Day 2 existing footprint at bedtype1 overwrites incoming at bedtype2
158+
# - Day 3 & 4 incoming footprint at bedtype2 overwrites existing allocation to bedtype3
159+
# - Day 5 both footprints want bedtype3
160+
# - Day 6 existing footprint needs bedtype3, whilst incoming footprint is over.s
161+
existing_footprint = {"bedtype1": 2, "bedtype2": 0, "bedtype3": 4}
162+
incoming_footprint = {"bedtype1": 1, "bedtype2": 3, "bedtype3": 1}
163+
expected_resolution = {"bedtype1": 2, "bedtype2": 2, "bedtype3": 2}
164+
allocated_footprint = hs.bed_days.combine_footprints_for_same_patient(
165+
existing_footprint, incoming_footprint
166+
)
167+
assert_footprint_matches_expected(allocated_footprint, expected_resolution)
168+
86169
def check_dtypes(simulation):
87170
# check types of columns
88171
df = simulation.population.props

0 commit comments

Comments
 (0)