Event Driven Design of a Resuscitation app

For some time now I have been thinking of writing an app to help with running a resuscitation protocol. The resuscitation protocol as provided by the Dutch Advanced Pediatric Life Support is rather straight forward. Yet to adhere to this protocol in a stressful situation for a patient with a life threatening condition is a challenge to many physicians or health care providers.

The Resuscitation Protocol for Children

The protocol itself is not overly complex, there are some common pitfalls where mistakes can occur. Also, in the event of a blackout (which can happen even to the most experienced practitioner), it would be most helpful if there would be an app to guide you through the various steps in the protocol.

The first thing to notice is that a protocol is very much event driven. Basically there are two kinds of events: 1) Observations, 2) Interventions. This results in the following domain model.

type Observation =
    | Unresponsive
    | NoSignsOfLife
    | SignsOfLife
    | Shockable
    | NonShockable
    | ChangeToNonShockable
    | ChangeToShockable
    | ChangeToROSC
    | ROSC

type Intervention =
    | BLS
    | CPR
    | ChargeDefib
    | UnChargeDefib
    | Shock
    | Adrenalin
    | Amiodarone

The observations drive the subsequent interventions which subsequently will be evaluated by … observations. So, the whole cycle is like:

Observation -> Intervention (one or more) -> Evaluation (by observation)

type Evaluation =
    | CheckForSignsOfLife of Observation list
    | CheckRhythm of Observation list
    | Finished

There are two major groups of observations: 1) Observations for signs of life and 2) observations what the current rhythm is, the rhythm check in the protocol. So you can implement the evaluations like:

let checkForSignsOfLife : Evaluation = 
	[ SignsOfLife; NoSignsOfLife ]
	|> CheckForSignsOfLife
	
let checkRhythm : Evaluation = 
	[ ROSC; NonShockable; ChangeToShockable ]
	|> CheckRhythm

If you check for life, the observation can be signs of life, or no signs of life. When you check the rhythm, the result can be either a ‘shock-able rhythm’ or a ‘non shock-able rhythm’ or a return of spontaneous circulation (ROSC). Pretty straightforward, huh.

The interventions and subsequent evaluation belong together and are triggered by an observation. So, let’s call this an intervention block.

type InterventionBlock = 
    { 
        Interventions : Intervention list 
        Evaluation : Evaluation
    } 

Then things get slightly more complicated as an intervention block can be run once, or be repeated. For example, the initial basic life support (BLS) is just performed once, but the shock-able rhythm and the non shock-able rhythm interventions are repeated over and over until ROSC. Therefore, you get:

type ProtocolItem = 
    | Repeatable of InterventionBlock
    | NonRepeatable of InterventionBlock

type ProtocolBlock = Observation * ProtocolItem list

type Protocol = ProtocolBlock list

So, a protocol item can either be a repeatable or a non-repeatable intervention block. Note that one observation can trigger multiple protocol items (that are either repeatable or non repeatable) and that they can be defined as a protocol block. Taken together, a list of protocol blocks form a protocol.

An example of a combination of non-repeatable intervention blocks with repeated intervention blocks is the shock-able protocol block:

Non repeatable and repeatable intervention blocks, triggered by the same observation

This can be nicely modeled like:

