Tuesday, November 8, 2011

HL7 HQMF Proof of Concept with hQuery

I've been tweeking the HL7 eMeasures DSTU to support Query Health this week and last to support a declarative, implementation independent, standards-based expression of queries.  There were a couple of things I needed to do to the e-Measure schema in order to make it workable for this effort:
  1. I added the RIM localVariableName attribute to the entries so I could have human readable names instead of just computer managed identifiers within the eMeasure.
  2. I fixed a bug in the Data types Release 1.1 schema released with the DSTU to support use of the expression element in specifying a criteria for an entry.  This makes for much simpler syntax in expressing queries on parameters.
  3. I did one other thing which I've forgotton right now.
The first step was to create a sample query expressed in the eMeasure format.  I used the NQF Diabetes: HbA1c Poor Control Quality Measure (measure 59) found on the Query and Data Model Analysis Page.  I was a little miffed about having to create the EMF from scratch, because it is clear that HTML comes from the eMeasure format, but I couldn't find the original XML for it.

Last week, Mark Hadley had posted the hQuery implementation for this measure, so I used hQuery as my first target implementation.

The JavaScript implementation declares a map() function which is used in the hQuery map/reduce implementation.  It does the following:
  1. Defines value sets
  2. Defines some query parameter variables for the start and end of the measure
  3. Defines matching criteria.
  4. Defines measure functions for the population, numerator, denominator, and exception critiera.
  5. Implements the map() function logic, which is fixed and very short (see below)
Here is the main logic of the map() function.  That never changes so I just emit it using a set of <xsl:text> statements.

  if (population(patient)) {
    emit("population", 1);
    if (denominator(patient)) {
      if (numerator(patient)) {
        emit("denominator", 1);
        emit("numerator", 1);
      } else if (exclusion(patient)) {
        emit("exclusion", 1);
      } else {
        emit("denominator", 1);
      }    
    }
  }


Defining Value Sets
So, my first step was to identify value sets and generate the code for them.  That was pretty easy.  I just searched for every occurrence of @valueSet in the EMF document, and then wrote a bit of XSLT to translate the SVS format XML document I created from the NQF Value Set spreadsheet into a set of JavaScript declarations.  Using SVS to access value sets is one of the suggestions I made last week.


This is the XSLT that does that:
 <xsl:variable name='codeSets' select="document('svs.xml')"/>
 <xsl:for-each select="$codeSets//ValueSet[@id=$valueSets]">
   <xsl:text>    "</xsl:text><xsl:value-of select='@id'/>
   <xsl:text>": {&#xA;</xsl:text> 
   <xsl:variable name="this" select="."/>
   <xsl:for-each select="set:distinct(ConceptList/Concept/@codeSystemName)">
     <xsl:variable name="cs" select="."/>
     <xsl:text>      "</xsl:text><xsl:value-of select='.'/>
     <xsl:text>": [ </xsl:text>
     <xsl:for-each select='$this/ConceptList/Concept[@codeSystemName=$cs]'>
       <xsl:text>"</xsl:text>
       <xsl:value-of select='@code'/>
       <xsl:text>", </xsl:text>
     </xsl:for-each>
     <xsl:text> ], &#xA;</xsl:text>
    </xsl:for-each>
    <xsl:text>      },&#xA;</xsl:text>
  </xsl:for-each>
  <xsl:text>    };&#xA;</xsl:text> 




That gave me text looking like this (with a lot of stuff removed just to show what I did):

  var terms = {
    "2.16.840.1.113883.3.464.1.72": {
      "CPT": [ "83036", "83037",  ], 
      "LOINC": [ "17856-6", "4548-4", "4549-2",  ], 
      "SNOMED-CT": [ "117346004", "165680008", "259689004", "259690008", "313835008", "33601001", "40402000", "408254005", "43396009",  ], 
    },
    "2.16.840.1.113883.3.464.1.98": {
      "ICD-10-CM": [ "E28.2",  ], 
      "ICD-9-CM": [ "256.4",  ], 
      "SNOMED-CT": [ "69878008",  ], 
    },
  };


