Rule HOWTO

What are rules?

In the context of logic service, a Rule is a piece of business logic used to mold the data received from various data sources and present them in a desired way to the logic service user. This frees the user of performing complex calculations on data and moves that functionality into a single container - the Rule.

Think, for example, of a situation where a user wants to calculate a person's age based on data retrieved from the PersonService. A typical solution would be to retrieve the birthdate and then use Java (or whatever language the user is accessing OpenMRS from) to calculate age. Now if another user also needs to calculate a person's age, but from a different point in code, the functionality would have to be re-implemented. Of course, this is just a simple example, but you can imagine how the amount of duplicated code (some of it probably buggy and/or outdated) would grow for some more complex demands.

This HOWTO presents a sample rule that calculates if a patient is HIV positive. Not all concepts used in this example are valid HIV indicators, but they are used for demonstration's sake. Hopefully this example should be enough for the interested reader to understand how Rules work and encourage him/her to start writing new Rules.

HIVPositiveRule

This Rule returns the date for which supporting data indicate the patient is HIV positive, otherwise nothing. This is how we will define the criteria:

  • cd4Count = first CD4 COUNT < 200 (not a valid diagnostic criteria, but useful for our example)
  • age = age in months calculated from patient's birthdate (again, using age in this to bring in demographic information)
  • result = first among firstHivDiagnosis, viralLoad, viralLoadQual, cd4Count iff age > 1 year

We want this business logic in a single place, so people can use it no matter what their access points into OpenMRS are. Thus, we create an HIVPositiveRule class and implement the Rule interface defined in org.openmrs.logic.rule.Rule:package org.openmrs.logic.rule;

public class HIVPositiveRule implements Rule {

/*
The central point of our implementation is the eval() method - it takes a patient and the criteria as arguments. Basically this is the place that will contain the business logic defined above. So let's see what it looks like, step-by-step: */

public Result eval(Patient patient, LogicCriteria criteria) {

Result allDiagnoses = Result.nullResult();
The allDiagnoses result is used for keeping all the results from individual data lookups below. It is initialized by using a static Result.nullResult() method, to indicate to the logic service it is actually just a placeholder for future results.
try
  { Boolean ageOK = Context.getLogicService().eval(patient, new LogicCriteria("AGE").gt(1)).toBoolean();
  if (!ageOK) return Result.nullResult();

/*
As defined earlier, our criteria for HIV positive can only be fulfilled if and only if the patient is older than 1. Thus, we use the Logic Service to do the thinking for us by supplying it this criteria as _new LogicCriteria("AGE").gt(1).toBoolean()_. Simple enough. What this call actually does is it invokes the AgeRule behind the scenes, which does the actual birthdate retrieval (via DemographicsRule) and age calculation. Although we could code our _age > 1_ condition here, it is best left to the Logic Service, since it transforms the request into a much faster database query. When we apply the _gt(1)_ criteria to our "AGE" token, the actual result (patient's age) is returned only if the patient's data matches the criteria. Otherwise, the result returned is a null result. When a non-null result is coerced into a Boolean, the resulting boolean value is _true_. Null results are coerced into _false_.
*/

// we find the first HIV diagnosis
allDiagnoses.add(Context.getLogicService().eval( patient, new LogicCriteria("PROBLEM ADDED").contains( "HUMAN IMMUNODEFICIENCY VIRUS").first()));
allDiagnoses.add(Context.getLogicService().eval( patient, new LogicCriteria("PROBLEM ADDED").contains("HIV INFECTED") .first()));
allDiagnoses.add(Context.getLogicService().eval( patient, new LogicCriteria("PROBLEM ADDED").contains( "ASYMPTOMATIC HIV INFECTION").first()));

/*
This is where we begin our actual data retrievals. (All these _eval(...)_ calls are actually calling ObservationRule's _eval()_ method behind the scenes.) Since the criteria we seek deals with coded results, we need to use the _contains()_ operator. The mechanism behind the _new LogicCriteria("PROBLEM ADDED").contains("HUMAN IMMUNODEFICIENCY VIRUS").first()_ call is:# The obs table is looked up for the given patient, using as filter the [PROBLEM ADDED|http://demo.openmrs.org/openmrs/dictionary/concept.htm?conceptId=6042] concept# The list of observations is then filtered to match the value_coded=884, which is the concept ID for [HUMAN IMMUNODEFICIENCY VIRUS|http://demo.openmrs.org/openmrs/dictionary/concept.htm?conceptId=884]# The result would then normally be returned to the user, listing all observations that match the criteria. But, since there is a _.first()_ method attached to the end of the criteria, only the earliest result (sorted by _date_created_) is returned. Again, if there are no observations that match the given criteria, a null result is returned. */

// first viral load
allDiagnoses.add(Context.getLogicService().eval(patient, new LogicCriteria("HIV VIRAL LOAD").first()));

// first qualitative viral load
allDiagnoses.add(Context.getLogicService().eval(patient, new LogicCriteria("HIV VIRAL LOAD, QUALITATIVE").first()));

// first CD4 COUNT < 200
allDiagnoses.add(Context.getLogicService().eval(patient, new LogicCriteria("CD4 COUNT").lt(200).first()));

Next, we fetch more observation results, this time with no need for the _contains()_ operator, since these are trivial lookups. The _first()_ method is used in the same fashion, returning the first result by date (if any).

return allDiagnoses.getFirstByDate();

}

catch (LogicException e) {
  return Result.nullResult();
}

}

Once we have all our results fetched and stored inside the allDiagnoses result, we use allDiagnoses.getFirstByDate() to fetch the first event leading to an HIV positive diagnosis, and return it. If there were none (or if there was an exception at some point), a null result is returned.

Registering a Rule with Logic Service

Now that we have a functioning HIVPositiveRule, we need to make it public, so other people can use it. A Rule needs to be registered with the Logic Service, and supply to it the tokens that it "understands" - in this case, the "HIV POSITIVE" token.

At the moment of writing of this document, this registering mechanism is still not perfected. Interested readers are invited to look at our RuleFactory and LogicServiceImpl implementations and track the changes. As soon as the mechanism is established, this section will be updated.

Usage

Once the rule is registered with the Logic Service, one can use standard Logic Service eval() calls to invoke a Rule and test its functionality. We can test the HIVPositiveRule by using:

PatientSet patients = ... // initialize a cohort
Map<Integer, Result> hivPositive = Context.getLogicService().eval(patient, "HIV POSITIVE");

After the call returns, the hivPositive map would be populated with results, grouped by each HIV positive patient. Patients that aren't HIV positive are not returned, and thus aren't present in the result map.

For each HIV positive patient, the Result object contained in the result map holds the actual first observation that gave an HIV positive status to the patient.