Tuesday, November 15, 2011

Implementing QueryHealth on a Collection of HL7 CCD Documents using XQuery

As promised, my next implementation of Query Health using the HL7 EQMF standard is over a collect of CCD documents.  This implementation isn't quite as cooked as the hQuery implementation, primarily because it doesn't include a "working infrastructure".  Instead, it relies on a conceptual infrastructure that assumes that you have a collection of CCD documents, and that you want to product results from those.  I haven't yet generated a collection of CCD documents against which this work has been tested, so what you see in this post might not quite work (yet).

As I mentioned last night, I discovered last week that my favorite XML Editor has an XQuery debugger built in.  Since XQuery is designed to support querying of collections of XML Documents, I thought it would be an excellent implementation platform for querying a collection of CDA document (which are of course in XML).

I reused the basic structure of the hQuery implementation XSLT, because I figured a lot of the features would be the same:  declarations for parameters, predicates, and query functions (numerator, denominator, et cetera).

The very first part of my XSLT prepares an XML element that collects up all the value sets used in the EQMF content:


        <result>
            <xsl:variable name="valueSets" select="set:distinct(//*/@valueSet)"/>
            <collection xml:id="terms">
                <xsl:for-each select="$valueSets">
                    <doc href="https://example.com/RetrieveValueSet?id={.}"/>
                </xsl:for-each>    
            </collection>

As you can see, it just iterates over the distinct value sets appearing in any valueSet attribute, and produces the following chunk of XML:
   <collection xml:id="terms">
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.42"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.1142"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.37"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.67"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.98"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.113"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.72"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.94"/>
   </collection>

This is just a set of references (links) to value sets being served up using the IHE SVS (pdf) profile as I mentioned in previous posts.  Later, in the XQuery code, I reference this collection using the following variable declaration:
let $terms := fn:collection("#terms")

Since #terms is a fragment identifier in a local URI, it means: the XML element with the ID of terms.  The XML generated above uses xml:id to add that identifier schemalessly to the XML content.  I don't know if my XQuery processor supports that sort of referencing, but other XML processors do.  I used this trick with Self Displaying CDA previously to point to a CSS stylesheet inside a CDA document. 

The next chunk of XSLT  that I wrote created variables for the query parameters:
    <xsl:for-each
      select="$DataDeclarationSection//emf:entry[
        emf:localVariableName != '']/emf:observation[@moodCode='EVN' 
        and not(@isCriterionInd='true')]">
      <xsl:text>
        let $</xsl:text><xsl:value-of select="../emf:localVariableName"/>
        <xsl:text>:=</xsl:text>            
        <xsl:copy-of select="emf:value"/>            
    </xsl:for-each>
 
All this did was declare local variables defined in the EQMF document in the XQuery code, using the XML that was already present in the EQMF document.  This is a result of xml inside an XQuery expression being essentially a "constant" expression having the value of that XML.  Here is what the output looked like:

  let $StartDate:=<value xmlns="urn:hl7-org:v3" xsi:type="TS" value="20100101"/>
  let $EndDate:=<value xmlns="urn:hl7-org:v3" xsi:type="TS" value="20101231"/>

Then I had to deal with data element declarations:
    <xsl:for-each 
      select="$DataDeclarationSection//emf:entry/emf:*[@isCriterionInd='true']">
      <xsl:text>
    declare function local:</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>($ids, $ccds) {
      for $d in local:</xsl:text>
      <xsl:value-of 
        select="emf:sourceOf[@typeCode='INST']/*/emf:id/@extension"/>
      <xsl:text>($ids, $ccd)
                where local:matches($d, </xsl:text>
      <xsl:copy-of select="."/>
      <xsl:text>)
                return $e//cda:recordTarget/cda:id
    }
      </xsl:text>
    </xsl:for-each>

What this does is loop through each data declaration and create a function of the form:
declare function local:functionName($ids, $ccds) {
    for $e in local:modelElementName($ids, $ccds)
    where local:matches($e, DateElementCriterion)
    return $e//cda:recordTarget/cda:id
}

The functionName is supplied by the localVariableName for the criterion.  The modelElementName comes from the identifiers I used in the definitions for the criterion model elements. And the DataElementCritierion comes from the XML for the criteria.  A completed one looks like something this:

    declare function local:ageBetween17and64($ids, $ccds) {
      for $d in local:Demographics($ids, $ccd)
      where local:matches($e, 
  <observation xmlns="urn:hl7-org:v3" 
    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>
   </observation>)
      return $e//cda:recordTarget/cda:id
    }
 
The critical component here is the "matches" function, which I haven't yet defined.  That function would look at the components of the input element and compare them to the criteria in the criterion element, and return true if they matched.  It's a complex function, but mostly filled with if then else logic to check matches on dates, codes and values.

The next template declares functions for the population, numerator, denominator, and exclusion functions:
 
    <xsl:template name="declareFunction">
        <xsl:param name="name"/>
        <xsl:param name="entries"/>
        <xsl:text>
    declare function local:</xsl:text><xsl:value-of select="$name"/>
        <xsl:text>($ccds, $ids) {
        return 
        </xsl:text>
        <xsl:call-template name="processEntries">
            <xsl:with-param name="entries" select="$entries"/>
        </xsl:call-template>
        <xsl:text>
    }
        </xsl:text>
    </xsl:template>

The general structure of this function is:
  declare function local:population($ccds, $ids) {
    return logicalExpression 
  }
    