Define Query Parameter Variables
The next step was to create variables for some of the measure parameters.  I declared the parameters in the EMF file as follows:



  <entry typeCode="COMP">
    <localVariableName>StartDate</localVariableName>
    <observation classCode="OBS" moodCode="EVN">
      <code code="52832-3" codeSystem="2.16.840.1.113883.6.1"/>
      <value xsi:type="TS" value="20100101"/>
    </observation>
  </entry>
  <entry typeCode="COMP">
    <localVariableName>EndDate</localVariableName>
    <observation classCode="OBS" moodCode="EVN">
      <code code="52833-1" codeSystem="2.16.840.1.113883.6.1"/>
      <value xsi:type="TS" value="20101231"/>
    </observation>
  </entry>

These were translated to variable declarations using this bit of XSLT:

  <xsl:for-each 
    select='$DataDeclarationSection//emf:entry[emf:localVariableName != ""]/
            emf:observation[@moodCode="EVN" and not(@isCriterionInd="true")]'>
    <xsl:text>  var </xsl:text><xsl:value-of select="../emf:localVariableName"/>
    <xsl:text> = new </xsl:text>
    <xsl:value-of select="emf:value/@xsi:type"/><xsl:text>(</xsl:text>
    <xsl:choose>
      <xsl:when test="emf:value/@xsi:type='CD'">
        <xsl:text>"</xsl:text>
        <xsl:value-of select='emf:value/@code'/><xsl:text>","</xsl:text>
        <xsl:value-of select='@codeSystem'/><xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:when test="emf:value/@xsi:type='TS'">
        <xsl:text>"</xsl:text>
        <xsl:value-of select='emf:value/@value'/><xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:when test="emf:value/@xsi:type='PQ'">
        <xsl:text>"</xsl:text>
        <xsl:value-of select='emf:value/@value'/><xsl:text>","</xsl:text>
        <xsl:value-of select='emf:value/@unit'/><xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:when test="emf:value/@xsi:type='ST'">
        <xsl:text>"</xsl:text><xsl:value-of select='emf:value'/>
        <xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:text>"</xsl:text><xsl:value-of select='emf:value/@value'/>
        <xsl:text>"</xsl:text>
      </xsl:otherwise>
    </xsl:choose>
  <xsl:text>);&#xA;</xsl:text>
</xsl:for-each>

That looked like this when it was done:

// Generate named variables
  var StartDate = new TS("20100101");
  var EndDate = new TS("20101231");

Note how I used constructors to create HL7 data types.

Defining Matching Criteria
Next was the data criteria.  Here are a couple of examples of those in the EMF document:

  <entry typeCode="COMP">
    <localVariableName>ageBetween17and64</localVariableName>
    <observation classCode="OBS" moodCode="EVN" isCriterionInd="true">
      <id root="0" extension="ageBetween17and64"/>
      <code code="424144002" codeSystem="2.16.840.1.113883.6.96"
        displayName="Age"/>
      <value xsi:type="IVL_PQ">
        <low value="17" unit="a"/>
        <high value="64" unit="a"/>
      </value>
      <sourceOf typeCode="INST">
        <observation classCode="OBS" moodCode="DEF">
          <id root="0" extension="Demographics"/>
        </observation>
      </sourceOf>
    </observation>
    </entry>
    <entry typeCode="COMP">
      <localVariableName>EDorInpatientEncounter</localVariableName>
      <encounter classCode="ENC" moodCode="EVN" isCriterionInd="true">
        <id root="0" extension="EDorInpatientEncounter"/>
          <code valueSet="2.16.840.1.113883.3.464.1.42"/>
          <effectiveTime>
            <high nullFlavor="DER">
              <expression>EndDate.add(new PQ(-2,"a"))</expression>
            </high>
          </effectiveTime>
          <sourceOf typeCode="INST">
            <observation classCode="OBS" moodCode="DEF">
              <id root="0" extension="Encounter"/>
            </observation>
          </sourceOf>
       </encounter>
     </entry>

There are a couple of things to note here:
  1. Note that I'm using the localVariableName here to give a name to the criteria.  This is useful later.
  2. The second criterion is parameterized using the variables defined previously, and the <expression> element which I added to the DataTypes schema.  I cheated here and used a JavaScript syntax for this example, but I will eventually need an expression syntax that is implementation independent.
  3. Finally, note how each criterion references a model definition as I described a couple of days ago.  This is how I enforce expression of criteria to the S&I Framework CIM Model.
Translating this to JavaScript variables was pretty easy.  Eventually, these should be translated to functions that delay evaluation until they are needed and cache results if they are evaluated again, but that is an optimization that can be done later.  Here is the XSLT:

  <xsl:text>// Create variables for each data criteria&#xA;</xsl:text>
  <xsl:for-each 
    select='$DataDeclarationSection//emf:entry/emf:*[@isCriterionInd="true"]'>
    <xsl:text>&#xA;  var </xsl:text>
    <xsl:value-of select="../emf:localVariableName"/>
    <!-- Generate a name if one isn't given -->
    <xsl:if test="not(../emf:localVariableName)">
      <xsl:value-of select="generate-id(.)"/>
    </xsl:if>
    <xsl:text>= patient.</xsl:text>
    <xsl:value-of select="emf:sourceOf[@typeCode='INST']/*/emf:id/@extension"/>
    <xsl:text>()</xsl:text>
    <xsl:call-template name="criteria">
      <xsl:with-param name="act" select="."/>
    </xsl:call-template>
  </xsl:for-each>

The criteria template (not shown), generates the .match() expression based on code, effectiveTime and value elements expressed in the criterion.  So now I wind up with:
// Create variables for each data criteria
  var ageBetween17and64 = patient.Demographics().match(new CD("424144002","2.16.840.1.113883.6.96), new IVL(new PQ("17","a"),new PQ("64","a")), null);
  var EDorInpatientEncounter = patient.Encounter().match(term["2.16.840.1.113883.3.464.1.42"], new IVL(null, EndDate.add(new PQ(-2,"a"))),null);

Defining Measure Functions
The final step is to generate the population, numerator, denominator and exclusion functions defined using these variables.  This is essentially the same transformation applied to each of the different population groups.  This is split into three components, a declaration template, and an expression processing template that walks the precondition tree, and a template that generates the variable name associated with each precondition.  The declaration template appears below.

<xsl:template name="declareFunction">
  <xsl:param name="name"/>
  <xsl:param name="entries"/>
  <xsl:text>&#xA;  var </xsl:text>
  <xsl:value-of select="$name"/>
  <xsl:text> = function(patient) {&#xA;</xsl:text>
  <xsl:text>    return </xsl:text>
  <xsl:call-template name="processEntries">
      <xsl:with-param name="entries" select="$entries"/>
  </xsl:call-template>
  <xsl:text>;&#xA;  }&#xA;</xsl:text>
</xsl:template>

It's pretty simple, relying on the processEntries template to do most of the heavy lifting.  That appears here:

<xsl:template name="processEntries">
  <xsl:param name="entries"/>
  <xsl:if test="$entries[emf:conjunctionCode/@code='AND' or
                not(emf:conjunctionCode)]">
    <xsl:text>allOf(</xsl:text>
    <xsl:apply-templates 
      select="$entries[emf:conjunctionCode/@code='AND' or
              not(emf:conjunctionCode)]"/>
    <xsl:text>true)</xsl:text>
    <xsl:if test="$entries[emf:conjunctionCode/@code != 'AND']">
      <xsl:text> &amp;&amp; </xsl:text>
    </xsl:if>
  </xsl:if>
  <xsl:if test="$entries/emf:conjunctionCode/@code=&quot;OR&quot;">
    <xsl:text>atLeastOne(</xsl:text>
      <xsl:apply-templates select="$entries[emf:conjunctionCode/@code='OR']"/>
      <xsl:text>false) </xsl:text>
      <xsl:if test="$entries[emf:conjunctionCode/@code = 'XOR']">
        <xsl:text> &amp;&amp; </xsl:text>
      </xsl:if>
  </xsl:if>
  <xsl:if test="$entries/emf:conjunctionCode/@code=&quot;XOR&quot;">
    <xsl:text>onlyOne(</xsl:text>
    <xsl:apply-templates select="$entries[emf:conjunctionCode/@code='OR']"/>
    <xsl:text>false)</xsl:text>
  </xsl:if>
</xsl:template>

All it really does is gather the preconditions into the AND/OR/XOR groups, and generates a call to a function that evaluates to true if the group matches the criteria based on the conjunctionCode.  The criteria are gathered below in this template, which is used recursively 

<xsl:template match="emf:sourceOf[@typeCode='PRCN']">
  <!-- 
    Find the act that was declared in Data Criteria
  -->
  <xsl:variable name="act"
    select="emf:act|emf:observation|emf:encounter|emf:procedure|
           emf:substanceAdministration|emf:supply"/>
  <xsl:variable name="declaration"
    select="$DataDeclarationSection/emf:entry/*[
      $act/emf:id/@root = emf:id/@root and 
      $act/emf:id/@extension = emf:id/@extension]"/>
  <!-- 
    If there was a declaration in the data section, then this is
    not a grouper (which should be represented using organizer?). 
  -->
  <xsl:if test="$declaration">
    <xsl:value-of select="$declaration/../emf:localVariableName"/>
    <!-- Generate a name if one isn't given -->
    <xsl:if test="not($declaration/../emf:localVariableName)">
      <xsl:value-of select="generate-id($declaration)"/>
    </xsl:if>
    <!-- deal with negationInd on sourceOf and actionNegationInd on criteria -->
    <xsl:choose>
      <xsl:when test="$act/../@negationInd = 'true' and 
         $act/@actionNegationInd = 'true'">.length != 0, </xsl:when>
      <xsl:when test="$act/@actionNegationInd = 'true'">.length == 0,</xsl:when>
      <xsl:when test="$act/../@negationInd = 'true'">.length == 0, </xsl:when>
      <xsl:otherwise>.length != 0, </xsl:otherwise>
    </xsl:choose>
  </xsl:if>
  <xsl:if test="$act/emf:sourceOf[@typeCode=&quot;PRCN&quot;]">
    <xsl:call-template name="processEntries">
      <xsl:with-param name="entries"
        select="$act/emf:sourceOf[@typeCode='PRCN']"/>
    </xsl:call-template>
    <xsl:text>, </xsl:text>
  </xsl:if>
