-
Notifications
You must be signed in to change notification settings - Fork 663
Generic Module Framework
Synthea contains a framework for defining modules using JSON. A JSON module configuration describes a progression of states and the transitions between them. On each Synthea generation "cycle", the generic framework processes states one at a time to trigger conditions, encounter, medications, and other clinical events.
The generic module framework currently supports the following states: Initial, Terminal, Guard, Delay, Encounter, ConditionOnset, MedicationOrder, Procedure, and Death.
The following states are also planned for future implementation: Lab, ConditionEnd, and MedicationEnd.
The Initial
state type is the first state that is processed in a generic module. It does not provide any specific function except to indicate the starting point, so it has no properties except its type
. The Initial state is the only state that requires a specific name: "Initial". In addition, it is the only state for which there can only be one in the whole module.
Supported Properties
- type: must be "Initial" (required)
Example
Please note that, for simplicity, state examples in this document will not include any transition properties. See the Transitions section for information about transitions.
{
"type": "Initial"
}
The Terminal
state type indicates the end of the module progression. Once a Terminal state is reached, no further progress will be made. As such, Terminal states cannot have any transition properties. If desired, there may be multiple Terminal states with different names to indicate different ending points -- but this has no actual effect on the records that are produced.
Supported Properties
- type: must be "Terminal" (required)
Example
{
"type": "Terminal"
}
The Guard
state type indicates a point in the module through which a patient can only pass if they meet certain logical conditions. For example, a Guard may block a workflow until the patient reaches a certain age, after which the Guard allows the module to continue to progress. Depending on the condition, a patient may be blocked by a Guard until they die -- in which case they never reach a Terminal
state. The Guard state's allow
property provides the logical condition which must be met to allow the module to continue to the next state. Please see the Logic section for more information about creating logical conditions.
Guard states are similar to conditional transitions in some ways, but also have an important difference. A conditional transition tests conditions once and uses the result to immediately choose the next state. A Guard state will test the same condition on every time cycle until the condition passes, at which point it progresses to the next state.
Supported Properties
- type: must be "Guard" (required)
- allow: the condition under which the Guard allows the module to progress to the next state; otherwise the module remains at the Guard state (required)
Example
The following is an example of a Guard state that only allows the module to continue if the patient is male and at least 40 years old.
{
"type": "Guard",
"allow": {
"condition_type": "And",
"conditions": [
{
"condition_type": "Gender",
"gender": "M"
},
{
"condition_type": "Age",
"operator": ">=",
"quantity": 40,
"unit": "years"
}
]
}
}
The Delay
state type introduces a pre-configured temporal delay in the module's timeline. As a simple example, a Delay state may indicate a one-month gap in time between an initial encounter and a followup encounter. The module will not pass through the Delay state until the proper amount of time has passed. The Delay state may define an exact time to delay (e.g., 4 days) or a range of time to delay (e.g., 5 - 7 days).
Implementation Detail
Synthea generation occurs in time cycles; currently 7-day cycles. This means that if a module is processed on a given date, the next time it is processed will be exactly 7 days later. If a delay expiration falls between cycles (e.g., day 3 of a 7-day cycle), then the first cycle after the delay expiration will effectively rewind the clock to the delay expiration time and process states using that time. Once it reaches a state that it can't pass through, it will process it once more using the original (7-day cycle) time.
Supported Properties
- type: must be "Delay" (required)
-
exact: an exact amount of time to delay (required if
range
is not set)- quantity: the number of units to delay (e.g., 4) (required)
-
unit: the unit of time pertaining to the quantity (e.g., "days"). Valid unit values are:
years
,months
,weeks
,days
,hours
,minutes
, andseconds
. (required)
-
range: a range indicating the allowable amounts of delay. The actual delay time will be chosen randomly from the range. (required if
exact
is not set)- low: the lowest number (inclusive) of units to delay (e.g., 5) (required)
- high: the highest number (inclusive) of units to delay (e.g., 7) (required)
-
unit: the unit of time pertaining to the range (e.g., "days"). Valid unit values are:
years
,months
,weeks
,days
,hours
,minutes
, andseconds
. (required)
Examples
The following is an example of a Delay state that delays exactly 4 days.
{
"type": "Delay",
"exact": {
"quantity": 4,
"unit": "days"
}
}
The following is an example of a Delay state that delays at least 5 days and at most 7 days.
{
"type": "Delay",
"range": {
"low": 5,
"high": 7,
"unit": "days"
}
}
The Encounter
state type indicates a point in the module where an encounter should take place. Encounters are important in Synthea because they are generally the mechanism through which the actual patient record is updated (a disease is diagnosed, a medication is prescribed, etc). The generic module framework supports integration with scheduled wellness encounters from Synthea's Encounters module, as well as creation of new stand-alone encounters.
Scheduled Wellness Encounters vs. Standalone Encounters
An Encounter state with the wellness
property set to true
will block until the next scheduled wellness encounter occurs. Scheduled wellness encounters are managed by the Encounters module in Synthea and, depending on the patient's age, typically occur every 1 - 3 years. When a scheduled wellness encounter finally does occur, Synthea will search the generic modules for currently blocked Encounter states and will immediately process them (and their subsequent states). An example where this might be used is for a condition that onsets between encounters, but isn't found and diagnosed until the next regularly scheduled wellness encounter.
An Encounter state without the wellness
property set will be processed and recorded in the patient record immediately. Since this creates an encounter, the encounter class and at least one code must be specified in the state configuration. This is how generic modules can introduce encounters that are not already scheduled by other modules.
Encounters and Related Events
Encounters are typically the mechanism through which a patient's record will be updated. This makes sense since most recorded events (diagnoses, prescriptions, and procedures) should happen in the context of an encounter. When an Encounter state is successfully processed, it will look through the previously processed states for un-recorded ConditionOnset instances that indicate it (by name) as the target_encounter
. If it finds any, it will record the corresponding diagnosis in the patient's record at the time of the encounter. This is the mechanism for onsetting a disease before it is discovered and diagnosed.
As soon as the Encounter state is processed, Synthea will continue to progress through the module. If any subsequent states identify the previous Encounter as their target_encounter
and they occur at the same time as the target encounter, then they will be added to the patient record. This is the preferred mechanism for simulating events that happen at the encounter (e.g., MedicationOrders, Procedures, and Encounter-caused ConditionOnsets).
Future Implementation Considerations
Future implementations should also consider a more robust mechanism for defining the length of an encounter and the activities that happen during it. Currently, encounter activities must start at the same exact time as the encounter start in order to be recorded. This, however, is unrealistic for multi-day inpatient encounters.
Supported Properties
- type: must be "Encounter" (required)
-
wellness: if
true
, indicates that this state should block until a regularly scheduled wellness encounter occurs (required ifclass
andcodes
are not set) -
class: indicates the class of the encounter, as defined in the EncounterClass value set (required if
wellness
is not set) -
codes[]: a list of codes indicating the encounter type (at least one required if
wellness
is not set)-
system: the code system. Currently, only
SNOMED-CT
is allowed. (required) - code: the code (required)
- display: the human-readable code description (required)
-
system: the code system. Currently, only
Examples
The following is an example of an Encounter state that blocks until a regularly scheduled encounter.
{
"type": "Encounter",
"wellness": true
}
The following is an example of an Encounter state indicating an ED visit.
{
"type": "Encounter",
"class": "emergency",
"codes": [{
"system": "SNOMED-CT",
"code": "50849002",
"display": "Emergency Room Admission"
}]
}
The ConditionOnset
state type indicates a point in the module where the patient acquires a condition. This is not necessarily the same as when the condition is diagnosed and recorded in the patient's record. In fact, it is possible for a condition to onset but never be discovered.
If the ConditionOnset state's target_encounter
is set to the name of a future encounter, then the condition will be diagnosed when that future encounter occurs. If the target_encounter
is set to the name of a previous encounter, then the condition will only be diagnosed if the ConditionOnset start time is the same as the encounter's start time. See the Encounter section above for more details.
Future Implementation Considerations
Although the generic module framework supports a distinction between a condition's onset date and diagnosis date, currently only the diagnosis date is recorded. In the future, the Synthea::Output::Record::condition
method should be updated to support an onset date.
Currently, the generic module framework does not provide a way to resolve (or abate) conditions. There are two ways this could potentially be implemented in the future: (1) by introducing a ConditionEnd
state (recommended), or (2) by introducing a property in ConditionOnset
to indicate its intended duration.
Supported Properties
- type: must be "ConditionOnset" (required)
- target_encounter: the name of the Encounter state at which this condition should be diagnosed and recorded (optional)
-
codes[]: a list of codes indicating the condition (at least one required)
-
system: the code system. Currently, only
SNOMED-CT
is allowed. (required) - code: the code (required)
- display: the human-readable code description (required)
-
system: the code system. Currently, only
Example
The following is an example of a ConditionOnset that should be diagnosed at the "ED_Visit" Encounter.
{
"type": "ConditionOnset",
"target_encounter": "ED_Visit",
"codes": [{
"system": "SNOMED-CT",
"code": "47693006",
"display": "Rupture of appendix"
}]
}
The MedicationOrder
state type indicates a point in the module where a medication should be prescribed. The MedicationOrder state must come after the target_encounter
Encounter state in the module, but must have the same start time as that Encounter; otherwise it will not be recorded in the patient's record. See the Encounter section above for more details.
The MedicationOrder
also supports identifying a previous ConditionOnset
as the reason
for the prescription.
Future Implementation Considerations
Currently, the generic module framework does not provide a way to end medications. There are two ways this could potentially be implemented in the future: (1) by introducing a MedicationEnd
state, or (2) by introducing a property in MedicationOrder
to indicate its intended duration.
Supported Properties
- type: must be "MedicationOrder" (required)
- target_encounter: the name of the Encounter state at which this medication should be prescribed. This Encounter must come before the MedicationOrder in the module, but must have the same start time as the MedicationOrder. (required)
-
codes[]: a list of codes indicating the medication (at least one required)
-
system: the code system. Currently, only
RxNorm
is allowed. (required) - code: the code (required)
- display: the human-readable code description (required)
-
system: the code system. Currently, only
- reason: the name of the ConditionOnset state which represents the reason for which the medication is prescribed. This ConditionOnset must come before the MedicationOrder in the module. (optional)
Example
The following is an example of a MedicationOrder that should be prescribed at the "Annual_Checkup" Encounter and cite the "Diabetes" ConditionOnset as the reason.
{
"type": "MedicationOrder",
"target_encounter": "Annual_Checkup",
"codes": [{
"system": "RxNorm",
"code": "860975",
"display": "24 HR Metformin hydrochloride 500 MG Extended Release Oral Tablet"
}],
"reason": "Diabetes"
}
The Procedure
state type indicates a point in the module where a procedure should be performed. The Procedure state must come after the target_encounter
Encounter state in the module, but must have the same start time as that Encounter; otherwise it will not be recorded in the patient's record. See the Encounter section above for more details.
The Procedure
also supports identifying a previous ConditionOnset
as the reason
for the procedure.
Future Implementation Considerations
Currently, the generic module framework does not provide a way to indicate the duration of a procedure. This would probably be best implemented by simply introducing a property in Procedure
to indicate its intended duration.
Supported Properties
- type: must be "Procedure" (required)
- target_encounter: the name of the Encounter state at which this procedure should be performed. This Encounter must come before the Procedure in the module, but must have the same start time as the Procedure. (required)
-
codes[]: a list of codes indicating the procedure (at least one required)
-
system: the code system. Currently, only
SNOMED-CT
is allowed. (required) - code: the code (required)
- display: the human-readable code description (required)
-
system: the code system. Currently, only
- reason: the name of the ConditionOnset state which represents the reason for procedure. This ConditionOnset must come before the Procedure in the module. (optional)
Example
The following is an example of a Procedure that should be performed at the "Inpatient_Encounter" Encounter and cite the "Appendicitis" ConditionOnset as the reason.
{
"type": "Procedure",
"target_encounter": "Inpatient_Encounter",
"codes": [{
"system": "SNOMED-CT",
"code": "6025007",
"display": "Laparoscopic appendectomy"
}],
"reason": "Appendicitis"
}
The Death
state type indicates a point in the module at which the patient dies. When the Death state is processed, the patient's death is immediately recorded and the patient's :is_alive
attribute is set to false
. The module will continue to progress to the next state(s) for the current cycle, but will not progress any further in future cycles. Typically, the Death state should transition to a Terminal
state.
Implementation Warning
If a Death
state is processed after a Delay
, it may cause inconsistencies in the record. This is because the Delay
implementation must rewind time to correctly honor the requested delay duration. If it rewinds time, and then the patient dies at the rewinded time, then any modules that were processed before the generic module may have created events and records with a timestamp after the patient's death.
Supported Properties
- type: must be "Death" (required)
Example
{
"type": "Death"
}
The generic module framework currently supports the following transitions: Direct, Distributed, and Conditional.
Direct
transitions are the simplest of transitions. They transition directly to the indicated state. The value of a direct_transition
is simply the name of the state to transition to.
Example
The following example demonstrates a state that should transition directly to the "Foo" state.
Please note that, for simplicity, transition examples in this document will always start at an Initial
state. In real modules, transitions can be applied to any state (except a Terminal
state) and can transition to any state.
{
"type": "Initial",
"direct_transition": "Foo"
}
Distributed
transitions will transition to one of several possible states based on the configured distribution. Distribution values are from 0.0 to 1.0, such that a value of 0.55 would indicate a 55% chance of transitioning to the corresponding state. A distributed_transition
consists of an array of distribution
/transition
pairs for which the distribution
values should sum up to 1.0.
If the distribution
values do not sum up to 1.0, the remaining distribution will transition to the last defined transition
state. For example, given distributions of 0.3 and 0.6, the effective distribution of the last transition will actually be 0.7.
If the distribution
values sum up to more than 1.0, the remaining distributions are ignored (for example, if distribution values are 0.75, 0.5, and 0.3, then the second transition will have an effective distribution of 0.25, and the last transition will have an effective distribution of 0.0).
Example
The following example demonstrates a state that should transition to the "Foo" state 15% of the time, the "Bar" state 55% of the time, and the "Baz" state 30% of the time.
{
"type": "Initial",
"distributed_transition": [
{
"distribution": 0.15,
"transition": "Foo"
},
{
"distribution": 0.55,
"transition": "Bar"
},
{
"distribution": 0.30,
"transition": "Baz"
}
]
}
Conditional
transitions will transition to one of several possible states based on conditional logic. A conditional_transition
consists of an array of condition
/transition
pairs which are tested in the order they are defined. The first condition that evaluates to true
will result in a transition to its corresponding transition
state. The last element in the condition_transition
array may contain only a transition
(with no condition
) to indicate a fallback transition when all other conditions are false
.
Please see the Logic section for more information about creating logical conditions.
Example
The following example demonstrates a state that should transition to the "Foo" state for male patients, the "Bar" state for female patients, and the "Baz" state for patients with an unrecognized gender value.
{
"type": "Initial",
"conditional_transition": [
{
"condition": {
"condition_type": "Gender",
"gender": "M"
},
"transition": "Foo"
},
{
"condition": {
"condition_type": "Gender",
"gender": "F"
},
"transition": "Bar"
},
{
"transition": "Baz"
}
]
}
The Guard state and Conditional transition use conditional (boolean) logic. The following condition types are currently supported: Gender, Age, And, Or, Not, True, and False.
The following condition types should be considered for future versions:
- Attribute: check if a patient attribute is present or compare it against a value
- PriorEvent: check if a patient event occurred
- PriorState: check if a specific state is in the module's state history
The Gender
condition type tests the patient's gender.
Supported Properties
- condition_type: must be "Gender" (required)
-
gender: the gender to test for. Synthea currently defines the following gender values:
M
: male, andF
: female (required)
Example
The following Gender condition will return true
if the patient is male; false
otherwise.
{
"condition_type": "Gender",
"gender": "M"
}
The Age
condition type tests the patient's age. The following units are supported: years
, months
, weeks
, days
, hours
, minutes
, and seconds
.
Supported Properties
- condition_type: must be "Age" (required)
-
operator: indicates how to compare the actual age against the quantity. Valid operator values are:
<
,<=
,==
,>=
,>
, and!=
. (required) - quantity: the number, corresponding to the unit, to indicate the age to test against (required)
-
unit: the unit corresponding to the quantity. Valid unit values are:
years
,months
,weeks
,days
,hours
,minutes
, andseconds
. (required)
Example
The following Age condition will return true
if the patient is 40 years old or more.
{
"condition_type": "Age",
"operator": ">=",
"quantity": 40,
"unit": "years"
}
The And
condition type tests that a set of sub-conditions are all true. If all sub-conditions are true, it will return true
, but if any are false, it will return false
.
Supported Properties
- condition_type: must be "And" (required)
- conditions[]: an array of sub-conditions to test (required)
Example
The following And condition will return true
if the patient is male and at least 40 years old; false
otherwise.
{
"condition_type": "And",
"conditions": [
{
"condition_type": "Gender",
"gender": "M"
},
{
"condition_type": "Age",
"operator": ">=",
"quantity": 40,
"unit": "years"
}
]
}
The Or
condition type tests that at least one of its sub-conditions is true. If any sub-condition is true, it will return true
, but if all sub-conditions are false, it will return false
.
Supported Properties
- condition_type: must be "Or" (required)
- conditions[]: an array of sub-conditions to test (required)
Example
The following Or condition will return true
if the patient is male or the patient is at least 40 years old; false
otherwise.
{
"condition_type": "Or",
"conditions": [
{
"condition_type": "Gender",
"gender": "M"
},
{
"condition_type": "Age",
"operator": ">=",
"quantity": 40,
"unit": "years"
}
]
}
The Not
condition type negates its sub-condition. If the sub-condition is true, it will return false
; if the sub-condition is false, it will return true
.
Supported Properties
- condition_type: must be "Not" (required)
- condition: the sub-condition to test (required)
Example
The following Not condition will return true
if the patient is not male; false
otherwise.
{
"condition_type": "Not",
"condition": {
"condition_type": "Gender",
"gender": "M"
}
}
The True
condition always returns true
. This condition is mainly used for testing purposes and is not expected to be used in any real module.
Supported Properties
- condition_type: must be "True" (required)
Example
The following True condition always returns true
.
{
"condition_type": "True"
}
The False
condition always returns false
. This condition is mainly used for testing purposes and is not expected to be used in any real module.
Supported Properties
- condition_type: must be "False" (required)
Example
The following False condition always returns false
.
{
"condition_type": "False"
}
The synthea_graphviz tool will generate diagrams for all generic modules found in lib/generic/modules/. The following is an example diagram representing a complete module for a made up disease: Examplitis.
The above diagram was generated based on the following JSON module file:
{
"name": "Examplitis",
"states": {
"Initial": {
"type": "Initial",
"conditional_transition": [
{
"condition": {
"condition_type": "Gender",
"gender": "M"
},
"transition": "Age_Guard"
},
{
"transition": "Terminal"
}
]
},
"Age_Guard": {
"type": "Guard",
"allow": {
"condition_type": "Age",
"operator": ">",
"quantity": 40,
"unit": "years"
},
"distributed_transition": [
{
"distribution": 0.10,
"transition": "Pre_Examplitis"
},
{
"distribution": 0.90,
"transition": "Terminal"
}
]
},
"Pre_Examplitis": {
"type": "Delay",
"range": {
"low": 0,
"high": 10,
"unit": "years"
},
"direct_transition": "Examplitis"
},
"Examplitis": {
"type": "ConditionOnset",
"target_encounter": "Wellness_Encounter",
"codes": [{
"system": "SNOMED-CT",
"code": "123",
"display": "Examplitis"
}],
"direct_transition": "Wellness_Encounter"
},
"Wellness_Encounter": {
"type": "Encounter",
"wellness": true,
"direct_transition": "Examplitol"
},
"Examplitol": {
"type": "MedicationOrder",
"target_encounter": "Wellness_Encounter",
"codes": [{
"system": "RxNorm",
"code": "456",
"display": "Examplitol"
}],
"reason": "Examplitis",
"distributed_transition": [
{
"distribution": 0.2,
"transition": "Pre_Examplotomy"
},
{
"distribution": 0.1,
"transition": "Last_Days"
},
{
"distribution": 0.7,
"transition": "Terminal"
}
]
},
"Pre_Examplotomy": {
"type": "Delay",
"range": {
"low": 18,
"high": 36,
"unit": "months"
},
"direct_transition": "Examplotomy_Encounter"
},
"Examplotomy_Encounter": {
"type": "Encounter",
"class": "daytime",
"codes": [{
"system": "SNOMED-CT",
"code": "ABC",
"display": "Examplotomy Encounter"
}],
"direct_transition": "Examplotomy"
},
"Examplotomy": {
"type": "Procedure",
"target_encounter": "Examplotomy_Encounter",
"codes": [{
"system": "SNOMED-CT",
"code": "789",
"display": "Examplotomy"
}],
"reason": "Examplitis",
"distributed_transition": [
{
"distribution": 0.1,
"transition": "Death"
},
{
"distribution": 0.9,
"transition": "Terminal"
}
]
},
"Last_Days": {
"type": "Delay",
"range": {
"low": 8,
"high": 20,
"unit": "years"
},
"direct_transition": "Death"
},
"Death": {
"type": "Death"
},
"Terminal": {
"type": "Terminal"
}
}
}
The generic module framework supports simple logging to show how patients progress through modules. When enabled, a table will be printed to standard out when each patient reaches a Terminal
state in the workflow. (Note that if a patient never reaches a Terminal
state, then that patients table will not be generated).
Logging is disabled by default. To enable logging, turn it on in the config/synthea.yml file:
generic:
log: true
Examples From the Examplitis Module
The following example shows the log of a patient who was female, so she went from "Initial" directly to "Terminal".
/===============================================================================
| Examplitis Log
|===============================================================================
| Entered | Exited | State
|---------------------------|---------------------------|-----------------------
| 1980-02-02T19:59:42-05:00 | 1980-02-02T19:59:42-05:00 | Initial
| 1980-02-02T19:59:42-05:00 | | Terminal
\===============================================================================
As opposed the the previous example, this example shows the log of a patient who acquired Examplitis and ultimately needed surgery. Since he went from "Examplotomy" to "Terminal", however, we know he did not die of Examplitis.
/===============================================================================
| Examplitis Log
|===============================================================================
| Entered | Exited | State
|---------------------------|---------------------------|-----------------------
| 1957-06-28T18:44:14-04:00 | 1957-06-28T18:44:14-04:00 | Initial
| 1957-06-28T18:44:14-04:00 | 1998-06-26T18:44:14-04:00 | Age_Guard
| 1998-06-26T18:44:14-04:00 | 2005-06-26T18:44:14-04:00 | Pre_Examplitis
| 2005-06-26T18:44:14-04:00 | 2005-06-26T18:44:14-04:00 | Examplitis
| 2005-06-26T18:44:14-04:00 | 2005-08-30T00:54:57-04:00 | Wellness_Encounter
| 2005-08-30T00:54:57-04:00 | 2005-08-30T00:54:57-04:00 | Examplitol
| 2005-08-30T00:54:57-04:00 | 2008-07-30T00:54:57-04:00 | Pre_Examplotomy
| 2008-07-30T00:54:57-04:00 | 2008-07-30T00:54:57-04:00 | Examplotomy_Encounter
| 2008-07-30T00:54:57-04:00 | 2008-07-30T00:54:57-04:00 | Examplotomy
| 2008-07-30T00:54:57-04:00 | | Terminal
\===============================================================================
Within the synthea repository, the following files and paths are relevant to the generic module framework:
- lib/generic/modules/: the path containing the JSON module files that Synthea should process
-
lib/generic/context.rb: the
Context
class definition; responsible for maintaining a modules state definition, state history, and current state for each patient. TheContext
class also processes states and transitions. -
lib/generic/logic.rb: the
Logic
module implements the methods corresponding to the support logical condition types -
lib/generic/states.rb: the
States
module implements the support state types -
modules/generic.rb: the
Generic
module defines the Synthearule
that is executed on every cycle. It loops through the modules in lib/generic/modules/, setting up and running the context for each module. - test/fixtures/generic/*.json: the test fixtures for all of the generic module framework unit tests
- test/generic_*_test.rb: the unit tests for the generic module framework's context, logic, and state implementations