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

BedDays availability switch now correctly updates capacities #1352

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
38 changes: 36 additions & 2 deletions src/tlo/methods/bed_days.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

"""
from collections import defaultdict
from typing import Dict, Tuple
from typing import Dict, Literal, Tuple

import numpy as np
import pandas as pd

from tlo import Property, Types, logging
from tlo import Date, Property, Types, logging

# ---------------------------------------------------------------------------------------------------------
# CLASS DEFINITIONS
Expand Down Expand Up @@ -145,6 +145,40 @@ def initialise_beddays_tracker(self, model_to_data_popsize_ratio=1.0):
assert not df.isna().any().any()
self.bed_tracker[bed_type] = df

def switch_beddays_availability(
self,
new_availability: Literal["all", "none", "default"],
effective_on_and_from: Date,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way in which we expect to use this (i.e., from HealthSystemChangeParameters) I think we can always assume that this is equal to today's date. This is the way in which other things (consumables, equipment) have their availability updated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With regards to this and the below comment - I think exposing effective_on_and_from and model_to_data_popsize_ratio as arguments has an advantage in terms of making it easier to test (as we can pass in relevant values directly rather than having to manipulate the other objects they would be instead read from) and keeping it's implementation decoupled from implementation details of other classes, however we can possibly still achieve this by making the current switch_beddays_availability method instead _switch_beddays_availability to indicate for internal use only, and exposing a 'public' method

   def switch_beddays_availability(self, new_availability: Literal["all", "none", "default"]) -> None:
        """..."""
        self._switch_beddays_availability(
            new_availability=new_availability, 
            effective_on_and_from=self.sim.date,
            model_to_data_popsize_ratio=self.sim.modules["Demography"].initial_model_to_data_popsize_ratio
        )

This would still allow using the current _switch_beddays_availability implementation in tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to make a public/private method like is suggested above, though I'd quite like to avoid spreading the self.sim.modules... cross-class-coupling into the bed-days class if possible. switch_beddays_availability is already called by classes that have this cross-class-coupling anyway, so it might as well stay isolated to there?

model_to_data_popsize_ratio: float = 1.0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nicer (I think) if the user didn't need to pass this. It hasn't changed since the beginning of the simulation, so it seems awkward to pass it in just to instruct a change in availability to all/none/default.

) -> None:
"""
Action to be taken if the beddays availability changes in the middle
of the simulation.

If bed capacities are reduced below the currently scheduled occupancy,
inpatients are not evicted from beds and are allowed to remain in the
bed until they are scheduled to leave. Obviously, no new patients will
be admitted if there is no room in the new capacities.

:param new_availability: The new bed availability. See __init__ for details.
:param effective_on_and_from: First day from which the new capacities will be imposed.
:param model_to_data_popsize_ratio: As in initialise_population.
"""
# Store new bed availability
self.availability = new_availability
# Before we update the bed capacity, we need to store its old values
# This is because we will need to update the trackers to reflect the new#
# maximum capacities for each bed type.
old_max_capacities: pd.DataFrame = self._scaled_capacity.copy()
# Set the new capacity for beds
self.set_scaled_capacity(model_to_data_popsize_ratio)
# Compute the difference between the new max capacities and the old max capacities
difference_in_max = self._scaled_capacity - old_max_capacities
# For each tracker, after the effective date, impose the difference on the max
# number of beds
for bed_type, tracker in self.bed_tracker.items():
tracker.loc[effective_on_and_from:] += difference_in_max[bed_type]

def on_start_of_day(self):
"""Things to do at the start of each new day:
* Refresh inpatient status
Expand Down
6 changes: 5 additions & 1 deletion src/tlo/methods/healthsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2776,7 +2776,11 @@ def apply(self, population):
self.module.consumables.on_start_of_day(self.module.sim.date)

if 'beds_availability' in self._parameters:
self.module.bed_days.availability = self._parameters['beds_availability']
self.module.bed_days.switch_beddays_availability(
new_availability=self._parameters["beds_availability"],
effective_on_and_from=self.sim.date,
model_to_data_popsize_ratio=self.sim.modules["Demography"].initial_model_to_data_popsize_ratio
)

if 'equip_availability' in self._parameters:
self.module.equipment.availability = self._parameters['equip_availability']
Expand Down
71 changes: 71 additions & 0 deletions tests/test_beddays.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,3 +973,74 @@ def apply(self, person_id, squeeze_factor):
# Check that the facility_id is included for each entry in the `HSI_Events` log, including HSI Events for
# in-patient appointments.
assert not (log_hsi['Facility_ID'] == -99).any()

def test_beddays_availability_switch(seed):
"""
Test that calling bed_days.switch_beddays_availability correctly updates the
bed capacities and adjusts the existing trackers to reflect the new capacities.
"""
sim = Simulation(start_date=start_date, seed=seed)
sim.register(
demography.Demography(resourcefilepath=resourcefilepath),
healthsystem.HealthSystem(resourcefilepath=resourcefilepath),
)

# get shortcut to HealthSystem Module
hs: healthsystem.HealthSystem = sim.modules["HealthSystem"]

# Create a simple bed capacity dataframe with capacity designated for two regions
hs.parameters["BedCapacity"] = pd.DataFrame(
data={
"Facility_ID": [
128, #<-- patient 0 is admitted here
129,
],
"bedtype1": 5,
"bedtype2": 10,
}
)
sim.make_initial_population(n=100)
sim.simulate(end_date=start_date)

day_2 = start_date + pd.DateOffset(days=1)
day_3 = start_date + pd.DateOffset(days=2)
day_4 = start_date + pd.DateOffset(days=3)

bed_days = hs.bed_days
# Reset the bed occupancies
bed_days.initialise_beddays_tracker()
# Have a patient occupy a bed at the start of the simulation
bed_days.impose_beddays_footprint(person_id=0, footprint={"bedtype1": 3, "bedtype2": 0})

# Have the bed_days availability switch to "none" on the 2nd simulation day
bed_days.switch_beddays_availability("none", effective_on_and_from=day_2)

# We should now see that the scaled capacities are all zero
assert (
not bed_days._scaled_capacity.any().any()
), "At least one bed capacity was not set to 0"
# We should also see that bedtype1 should have -1 beds available for days 2 and 3 of the simulation,
# due to the existing occupancy and the new capacity of 0.
# It should have 4 beds available on the first day (since the original capacity was 5 and the availability
# switch happens day 2).
# It should then have 0 beds available after (not including) day 3
bedtype1: pd.DataFrame = bed_days.bed_tracker["bedtype1"]
bedtype2: pd.DataFrame = bed_days.bed_tracker["bedtype2"]

assert (
bedtype1.loc[start_date, 128] == 4 and bedtype1.loc[start_date, 129] == 5
), "Day 1 capacities were incorrectly affected"
assert (bedtype1.loc[day_2:day_3, 128] == -1).all() and (
bedtype1.loc[day_2:day_3, 129] == 0
).all(), "Day 2 & 3 capacities were not updated correctly"
willGraham01 marked this conversation as resolved.
Show resolved Hide resolved
assert (
(bedtype1.loc[day_4:, :] == 0).all().all()
), "Day 4 onwards did not have correct capacity"

# Bedtype 2 should have also have been updated, but there is no funny business here.
assert (
(bedtype2.loc[day_2:, :] == 0).all().all()
), "Bedtype 2 was not updated correctly"
assert (
(bedtype2.loc[start_date, :] == 10).all().all()
), "Bedtype 2 had capacity updated on the incorrect dates"