let shockable : ProtocolBlock =
	Shockable,
	[
		// First Shock
		{
			Interventions = [ Shock; CPR; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> NonRepeatable
		// Second Shock
		{
			Interventions = [ Shock; CPR; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> NonRepeatable
		// Third Shock
		{
			Interventions = [ Shock; CPR; Adrenalin; Amiodarone; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> NonRepeatable
		// Fourth Shock
		{
			Interventions = [ Shock; CPR; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> NonRepeatable
		// Fifth Shock
		{
			Interventions = [ Shock; CPR; Adrenalin; Amiodarone; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> NonRepeatable
		// Continue ...
		{
			Interventions = [ Shock; CPR; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> Repeatable
		{
			Interventions = [ Shock; CPR; Adrenalin; ChargeDefib ]
			Evaluation = 
				[ Shockable; ChangeToNonShockable; ChangeToROSC ]
				|> CheckRhythm
		} |> Repeatable
	]

Note that the first 5 protocol items are non repeatable. Meaning that when this protocol block is observed 5 times, these will not be run again. The remaining 2 protocol items will be run over and over again as long as the observation of a shock-able rhythm is made.

The events and commands that result from running the protocol are:

type Event =
    | Observed of Observation
    | Intervened of Intervention

type Command = 
    | Observe of Observation
    | Intervene of Intervention

So, actually, really simple. The event is that an observation has been observed or that an intervention has been ‘intervened’. The commands are that an observation has to be made (remember the evaluate in the intervention block) or that an intervention has to be performed.

To run the protocol the following processes have to be implemented:

type GetCurrentProtocolBlock = 
    Protocol -> Event list -> ProtocolBlock Option

type RemoveNonRepeatableFromBlock =
    Event list -> ProtocolBlock -> (int * ProtocolItem list)

type GetCurrentProtocolItem = (int * ProtocolItem list) -> ProtocolItem Option

type GetCommandsFromProtocolItem =
    Event list -> ProtocolItem -> Command list

type ProcessCommand = Command -> Event list -> Event list

First from what previously has happened (the event list) I can figure out where in the protocol I currently am, the GetCurrentProtocolBlock.

When I have this block, again using previous events, the non repeatable intervention blocks have to be removed, otherwise we keep repeating. The result is a number which is a count of remaining observations that triggers the protocol items. Remember, after 5 shock-able rhythm observations, there only remains a repeatable sequence of shock, CPR and de-charge defibrillator and shock, CPR, de-charge and give adrenaline. The RemoveNonRepeatableFromBlock takes care of that.

Next when it is known exactly what protocol item list has to be processed, the current protocol item is identified using the remaining number of observations. This is handled by the GetCurrentProtocolItem.

Finally, the list of interventions or (when the interventions have all been ‘Intervened’), the list of observations (the evaluation part of the intervention block) are turned into a list of commands that can be chosen from. The GetCommandsFromProtocolItem function.

This setup enables a protocol definition that drives the correct processing of the protocol. The protocol itself is quite readable for non programmer medical professionals.

let resuscitation : Protocol =
	[
		unresponsive            // Start with an unresponsive patient
		noSignsOfLife           // When there are no signs of life
		changeToShockable       // Change to the shockable rhythm block 
		nonShockable            // Start with a non shockable rhythm
		shockable               // Continue in the shockable rhythm block
		changeToNonShockable    // Change to a non shockable rhythm
		changeToROSC            // Change from shockable to ROSC
	]	

As a nice proof of concept I can run the resuscitation protocol by randomly selecting commands and read the output.

-- Start run 94                        
Event: Observed Unresponsive            
Event: Observed NoSignsOfLife           
Event: Intervened BLS                   
Event: Observed NonShockable            
Event: Intervened CPR                   
Event: Observed NonShockable            
Event: Intervened CPR                   
Event: Intervened Adrenalin             
Event: Observed NonShockable            
Event: Intervened CPR                   
Event: Observed ChangeToShockable       
Event: Intervened CPR                   
Event: Intervened ChargeDefib           
Event: Observed Shockable               
Event: Intervened Shock                 
Event: Intervened CPR                   
Event: Intervened ChargeDefib           
Event: Observed ChangeToNonShockable    
Event: Intervened UnChargeDefib         
Event: Intervened CPR                   
Event: Observed ChangeToShockable       
Event: Intervened CPR                   
Event: Intervened ChargeDefib           
Event: Observed ChangeToROSC            
Event: Intervened UnChargeDefib         
--- Finished

                          
--- Start run 95                        
Event: Observed Unresponsive            
Event: Observed SignsOfLife             
--- Finished

                          
--- Start run 96                        
Event: Observed Unresponsive            
Event: Observed NoSignsOfLife           
Event: Intervened BLS                   
Event: Observed ChangeToShockable       
Event: Intervened CPR                   
Event: Intervened ChargeDefib           
Event: Observed ChangeToNonShockable    
Event: Intervened UnChargeDefib         
Event: Intervened CPR                   
Event: Observed ChangeToShockable       
Event: Intervened CPR                   
Event: Intervened ChargeDefib           
Event: Observed ChangeToNonShockable    
Event: Intervened UnChargeDefib         
Event: Intervened CPR                   
Event: Observed ROSC                    
--- Finished

Also, testing is very easy to do. Just feed the test with a list of events and check whether the processing produces the appropriate command list.

let tests =
	testList "Test resuscitation protocol" [
	
		test "For an unresponsive patient" {
			es
			|> run
			|> fun cmds ->
				cmds
				|> checkCmdsLength 2
				
				cmds
				|> expectExists ((=) (Observe SignsOfLife))
								"Should check for signs of life"

				cmds 
				|> expectExists ((=) (Observe NoSignsOfLife))
								"Should check for signs of life"
		}
	
		test "When a patient has signs of life" {
			[ Observed SignsOfLife ]
			|> List.append es
			|> run
			|> fun cmds ->
				cmds
				|> checkCmdsLength 0

		}

		test "When a patient has no signs of life" {
			[ Observed NoSignsOfLife ]
			|> List.append es
			|> run
			|> fun cmds ->
				cmds
				|> checkCmdsLength 1
				
				cmds 
				|> expectExists ((=) (Intervene BLS)) 
								"Should start BLS"

		}

		test "When a patient has had basic life support" {
			[ Observed NoSignsOfLife; Intervened BLS ]
			|> List.append es
			|> run
			|> fun cmds -> 
				cmds
				|> checkCmdsLength 3
				// Check command 1
				cmds
				|> expectExists ((=) (Observe ChangeToShockable)) 
									 "Should check for a change to shockable rhythm"
				// Check command 2
				cmds
				|> expectExists ((=) (Observe NonShockable)) 
									 "Should check for a non shockable rhythm"
				// Check command 3
				cmds
				|> expectExists ((=) (Observe ROSC)) 
									 "Should check for a return of spontaneous circulation"
		}

		test "When a patient has a non shockable rhythm" {
			[ Observed NoSignsOfLife; Intervened BLS; Observed NonShockable ]
			|> List.append es
			|> run
			|> fun cmds -> 
				cmds
				|> checkCmdsLength 1
				// Check command 1
				cmds
				|> expectExists ((=) (Intervene CPR)) 
									 "Should start cardiopulmonary resuscitation"
		}

		test "When a patient has a shockable rhythm" {
			[ Observed NoSignsOfLife; Intervened BLS; Observed ChangeToShockable ]
			|> List.append es
			|> run
			|> fun cmds -> 
				cmds
				|> checkCmdsLength 1
				// Check command 1
				cmds
				|> expectExists ((=) (Intervene CPR)) 
									 "Should start cardiopulmonary resuscitation"
		}

		test "When a patient has a shockable rhythm and CPR" {
			[ Observed NoSignsOfLife
			  Intervened BLS
			  Observed ChangeToShockable
			  Intervened CPR ]
			|> List.append es
			|> run
			|> fun cmds -> 
				cmds
				|> checkCmdsLength 1
				// Check command 1
				cmds
				|> expectExists ((=) (Intervene ChargeDefib)) 
									 "Should charge the defibrillator"
		}

	]

This is I think a very nice example to showcase the power of event driven design using a algebraic type system like F# has. The code can be found on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *