The objective of this document is threefold:
Many of the “pot-holes” that were present in Drools Guvnor v5.3 have been fixed in this version, but some of the fixes have introduced a new “gotcha.” It is our hope that the pot-holes will eventually all be fixed in future versions of Guvnor, and that the “pot-hole” portion of this document will no longer be needed.
This document will also furnish some common patterns to accomplish certain tasks in building rules for OpenCDS in Guvnor.
This list may not be comprehensive, but it represents the things that we bumped into repeatedly. If you have found other “pot-holes”, or have developed work-arounds, please either update this document with what you have learned, or let us know so that we can update this document.
The following approaches can help you avoid the pot-holes, or recover from falling into them. Of course, it is always better to avoid them in the first place… J
Develop Habit of Exporting the Guvnor Repository FREQUENTLY
Because it is possible to get Guvnor packages and rules into inconsistent or “locked” and unopenable states, it is critical to export the package to create backups. You will avoid a lot of grief if you make it a habit to always do an export frequently and especially before you do any of the following:
|
Techniques to Refresh Screen
There are three common techniques to refresh the screen. These were more useful in 5.3 than they are in 5.4, which is a lot better about refreshing the screen. Note that all of these techniques may refresh assets that have been changed, thereby losing any changes you have not saved. Save First!
|
Avoid Damaging a Guided Rule
Guvnor 5.4 no longer appears to have a problem with “damaged rules,” but the following suggestions are still probably good ones…
The primary method I found in Guvnor 5.3 to damage a Guided Rule was to change a dependency, such as renaming or removing an Enumeration that is referenced in a DSL, and then adding the DSL to the Guided Rule. You could also damage a rule by making changes to a DSL without testing them, and updating the rule with the broken changes.
You can avoid ever having damaged rules by following this advice:
|
TIP: Put a comment at the end of your DSLs which includes the name of the DSL, and references all of the Enumerations in the DSL. Then, in the event of a problem, looking at a Source | View Source on a Guided Rule which has this comment imbedded in the source code, will show where you have a bad DSL or a missing Enumeration. For Example:
Note that the two comments highlighted above (one at the end of the [when] clause, and the other at the end of the [then] clause) will show in the generated source code, and the macro replacement of the DSL will include the selected drop-down value of the Enumeration named “VMRTemplateConcept.determinationMethodCode”. If the displayed source simply shows X=={X}, then the enumeration was not selected, or there was no Enumeration list by that name.
TIP: You can look for Enumerations all in one place by opening the package and clicking on the Edit tab. Then select the URL for package source, and save the source to an editor such as Notepad++. You can then use the search facilities in the editor to look for the Enumeration names. If you followed the previous TIP about putting a comment in the DSL with the name of the DSL, you will know exactly which DSL is referencing the Enumeration.
Recovering from a Single Damaged Rule
This information no longer seems to be pertinent in Guvnor 5.4, but is kept here “just in case.”
If the rule is damaged, but you can open it, you can do the following:
|
If you forgot to check the Validity of a rule before you saved it, and you can’t open it to fix it or delete/archive it, you may (or may not) first want to try to recover the contents of the rule by doing one of the following.
|
If you were unable to fix the rule by this point, you then have two options to get rid of the damaged rule:
|
Recovering from an Inconsistent Package
I haven’t seen this happen in Guvnor 5.4. But if it does, here are some things to try.
In some cases you can recover by doing the Archive and Restore technique described above. This is the most desirable approach, because it doesn’t lose any of your work.
In most other cases you can recover from one damaged package among a long list of packages by archiving and deleting the damaged package, and then recreating it from scratch. You may want to use the technique described above to save the source code for the package first, because once you delete the package, you will lose everything in it.
If you know what asset is damaged within the package, you can try to fix the problem by manually updating the asset using a text editor or Eclipse. Access the source code file by connecting to Guvnor’s working version of the repository using webdav (http://localhost:8080/drools-guvnor/org.drools.guvnor.Guvnor/webdav/ -- substitute the hostname and port of your Guvnor installation, and add the package and filename that you need to mess with, e.g.: globalarea/AssertionDsl.dsl ).
In the worst cases, your only choice is the nuclear option. This will lose all of the work you have done in all packages since you last made a useable backup. Which option you choose will partly depend on how recently you created a backup. Having a recent backup is always a good thing…
The following suggestions represent the approach that we have learned to use in writing Guided Rules using DSLs for Guvnor. Although we write most of our rules in Guvnor, the same general steps probably apply to writing technical rules, and developing rules in Eclipse. Please add (or let us know about) any further suggestions or comments that you come up with as you work with OpenCDS and Guvnor DSLs.
Use a whiteboard, or Notepad, or scratch paper to think about the different logical pieces that you need to complete the rule. It may be helpful to build a flowchart. If you plan to use Drools Flow / BPMN2 / Oryx for your rules, then you can use this tool to build the flowchart. Stay in this process until you can isolate elements that you need in the LHS of a rule, and elements that you need in the RHS of a rule. You should probably stay in this process until you have a first-cut idea of how many rules you need, and what each rule should do.
Before you start writing your final rules, develop re-useable Domain Specific Language (DSL) elements for as much of them as possible.
These DSLs should be written in terms of “concepts”, so that they are abstracting actual data and code values. For example, write your rules against a concept of “asthma”, instead of writing your rules against an ICD9 or SNOMED code that represents asthma. OpenCDS is designed to support the flexible mapping of codes at run-time to the logical concepts that you use to write the rules.
Start a DSL as a SandBox rule
A single named DSL can have zero to many [when] clauses (aka the left-hand-side or LHS of a rule), and zero to many [then] clauses (aka the right-hand-side or RHS of a rule). The purpose of the multiple elements is that each individual element is optional, and can be individually chosen or rejected when you build the rules. Until you are adept at creating DSLs, it is probably a good idea to keep your DSL simple, and avoid multiple elements.
Create a “sandbox rule” by creating a new Guided Rule in Guvnor, and name it “Sandbox” or some other throw-away name, because you are not going to keep this rule. Once the rule appears in your editor, add elements that you want your DSL to perform, and validate (but do NOT save) what you have added.
Repeat this process in the sandbox rule until it both validates, and includes the logical element of a rule that you want to turn into a DSL. Here is an example of a sandbox rule with the logic that you might want to turn into a DSL:
Go to the Source | View Source tab, and view the text of the generated sandbox rule. This text is going to become your DSL:
Create a new DSL, and copy and paste the text from the [when] clause of the generated sandbox rule into the new DSL. Since there are two lines in the [when] clause, and Drools implies an “and” connection between separate lines in the LHS of a rule, we will need to insert an “and” between the two lines, and put them all on one line. We will also parenthesize the entire thing.
Our first effort might look like this (and it is all on one logical line):
($encTypeConcept : EncounterTypeConcept( openCdsConceptCode == "C44" ) and $encEvent : EncounterEvent( id == $encTypeConcept.conceptTargetId, subjectIsFocalPerson == true, encounterEventTime.high < evalTime )) |
If we then clean up the variable names (the elements beginning with “$”) to make them globally unique in a long and complex rule with many DSLs, this text might look like this (and it is still all on one line):
($PatientEncounterEventDsl_encounterConcept_OutpatientEncounter : EncounterTypeConcept(openCdsConceptCode == "C44") and EncounterEvent(id == $PatientEncounterEventDsl_encounterConcept_OutpatientEncounter.conceptTargetId, subjectIsFocalPerson == true, encounterEventTime.getHigh()< $evalTime )) |
This is now a working DSL, but it only knows how to do the one thing you hard-coded into it. You are going to do several more steps to turn this text into a very useful and re-useable DSL:
1. Write a clear plain-language statement of what the DSL is supposed to describe, follow it by an equal-sign (“=”) and place it in front of the text you copied from the sandbox rule. This might be something like
Patient has previously had an outpatient encounter = |
followed by the text of the rule that you wrote to implement this, as shown above.
2. Abstract out the concept instances into enumerations that you identify by {X} – any variable name surrounded by curly braces, and with some additional information that will be demonstrated in examples below.
Also abstract out any quantities you might want to change into variables that you identify by {n} – any variable name surrounded by curly braces.
The left-hand side of the DSL has then become something like
Patient has previously had a {X:ENUM:EncounterTypeConcept.openCdsConceptCode} = |
3. Replace the specific concept on the right-hand side of the = with the variable name[s] in curly braces, and you get this for the entire DSL:
Patient has previously had a {X:ENUM:EncounterTypeConcept.openCdsConceptCode} = ($PatientEncounterEventDsl_encounterConcept_{X} : EncounterTypeConcept(openCdsConceptCode == "{X}") and EncounterEvent(id == $PatientEncounterEventDsl_encounterConcept_{X}.conceptTargetId, subjectIsFocalPerson == true, encounterEventTime.getHigh()< $evalTime )) |
4. Finally clean it up by adding a list name at the beginning, and a comment at the end.
The list name is the first thing you will see in the Guvnor list of elements that you can add to a Guided Rule, and helps you choose the right rule. It does not become a part of the final source code for the rule.
The comment at the end of the DSL helps us identify the DSL when it is one among many DSLs contained in a single rule. We include the name of the DSL, and show the selected values for all the variables that are being substituted into the rule. This step is invaluable for debugging.
The final DSL named “PatientEncounterEventDsl” contains exactly the following (with no carriage returns in it anywhere – it is all on one logical line):
[when] Pt.Enc.Past - Patient has previously had a {X:ENUM:EncounterTypeConcept.openCdsConceptCode} = ($PatientEncounterEventDsl_encounterConcept_{X} : EncounterTypeConcept(openCdsConceptCode == "{X}") and EncounterEvent(id == $PatientEncounterEventDsl_encounterConcept_{X}.conceptTargetId, subjectIsFocalPerson == true, encounterEventTime.getHigh()< $evalTime )) //DslUsed==PatientEncounterEventDsl|||X=={X} |
If we click the plus sign on our sandbox rule to add this DSL to the sandbox rule, the drop-down list that Guvnor gives us to select from shows the rule like this, which makes it clear exactly what rule we are selecting:
Once we add it to the sandbox rule, and select the option we want from the drop-down list, the rule will look like this:
If we look at the Source | view Source, the source will look like this (and you can see the comment at the end of the DSL has been populated with the selected concept instance ID for “Outpatient encounter” that we selected in the drop-down):
Study the Sample Rules Provided
They include examples of more complicated rules that combine multiple concepts and settable values in useful and re-useable ways.
As you can see from the Sample Guided Rules we included, you can often build a quite complex rule from an assortment of generic DSLs.
Write Test Scenarios for each rule, and test all of the following thoroughly:
Remember that you can create a copy of a Test Scenario and modify or add to it to create another Test Scenario. You don’t have to create each test from scratch. The goal is to achieve 100% testing coverage of all aspects of every rule.
You have two choices: You can either export the compiled binary package as a *.pkg file, or you can export the source code for the package as a *.drl file. The PKG file will run without a lag-time the first time it is called, but is completely opaque, will only run on the version of Drools that it was built in, and you can’t modify it. The DRL file on the other hand can be modified with a text editor while it is sitting in a running OpenCDS Knowledge Repository, and that can be very useful for debugging.
Note that a PKG must be used when a Guvnor package includes processes in addition to rules. If processes are used, OpenCDS expects you to configure the setup file that knowledgeModules.xml with the name you assigned to the primary process.
Note also that when you use a PKG in OpenCDS, you must include the classPath of the correct version of the DroolsAdapter in OpenCDS as an attribute of the correct entry in the knowledgeModules.xml configuration file.
In building OpenCDS, we carefully created a structure to separate generalized “clinical concepts” that a clinical domain expert would use to describe the “logic” of the rule from the actual “concept descriptor” codes used in the data. This makes it possible to keep the logic of the rules separated from the mappings between the specific concepts used in the actual data, and the generalized concepts used in the rules.
It is useful to keep the logic and the mappings separate, because they tend to need to be updated on different schedules, and sometimes even by different experts (clinical domain experts versus terminology experts).
One component of the technology we use in writing rules are Domain Specific Languages (DSLs). DSLs are created by programmers to provide a way for the clinical domain experts to express the generalized clinical concepts in language they are familiar with, and preferably in the same language they would use to describe what the rule should accomplish.
OpenCDS furnishes a number of sample DSLs, and we add more frequently.
Study the DSLs in the Guvnor Binary download, along with the suggestions below. These suggestions will help you understand why the rule is written the way it is written, and how we separate the technical details of Drools from the clinical descriptions of what the rule should do.
Our goal in writing the DSLs has been to: 1.) make the final rule perfectly understandable by a clinician, and 2.) produce the correct results. It may be possible to do things differently, and to improve on either or both of those aims. We welcome feedback and suggestions in that regard.
We also welcome and encourage additional patterns to add to the following list. Please note that the suggested prefix, terminating comment, and robust naming that we describe above is omitted for conciseness in these patterns. You should always add them to your final DSL to make the DSL re-useable.
[Event] has happened
[when] Patient has previously had a {X:ENUM:<conceptType>.openCdsConceptCode} = ($x: <conceptType>(openCdsConceptCode == "{X}") and <clinicalStatement>(id == $x.conceptTargetId, subjectIsFocalPerson == true, <clinicalStatementEventTime>.high <= $evalTime )) |
Definitions of elements in the above pattern:
$x should be expanded to a meaningful name,
<conceptType> represents the name of an OpenCDS Concept Type, such as “EncounterTypeConcept”
<clinicalStatement> represents the name of an OpenCDS vMR clinical statement Type, such as “EncounterEvent”
<clinicalStatementEventTIme> represents the name of the relevant event time element in the particular clinical statement Type.
[Event] has happened at least {n} times
[Event] has happened in last {n} [timeUnits]
[Event] has happened at least {n1} times in last {n2} [timeUnits]
[Event] happened between {n1} and {n2} [timeUnits] ago
[Event] as tag {idTag}…
then uses all patterns shown above under Events in General
[Entity] has role {targetRole} to {clinicalStatementClass} identified as tag {idTag}
[Entity] has role {targetRole} to {clinicalStatementType} identified as tag {idTag} during {relationshipTimeInterval}
[Event] is {targetRelationshipToSource} of {clinicalStatementClass} identified as tag {idTag}…
then uses all patterns shown above under Events in General
[AdministrableSubstance] {substanceCode} [of type {entityType} ][strength {strength} ][form {form} ][brand {substanceBrandCode} ][generic {substanceGenericCode} ][mfg {manufacturer} ][lot# {lotNo} ]
Only the substanceCode is strictly required, but some use cases may require some or all of the additional elements. More specific patterns will be developed as the need arises.
Not all possible patterns are shown. The ones shown cover the use cases listed below. It is expected that some elements in all patterns may not be needed in particular constrained use cases, and that more specific patterns will be created to meet this need.
Not all possible patterns are shown. The ones shown cover the use cases listed below. It is expected that some elements in all patterns may not be needed in particular constrained use cases, and that more specific patterns will be created to meet this need.
Check the OpenCDS website for output templates that are already defined, and select an appropriate one. Use the templateId(s) assigned to that template in your output response message. This furnishes information to external software to define how your output is structured.
If an appropriate output template does not already exist, create a definition of one or more response message(s) that will suit the needs of your KM(s), and ask OpenCDS to assign new templateId values. This will be done quickly, and will be posted with the OpenCDS documentation so that it can be used by others. Write your definition using the TemplateId Request Form available on the OpenCDS website.
Create an ObservationResult as a “wrapper” for all of the response messages from each KnowledgeModule (aka Drools “Package”, or a “ruleset”) that OpenCDS was requested to apply to the current dataset. Use one of the DSLs whose name begins with “OutputRoot…” as this wrapper.
Imbed any additional ObservationResults and other clinical statements within this wrapper by adding a ClinicalStatementRelationship that relates the statement you wish to imbed to the wrapper statement. This puts the responses in the proper context when you are running multiple rules against a single dataset. Sample DSLs to imbed additional clinical statements have names that begin “OutputNested…”.
You are not limited to imbedding just ObservationResults in a wrapper. You can imbed any clinical statement(s) in whatever order is useful and meaningful. You can imbed all or portions of the submitted data by creating a ClinicalStatementRelationship to the wrapper element, in the same way that the “OutputNested…” DSLs do it.
Note that you can return both “typed” values and text values in an ObservationResult, and it is sometimes useful to include both a code, and explanatory text designed for humans (for validation and debugging purposes, even if the output is intended solely for machine processing). For example, you might return something like the “originalText” in the following examples to express the full contextual meaning of the code/codeSystem returned in an observationValue:
<observationValue>
<concept code=”…” codeSystem=”…” originalText=”Patient is overdue for an Hba1C test.”/>
</observationValue>
<observationValue>
<physicalQuantity value=”3.14” units=”ml”/>
</observationValue>
<interpretation code=”…” codeSystem=”…” originalText=”Extremely high value, further treatment should be considered”/>
Response messages most commonly include one or more of the following:
<Include an example of a response message that includes all of the above elements:>
There are two techniques for creating return messages from your rules. They can be combined in rules to create quite complex and specific output patterns.
Flag Input in FactLists as “toBeReturned”
Any of the input clinicalStatement and entity data that is present in Drools fact lists can be flagged to be returned in the output message from OpenCDS. By default, the Patient ID and Demographics (and the same thing for OtherEvaluatedPersons) are the only thing that will be returned, but you can add to that from your rules, if required.
You will undoubtedly need to return additional information. In some cases you might want to set some value on an input clinical statement, and return that clinical statement as output. For example, you might want to populate an Interpretation on an ObservationResult and return the input ObservationResult with that one additional element as an output or response. This is easily done by setting the toBeReturned flag to true on that particular clinicalStatement.
Create New Output Elements in Global NamedObjects
The basic technique for returning a result is to do the following:
|
Note that other output elements (e.g., ClinicalStatementRelationship, Entity, etc.) can also be returned as a part of the output. These (and any other output elements you create from scratch) must also be added to “namedObjects” in order to appear in the response message from OpenCDS. Examples of how to do this appear in the sample “OutputNested…” DSLs. Remember:
You can create an object (of any type), and add it to “namedObjects,” and you can reference the contents of the object in a rule, and/or use it as part of the output response from OpenCDS, but Drools will not fire a rule based on a change to the contents of a global variable (which is what “namedObjects” is). Drools only fires rules based on changes to facts in fact lists.
You can create new facts while running a rule, and reason on them. Changes to these new facts can cause rules to fire (unlike changes to globals). However, new facts will not be returned to OpenCDS to be used as response messages unless they are structured as ClinicalStatements and you also add them to “namedObjects.”
See the sample DSLs whose names begin with “Output…” for examples.
asdf
Change Log
Date | Author | Notes |
12-14-2011 | David Shields | Initial document without best practices section |
12-15-2011 | David Shields | Finished first draft of best practices section |
12-20-2011 | Ken Kawamoto | Minor edits |
12-20-2011 | David Shields | Added screen shots that were omitted earlier |
1-12-2012 | David Shields | Added one new “pot-hole” and work-around / fix |
6-28-2012 | David Shields | Edited for Drools Guvnor v5.4 |
7-5-2012 | David Shields | Added more “pot-holes”, worked on DSL Patterns |