</xsl:template>

This just dumps out variable names (in the first part), and deals with parenthetical (nested) sub-expressions in the precondition.

The whole solution, including the sample (partially completed) EMF, the conversion XSLT, and the vocabulary XML document can be found here in one ZIP file.  It's working code.  I think my next proof of concept will look at generating query results using collections of CCD documents for a given patient using XPath with the same EMF as a declarative description.

   Keith

P.S.  One thing I have noted in building  my implementation is that the HTML describing the measure, and the measure as defined by NQF appear to be slightly different.  That is not a Query Health issue, but certainly is something that needs to be addressed.

TBD for the JavaScript Implementation:  Declare constructor and object structures for key HL7 datatypes TS, PQ, IVL, and CD.  Declare logical conjunction functions for allOf(), atLeastOne(), onlyOne().

TBD for EMF: Find an implementation independent, easily parsed (XML) language to write expressions in. Write an implementation guide describing the rules I used to generate the EMF.



4 comments:

  1. Your expression rendering isn't valid. I'll have to sort out the schema for EXPR_TS. As for using javascript, what kind of platform independence do you want? But javascript doesn't avoid you doing the heavy lifting of defining the language binding, just like you have to do with OCL. I think this might be worth doing though - javascript is much better than OCL, and not too hard to adapt existing js lirary implementations to the task.

    ReplyDelete
  2. I'd prefer you tell me how to fix the expression rendering, then just say it isn't valid. As far as I can tell, it is handled the way that ISO Datatypes did it.

    ReplyDelete
  3. As I said, I'll have to sort it out, and then I'll post another comment here. It's close, but even under ISO data types what you have isn't valid - you need a mediaType.

    ReplyDelete
  4. Ah, I had assumed (incorrectly apparently) that it could be fixed to a particular value. Even so, that's not an insurmountable error, it just requires the insertion of an attribute.

    ReplyDelete