Actions are the method used to get instructions into Pulse and direct a physiology model to change. The physiology model is dependent on the model implementation, while an action is generic and usually associated with a general procedure done to the patient. An engine is free to interpret and modify its models according to the intent of the action and the data it provides.
In this post, we will discuss how to create the infrastructure to design and implement a new action in the Common Data Model so it can be included in a scenario and used by Pulse integrators. We will also look at how a physiology modeler can use the action in their methodology implementation.
Create the Common Data Model Action Proto buffer
You will need to define the data structure for your action. The data structures are defined in Google Protocol Buffers. The action structures are organized by the object on which the action takes place. The following files are where we define actions associated with various objects.
- Anesthesia Machine Actions
- Bag Valve Mask Actions
- Environment Actions
- Inhaler Actions
- Mechanical Ventilator Actions
- Patient Actions
For this discussion, we will look at adding a patient action, but the same principles can be applied to other object-related actions.
Open this PatientActions.proto file in your favorite editor and add the following. (Typically we strive to keep the actions in alphabetical order) Note the naming convention is to end each data structure with Data
message MyNewActionData
{
// Set the first field to be the encapsulation of the base class
// protobuf does support inheritance..
PatientActionData PatientAction = 1;
// Next add the properties associated with your action definition
Scalar0To1Data Severity = 2;
}
Next, add your new action object to the list of available patient actions.
message AnyPatientActionData
{
oneof Action
{
PatientAssessmentRequestData Assessment = 1;
AcuteRespiratoryDistressSyndromeExacerbationData AcuteRespiratoryDistressSyndromeExacerbation = 2;
AcuteStressData AcuteStress = 3;
AirwayObstructionData AirwayObstruction = 4;
ArrhythmiaData Arrhythmia = 5;
AsthmaAttackData AsthmaAttack = 6;
BrainInjuryData BrainInjury = 7;
BronchoconstrictionData Bronchoconstriction = 8;
ChestCompressionData ChestCompression = 9;
ChestCompressionAutomatedData ChestCompressionAutomated = 10;
ChestCompressionInstantaneousData ChestCompressionInstantaneous = 11;
ChestOcclusiveDressingData ChestOcclusiveDressing = 12;
ChronicObstructivePulmonaryDiseaseExacerbationData ChronicObstructivePulmonaryDiseaseExacerbation = 13;
ConsciousRespirationData ConsciousRespiration = 14;
ConsumeNutrientsData ConsumeNutrients = 15;
DyspneaData Dyspnea = 16;
ExerciseData Exercise = 17;
HemorrhageData Hemorrhage = 18;
HemothoraxData Hemothorax = 19;
ImpairedAlveolarExchangeExacerbationData ImpairedAlveolarExchangeExacerbation = 20;
IntubationData Intubation = 21;
MechanicalVentilationData MechanicalVentilation = 22;
NeedleDecompressionData NeedleDecompression = 23;
PericardialEffusionData PericardialEffusion = 24;
PneumoniaExacerbationData PneumoniaExacerbation = 25;
PulmonaryShuntExacerbationData PulmonaryShuntExacerbation = 26;
RespiratoryFatigueData RespiratoryFatigue = 27;
RespiratoryMechanicsConfigurationData RespiratoryMechanicsConfiguration = 28;
SubstanceBolusData SubstanceBolus = 29;
SubstanceCompoundInfusionData SubstanceCompoundInfusion = 30;
SubstanceInfusionData SubstanceInfusion = 31;
SupplementalOxygenData SupplementalOxygen = 32;
TensionPneumothoraxData TensionPneumothorax = 33;
TubeThoracostomyData TubeThoracostomy = 34;
UrinateData Urinate = 35;
// Set this property to a large number, if you do submit this new action
// As a merge request into the master branch, we will assign it a new id
// And properly integrate it into the system
MyNewActionData MyNewAction = 1001;
}
}
With our changes in place, we will need to generate the appropriate bindings classes. From the <path/to/pulse/build>/install/bin directory run the following command:
cmake -DTYPE:STRING=protoc -P run.cmake
# There is also .bat and .sh scripts to make this a little easier
# On windows, you can type
run protoc
# or, on Linux
./run.sh protoc
Create the Common Data Model C++ Action
Now we are ready to create the C++ files specific to this action class. The easiest thing to do is to create copies of engine/src/cpp/cdm/patient/actions/SEAcuteStress.h and engine/src/cpp/cdm/patient/actions/SEAcuteStress.cpp files and rename them to SEyour_action_name
.h/.cpp Then replace the string AcuteStress
with the name of your action, then create the properties to match your action Protobuf structure. Note the naming convention is to start each file and class name with SE
Next, add the new files to the cdm/files.cmake file, add your header to this section, and your cpp to this section. Files are listed in aphabetical order, so add your files appropriately. Once added you can build the ZERO_CHECK target to update your build environment to include these new files.
Add the Serialization Support for your Action Class
All CDM classes support serialization to and from a binding class. You will need to update the appropriate binding class for your action type.
Headers
In our example, for adding a patient action we will add something like the following code to the appropriate header
// Forward declare
CDM_BIND_DECL2(MyNewAction)
// Add the following methods to the class
static void Load(const cdm::MyNewActionData& src, SEMyNewAction& dst);
static cdm::MyNewActionData* Unload(const SEMyNewAction& src);
static void Serialize(const cdm::MyNewActionData& src, SEMyNewAction& dst);
static void Serialize(const SEMyNewAction& src, cdm::MyNewActionData& dst);
static void Copy(const SEMyNewAction& src, SEMyNewAction& dst);
Code
Implement the newly added methods. I suggest you just copy another action class method set and use the correct class names.
The Serialization class is also responsible for translating the action object to and from the AnyAction object. You will need to update the following methods to ensure generic action support.
// Update the Load method
SEPatientAction* PBPatientAction::Load(const cdm::AnyPatientActionData& any, SESubstanceManager& subMgr)
{
switch (any.Action_case())
{
// Note we will want this to be alphabetical in the switch
case cdm::AnyPatientActionData::ActionCase::kMyNewAction:
{
SEMyNewAction* a = new SEMyNewAction();
SEMyNewAction::Load(any.mynewaction(), *a);
return a;
}
...
// Update the Unload Method
// Note we will want the cast/if block to be alphabetical order
cdm::AnyPatientActionData* PBPatientAction::Unload(const SEPatientAction& action)
{
cdm::AnyPatientActionData* any = new cdm::AnyPatientActionData();
const SEMyNewAction* a= dynamic_cast<const SEMyNewAction*>(&action);
if (a != nullptr)
{
any->set_allocated_mynewaction(SEMyNewAction::Unload(*a));
return any;
}
...
Add the Common Data Model Action to the Action Manager
The action manager contains different classes to organize the collection of actions associated with the object on which the actions take place. The following files are those action collection managers:
Since we are adding a patient action, we will edit the SEPatientActionCollection.h/.cpp files. Note this example is for one instance of the action for the entire engine, there are patterns in this file if want to, for example, have an instance of the action be associated with a compartment or some other way to handle multiple instances of the action type. Also, note the example of the Tension Pneumothorax actions. The one action can be of 2 different types and associated with the two lungs. Hence the interface was designed to check if any pneumothorax actions are present, then method for each combination of type/location. This exposure is up to you and what makes the most sense for your action to present to model developers. In this example, we are taking a simple approach to adding this new action. Just like we used the SEAcuteStress action files as a basis, you can go through the SEPatientActionCollection files, look for SEAcuteStress, and copy the patterns in the file. Note again that we organize instructions alphabetically.
In the header file you will need to :
// Include the header to your action file
#include "patient/actions/SEMyNewAction.h"
// Create methods to expose your new action
bool HasMyNewAction() const;
SEMyNewAction* GetMyNewAction() const;
void RemoveMyNewAction();
// Add the member variable
SEMyNewAction* m_MyNewAction;
In the cpp file you will need to :
// initialize the member variable in the constructor
m_AcuteStress = nullptr;
// Support the clearing the action
RemoveMyNewAction();
// Support serialization of the action
if (src.HasMyNewAction())
dst.mutable_anyaction()->AddAllocated(SEAction::Unload(*src.m_MyNewAction));
// Support Processing the action
// Notice how we make a copy of any incoming action,
// so for a user to change an action they must call Process
const SEMyNewAction* mine = dynamic_cast<const SEMyNewAction*>(&action);
if (mine != nullptr)
{
if (m_MyNewAction == nullptr)
m_MyNewAction = new SEMyNewAction();
any.set_allocated_mynewaction(SEMyNewAction::Unload(*mine));
SEMyNewAction::Load(any.mynewaction(), *m_MyNewAction);
if (!m_MyNewAction->IsActive())
RemoveMyNewAction();
return true;
}
// Fill out your exposed methods
bool SEPatientActionCollection::HasMyNewAction() const
{
return m_MyNewAction == nullptr ? false : m_MyNewAction->IsActive();
}
SEMyNewAction* SEPatientActionCollection::GetMyNewAction() const
{
return m_MyNewAction;
}
void SEPatientActionCollection::RemoveMyNewAction()
{
SAFE_DELETE(m_MyNewAction);
}
// Go to the appropriate action manager and add logic to the GetActiveActions method
void SEPatientActionCollection::GetActiveActions(std::vector<const SEAction*>& actions) const
{
if(HasMyNewAction())
actions.push_back(GetMyNewAction());
}
With this file complete, the action has been added to the Common Data Model and the engine is able to accept these actions and the engine may now implement logic based on this action.
Adding Support for an Action to the Engine
You will need to identify the system(s) you wish to check for this action and put the following code inside a method that gets called during PreProcess.
if (m_data.GetActions().GetPatientActions().HasMyNewAction())
{
SEMyNewAction* s = m_data.GetActions().GetPatientActions().GetMyNewAction();
// Pull data from the action
double severity = s->GetSeverity().GetValue();
// Generate a multiplier based on the action data
multiplier = GeneralMath::LinearInterpolator(0, 1, 0, 30, severity);
// Apply the multiplier to something in the circuit
double new_next = CircuitPath->GetNextResistance(FlowResistanceUnit::mmHg_s_Per_mL) * multiplier;
CircuitPath->GetNextResistance().SetValue(new_next, FlowResistanceUnit::mmHg_s_Per_mL);
}
Note that this code will get executed every time-step. You should take into consideration that circuit elements (i.e, resistances, compliances, etc.) store three temporal parameters - the “next” value that will be used in the upcoming circuit calculation for the state of the next time-step, the “current” value that is used for the last circuit calculation, and the “baseline” value that is the unmodified original homeostatic value. At the beginning of each time-step, the next value is replaced with the baseline value.
A model should create a multiplier value based on the action data. That multiplier should then be applied to the next value each time-step (the next value may or may not have already been modified by a different action). Actions should not modify baseline values or current values, since they are merely used as input for transient calculations. Typically speaking, an action that remains active will call the same code to modify the next values every time-step.