Conditions are the method used to get instructions into Pulse and direct a physiology model to change its patient physiology to a new homeostatic stabilization point. This happens during stabilization, which is when Pulse examines the patient data and configures itself to meet the provided inputs (i.e. stabilize to the requested mean arterial blood pressure). The physiology model is dependent on the model implementation, while a condition is a generic data structure usually associated with a general long-term state of the patient. An engine is free to interpret and modify its models according to the intent of the condition and the data it provides. The engine will take the condition into account during stabilization and modify the patient to come to a new homeostatic physiology with the condition taken into account.
In this post, we will discuss how to create the infrastructure to design and implement a new condition in the Common Data Model so it can be included in scenarios and used by Pulse integrators. We will also look at how a physiology modeler can use the condition in their methodology implementation.
Create the Common Data Model Condition Proto buffer
You will need to define the data structure for your condition. The data structures are defined in Google Protocol Buffers. The condition structures are organized by the object on which the condition takes place. The following files are where we define conditions associated with various objects.
For this discussion, we will look at adding a patient condition, but the same principles can be applied to other object-related conditions.
Open the PatientConditions.proto file in your favorite editor and add the following. (Typically we strive to keep the conditions in alphabetical order) Note the naming convention is to end each data structure with Data
message MyNewConditionData
{
// Set the first field to be the encapsulation of the base class
// protobuf does support inheritance..
PatientConditionData PatientCondition = 1;
// Next add the properties associated with your action definition
Scalar0To1Data Severity = 2;
}
Next, add your new condition object to the list of available patient conditions.
message AnyPatientConditionData
{
oneof Condition
{
AcuteRespiratoryDistressSyndromeData AcuteRespiratoryDistressSyndrome = 1;
ChronicAnemiaData ChronicAnemia = 2;
ChronicObstructivePulmonaryDiseaseData ChronicObstructivePulmonaryDisease = 3;
ChronicPericardialEffusionData ChronicPericardialEffusion = 4;
ChronicRenalStenosisData ChronicRenalStenosis = 5;
ChronicVentricularSystolicDysfunctionData ChronicVentricularSystolicDysfunction = 6;
ConsumeMealData ConsumeMeal = 7;
ImpairedAlveolarExchangeData ImpairedAlveolarExchange = 8;
PneumoniaData Pneumonia = 9;
PulmonaryFibrosisData PulmonaryFibrosis = 10;
PulmonaryShuntData PulmonaryShunt = 11;
SepsisData Sepsis = 12;
// 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
MyNewConditionData MyNewCondition = 1001;
}
}
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
Update Convergence Criteria
A condition is applied during patient stabilization in order to ensure the homeostatic baselines properly take into account any and all condition data set on the patient. Each condition can modify any values in the engine, with the goal of affecting various system-level properties. At the end of stabilization, all of these affected system properties need to be in a homeostatic state, i.e. at a relatively level value. We provide a way to define your convergence criteria for your condition in our Data Spread Sheet. You will need to add a convergence criteria block for your condition in this spreadsheet. We suggest you copy the standard convergence block and add system-level data specifically associated with your condition. You provide the amount of change the value needs to stay within during stabilization to signal that the condition is stable and the engine can start the simulation.
With our changes in place, we will need to generate the convergence data file. From the <path/to/pulse/build>/install/bin directory run the following command:
cmake -DTYPE:STRING=genData -P run.cmake
# There is also .bat and .sh scripts to make this a little easier
# On windows, you can type
run genData
# or, on Linux
./run.sh genData
Create the Common Data Model C++ Condition
Now we are ready to create the C++ files specific to this condition class. The easiest thing to do is to create copies of engine/src/cpp/cdm/patient/conditions/SEChronicAnemia.h and engine/src/cpp/cdm/patient/conditions/SEChronicAnemia.cpp files and rename them to SEyour_condition_name
.h/.cpp Then replace the string AcuteStress
with the name of your condition, then create the properties to match your condition 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 Condition 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 condition we will add something like the following code to the appropriate header
// Forward declare
CDM_BIND_DECL2(MyNewAction)
// Methods to implement
static void Load(const cdm::MyNewConditionData& src, SEMyNewCondition& dst);
static cdm::MyNewConditionData* Unload(const SEMyNewCondition& src);
static void Serialize(const cdm::MyNewConditionData& src, SEMyNewCondition& dst);
static void Serialize(const SEMyNewCondition& src, cdm::MyNewConditionData& dst);
static void Copy(const SEMyNewCondition& src, SEMyNewCondition& dst);
Code
Implement the newly added methods. I suggest you just copy another condition class method set and use the correct class names.
The Serialization class is also responsible for translating the condition object to and from the AnyCondition object. You will need to update the following methods to ensure generic condition support.
// Update the Load method
SEPatientCondition* PBPatientCondition::Load(const cdm::AnyPatientConditionData& any, SESubstanceManager& subMgr)
{
switch (any.Condition_case())
{
case cdm::AnyPatientConditionData::ConditionCase::kMyNewCondition:
{
SEMyNewCondition* c = new SEMyNewCondition();
SEMyNewCondition::Load(any.mynewcondition(), *c);
return c;
}
...
// Update the Unload Method
// Note we will want the cast/if block to be alphabetical order
cdm::AnyPatientConditionData* PBPatientCondition::Unload(const SEPatientCondition& action)
{
cdm::AnyPatientConditionData* any = new cdm::AnyPatientConditionData();
const SEMyNewCondition* mnc= dynamic_cast<const SEMyNewCondition*>(&action);
if (mnc!= nullptr)
{
any->set_allocated_mynewcondition(SEMyNewCondition::Unload(*mnc));
return any;
}
...
Add the Common Data Model Condition to the Condition Manager
The condition manager contains different classes to organize the collection of conditions associated with the engine. Since there are so few conditions, there is only one manager for managing environment and patient conditions (unlike the action manager).
In this example, we will basically duplicate the code that manages the SEChronicAnemia condition. Just like we used the SEChronicAnemia condition files as a basis, you can go through the SEConditionManager files and look for SEChronicAnemia and copy the patterns in the file. Again, we organize instructions alphabetically.
In the header file you will need to :
// Include the header to your action file
#include "patient/conditions/SEMyNewCondition.h"
// Create methods to expose your new condition
bool HasMyNewCondition() const;
SEMyNewCondition* GetMyNewCondition() const;
// Add the member variable
SEMyNewCondition* m_MyNewCondition;
In the cpp file you will need to :
// initialize the member variable in the constructor
m_MyNewCondition = nullptr;
// Support the clearing the condition
SAFE_DELETE(m_MyNewCondition);
// Support serialization of the condition
if (src.HasMyNewCondition())
dst.mutable_anycondition()->AddAllocated(SECondition::Unload(*src.m_MyNewCondition));
// Support Processing the condition
// Notice how we make a copy of any incoming condition,
// so for a user to change an condition they must call Process
const SEMyNewCondition* mine = dynamic_cast<const SEMyNewCondition*>(&condition);
if (mine != nullptr)
{
if (HasMyNewCondition())
{
Error("Cannot have multiple MyNewCondition conditions");
m_Conditions.mutable_anycondition()->RemoveLast();
return false;
}
m_MyNewCondition = new SEMyNewCondition();
cData->mutable_patientcondition()->set_allocated_mynewcondition(SEMyNewCondition::Unload(*a));
SEMyNewCondition::Load(cData->mutable_patientcondition()->mynewcondition(), *m_MyNewCondition);
return true;
}
// Fill out your exposed methods
bool SEConditionManager::HasMyNewCondition() const
{
return m_MyNewCondition == nullptr ? false : m_MyNewCondition->IsValid();
}
SEMyNewCondition* SEConditionManager::GetMyNewCondition() const
{
return m_MyNewCondition;
}
// Go to the condition manager and add logic to the GetActiveConditions method
void SEConditionManager::GetActiveConditions(std::vector<const SECondition*>& conditions) const
{
if(HasMyNewCondition())
conditions.push_back(GetMyNewCondition());
}
With this file complete, the condition has been added to the Common Data Model and the engine is able to accept this condition and the engine may now implement logic based on this condition.
Adding Support for a Condition to the Engine
You will need to identify the system(s) you wish to check for this condition and put the following code inside
TODO