The logical expression is computed by the process entries template by walking the precondition tree in the EQMF.

    <xsl:template name="processEntries">
      <xsl:param name="entries"/>
      <xsl:if test="$entries[
                emf:conjunctionCode/@code='AND' or not(emf:conjunctionCode)]">
        <xsl:text>(</xsl:text>
        <xsl:call-template name="and">
          <xsl:with-param name='args' select="$entries[emf:conjunctionCode/@code='AND' or not(emf:conjunctionCode)]"/>
        </xsl:call-template>
        <xsl:text>)</xsl:text>
        <xsl:if test="$entries[emf:conjunctionCode/@code != 'AND']">
          <xsl:text> and </xsl:text>
        </xsl:if>
      </xsl:if>
          ... similar code for OR and XOR ...
    </xsl:template>

It groups the preconditions by their conjunctionCode (AND, OR and XOR), calls a template that generates the appropriate logical expression, and inserts, if necessary, the an "and" between each of the conjunctionCode groups.  The "and" template shown builds the logical expression from the list of preconditions.  It dumps out the first expression, and if there are any remaining, inserts the "and" operator, and dumps out the remainder using a classical XSLT tail recursive function.

    <xsl:template name="and">
      <xsl:param name="args"/>
      <xsl:variable name="first" select="$args[1]"/>
      <xsl:variable name="rest" select="$args[position() != 1]"/>
      <xsl:apply-templates select="$first"/>
      <xsl:if test='$rest'>
        <xsl:text> and </xsl:text>
          <xsl:call-template name="and">
            <xsl:with-param name="args" select="$rest"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:template>

The "precondition" processing template, is based on the same code used for the hQuery implementation, with just a small bit of reconstruction to use XQuery expressions instead of JavaScript.
    
    <xsl:template match="emf:sourceOf[@typeCode='PRCN']">
      <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]"/>
      <xsl:if test="$declaration">
        <xsl:choose>
          <xsl:when test="$act/../@negationInd = 'true' and
                          $act/@actionNegationInd = 'true'"></xsl:when>
          <xsl:when test="$act/@actionNegationInd = 'true'">not(</xsl:when>
          <xsl:when test="$act/../@negationInd = 'true'">not(</xsl:when>
          <xsl:otherwise></xsl:otherwise>
        </xsl:choose>
        <xsl:text>local:</xsl:text>
        <xsl:value-of select="$declaration/../emf:localVariableName"/>
        <xsl:if test="not($declaration/../emf:localVariableName)">
          <xsl:value-of select="generate-id($declaration)"/>
        </xsl:if>
        <xsl:text>($ccds, $ids)</xsl:text>
        <xsl:choose>
          <xsl:when test="$act/../@negationInd = 'true' and
                          $act/@actionNegationInd = 'true'"></xsl:when>
          <xsl:when test="$act/@actionNegationInd = 'true'">)</xsl:when>
          <xsl:when test="$act/../@negationInd = 'true'">)</xsl:when>
          <xsl:otherwise></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:if>
    </xsl:template>

The output of this code looks something like this in the XQuery result (without all the pretty indenting yet...):

    declare function local:population($ccds, $ids) {
        return local:ageBetween17and64($ccds, $ids)
    }
        
    declare function local:numerator($ccds, $ids) {
        return local:HbA1C($ccds, $ids)
    }
        
    declare function local:denominator($ccds, $ids) {
        return 
          ( 
            local:HasDiabetes($ccds, $ids)
          )
            and 
          ( 
            local:EDorInpatientEncounter($ccds, $ids) or
            local:AmbulatoryEncounter($ccds, $ids)) or
            local:DiabetesMedAdministered($ccds, $ids) or
            local:DiabetesMedIntended($ccds, $ids) or 
            local:DiabetesMedSupplied($ccds, $ids) or 
            local:DiabetesMedOrdered($ccds, $ids)
          )
    }
        
    declare function local:exclusion($ccds, $ids) {
        return 
        
          ( 
            local:HasPolycysticOvaries($ccds, $ids) and 
            not(local:HasDiabetes($ccds, $ids))
          ) or 
          local:HasSteroidInducedDiabetes($ccds, $ids) or 
          local:HasGestationalDiabetes($ccds, $ids)
        )
    }

The last piece of this is how the result is computed.  Here it is:
    let $ccds := fn:collection("file:///.?select=*.xml")
    let $ids := 
      $ccds/cda:ClinicalDocument/cda:recordTarget/cda:id[
        preceding::cda:id/@root != current()/@root and
        string(preceding::cda:id/@extension) != string(current()/@extension)
    ]
    <population>
    { local:population($ccds, $ids) }
    </population>
    <numerator>
    { local:numerator($ccds, $ids) }
    </numerator>
    <dednominator>
    { local:denominator($ccds, $ids) }
    </dednominator>
    <exclusion>
    { local:exclusion($ccds, $ids) }
    </exclusion>

This simply returns the list of <id> elements for which there are a match in the different categories.  From this, you can compute the count of patients that meet the criteria.  In a real world implementation, you wouldn't actually return the patient identifiers, you'd just return the counts.

The first two variables are what is processed.  I've show initializing the collection of the ccd documents using the XPath 2.0 collection function.  The second variable collects up the unique ids of all patients.  It is patients, and not ccds that we are counting.

So, there you have a description of how my XQuery implementation is created from the HL7 EQMF declaration.  More code is actually needed to make this one work.  And when I have some tested and working coded, I'll post a link to it just like I did for the hQuery implementation.

Next up is a SQL Table implementation.  For that, I'll start tomorrow with a post on the Clinical Data modeling needed in the database.  It turns out, as I said yesterday, that the models needed for all three implementations are very simple.

1 comment:

  1. The issue that clinical information concerns like display Name and code systemName attribute in code elements should be rectified.


    Sample Proposals

    ReplyDelete