|
|
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 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 actions structures are organized by the object the action takes place on. The following files are where we define actions associated with various objects.
|
|
|
|
|
|
* [Anesthesia Machine Actions](https://gitlab.kitware.com/physiology/engine/blob/master/schema/proto/cdm/AnesthesiaMachineActions.proto)
|
|
|
* [Environment Actions](https://gitlab.kitware.com/physiology/engine/blob/master/schema/proto/cdm/EnvironmentActions.proto)
|
|
|
* [Inhaler Actions](https://gitlab.kitware.com/physiology/engine/blob/master/schema/proto/cdm/InhalerActions.proto)
|
|
|
* [Patient Actions](https://gitlab.kitware.com/physiology/engine/blob/master/schema/proto/cdm/PatientActions.proto)
|
|
|
|
|
|
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;
|
|
|
AcuteStressData AcuteStress = 2;
|
|
|
AirwayObstructionData AirwayObstruction = 3;
|
|
|
ApneaData Apnea = 4;
|
|
|
AsthmaAttackData AsthmaAttack = 5;
|
|
|
BrainInjuryData BrainInjury = 6;
|
|
|
BronchoconstrictionData Bronchoconstriction = 7;
|
|
|
CardiacArrestData CardiacArrest = 8;
|
|
|
ChestCompressionForceData ChestCompressionForce = 9;
|
|
|
ChestCompressionForceScaleData ChestCompressionForceScale = 10;
|
|
|
ChestOcclusiveDressingData ChestOcclusiveDressing = 11;
|
|
|
ConsciousRespirationData ConsciousRespiration = 12;
|
|
|
ConsumeNutrientsData ConsumeNutrients = 13;
|
|
|
ExerciseData Exercise = 14;
|
|
|
HemorrhageData Hemorrhage = 15;
|
|
|
IntubationData Intubation = 16;
|
|
|
MechanicalVentilationData MechanicalVentilation = 17;
|
|
|
NeedleDecompressionData NeedleDecompression = 18;
|
|
|
PericardialEffusionData PericardialEffusion = 19;
|
|
|
TensionPneumothoraxData TensionPneumothorax = 20;
|
|
|
SubstanceBolusData SubstanceBolus = 21;
|
|
|
SubstanceCompoundInfusionData SubstanceCompoundInfusion = 22;
|
|
|
SubstanceInfusionData SubstanceInfusion = 23;
|
|
|
UrinateData Urinate = 24;
|
|
|
|
|
|
// 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/cdm/cpp/patient/actions/SEAcuteStress.h](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/patient/actions/SEAcuteStress.h) and [engine/cdm/cpp/patient/actions/SEAcuteStress.cpp](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/patient/actions/SEAcuteStress.cpp) files and rename them to SE``your_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``
|
|
|
|
|
|
Add the Common Data Model Action to its Action Base Class
|
|
|
---------------------------------------------------------
|
|
|
The Action base class is responsible for translating the action object to and from the AnyAction object. Each action base class will have a Load and Unload method you will need to add code to. The following files are those action base classes:
|
|
|
|
|
|
* [Anesthesia Machine Action](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/system/equipment/anesthesiamachine/actions/SEAnesthesiaMachineAction.cpp)
|
|
|
* [Environment Action](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/system/environment/actions/SEEnvironmentAction.cpp)
|
|
|
* [Inhaler Action](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/system/equipment/inhaler/actions/SEInhalerAction.cpp)
|
|
|
* [Patient Action](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/patient/actions/SEPatientAction.cpp)
|
|
|
|
|
|
In our example, for adding a patient action we will add the following code
|
|
|
|
|
|
```cpp
|
|
|
// Add the header
|
|
|
#include "patient/actions/SEMyNewAction.h"
|
|
|
|
|
|
// Update the Load method
|
|
|
SEPatientAction* SEPatientAction::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* SEPatientAction::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 that the actions takes place on. The following files are those action collection managers:
|
|
|
|
|
|
* [Anesthesia Machine Action Collection](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/scenario/SEAnesthesiaMachineActionCollection.cpp)
|
|
|
* [Environment Action Collection](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/scenario/SEEnvironmentActionCollection.cpp)
|
|
|
* [Inhaler Action Collection](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/scenario/SEInhalerActionCollection.cpp)
|
|
|
* [Patient Action Collection](https://gitlab.kitware.com/physiology/engine/blob/master/cdm/cpp/scenario/SEPatientActionCollection.cpp)
|
|
|
|
|
|
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 and look for SEAcuteStress and copy the patterns in the file. (Again, we organize instructions alphabetically)
|
|
|
|
|
|
In the header file you will need to :
|
|
|
```cpp
|
|
|
// 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 :
|
|
|
```cpp
|
|
|
// 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);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
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
|
|
|
|
|
|
```cpp
|
|
|
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 value 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 already 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 next values every time-step. |