Thursday, November 17, 2011

SNOMED Expressions in Everest 1.0


Wow, it has been a long time since I have posted a new article on this blog and it has mainly been because of the crazy-busy schedule at work. But, I just thought I had to share this with everyone in the HL7 integration world looking at implementing Data Types R2 and R1 (or those migrating R1 to R2), and some of the benefits that Everest 1.0 will have when released (it is getting close).

First, I will provide a little bit of an overview. In HL7v3 clinical concepts within messages are represented using one of four different data types (two in the Data Types R2 specification). These are:


R1R2Summary
CSCSCoded Simple - A simple code where only the code mnemonic is unknown
CVCD.CVCoded Value – A more complex code structure whereby the code system from which the mnemonic is taken is unknown at design time.
CECD.CECode with Equivalents – A CV instance where translations (or equivalents) can optionally be specified.
CDCDConcept Descriptor – A code mnemonic taken from a code system, optionally with one or more concept roles which qualify the primary code. For example, the code LEFT qualifies FOOT to mean LEFT FOOT.
CRN/AConcept Role - A name/value pair where the value concept qualifies the semantic meaning of the primary mnemonic by way of the named concept.


Wait a minute! Notice some differences? Well, for starters CV and CE are no longer "proper" types according to the data types, they are flavors of CD. This is an appropriate change as they remain structurally identical to the R1 structures.

The big change comes in the concept descriptor. Notice how the CR data type is not present in data types R2. When I first saw this I thought nothing of it, however when looking at how each are represented on the wire the difference is very pronounced.

I'm a code kind of guy, so I thought I would explain this using code. First off, Everest uses a hybrid of DT R1 and R2, so the codified data types in Everest resemble those found in R1 (and are mapped to appropriate R2 flavors on formatting). With that in mind, let's represent the following example: "severe burn on the skin between the fourth and fifth toes on the left side", in Everest.

First, we create the primary code of "burn":

var burnCode = new CD<string>("284196006", "2.16.840.1.113883.6.96") {
DisplayName = "Burn of Skin",
CodeSystemName = "SNOMED-CT",
CodeSystemVersion = "2009"
};
Next, the we want to qualify "Burn of Skin" with a severity of "Severe". This is accomplished by creating a CR instance:

// Severity
var severityQualifier = new CR<string>(
   new CV<String>("246112005", "2.16.840.1.113883.6.96")
            { DisplayName = "Severity" },
   new CD<String>("24484000", "2.16.840.1.113883.6.96")

            { DisplayName = "Severe" }
);

Next, our code has a finding site. The burn was located on the skin between the fourth and fifth toes, so once again it is another CR instance:

// Finding Site
var findingSiteQualifier = new CR<String>(
    new CV<String>("363698007", "2.16.840.1.113883.6.96")

             { DisplayName = "Finding Site" },
    new CD<String>("113185004", "2.16.840.1.113883.6.96")

             { DisplayName = "Skin Between fourth and fifth toes" }
);

Next, we want to describe the fact that the the burn was found on the skin between the fourth and fifth toes "on the left hand side". Obviously we want to create another qualifier for this:

// Laterality
var lateralityQualifier = new CR<String>(
    new CV<String>("272741003", "2.16.840.1.113883.6.96")
            { DisplayName = "Laterality" },
    new CD<string>("7771000", "2.16.840.1.113883.6.96")
            { DisplayName = "Left Side" }
);

But how would we structure these concept roles to describe the situation? First we have to look at each term and ask the question, "What does this qualify". So, for example, does "Laterality of Left Side" qualify the burn? Technically no, the laterality qualifies the finding site (i.e.: We found the burn on the toes on the left hand side). So we want to add the lateralityQualifier to the findingSiteQualifier's value:

// Laterality applies to the finding site
findingSiteQualifier.Value.Qualifier = new LIST<CR<string>>() { lateralityQualifier };

What does finding site qualify? Technically finding doesn't qualify the severity it qualifies the primary code (i.e.: The burn was "found on" the skin…), and the same with applies to the severity (i.e.: We didn't find a severe skin between toes, we found a severe burn). So we add these two qualifiers to the primary code:

// Finding site and severity apply to primary code
burnCode.Qualifier = new LIST<CR<string>>() {
     severityQualifier,
    
findingSiteQualifier
};

Now comes the easy part, when we format the data type using data types R1 formatter:

var formatter = new MARC.Everest.Formatters.XML.ITS1.Formatter();
formatter.ValidateConformance = false;
formatter.GraphAides.Add(
     typeof(MARC.Everest.Formatters.XML.Datatypes.R1.DatatypeFormatter)
);

// Setup the writer
StreamWriter sw = new StreamWriter("C:\\temp\\temp.xml");
XmlWriter xw = XmlWriter.Create(sw, new XmlWriterSettings() { Indent = true });
XmlStateWriter xsw = new XmlStateWriter(xw);

// Format and produce the XML file
try
{
     xsw.WriteStartElement("code", "urn:hl7-org:v3");   xsw.WriteAttributeString("xmlns", "xsi", null, http://www.w3.org/2001/XMLSchema-instance);
   var p = formatter.Graph(xsw, burnCode);   sw.WriteEndElement();
}
finally
{    xw.Close();    sw.Flush();
    formatter.Dispose();
}

The output of this is the following XML:

<code code="284196006" codeSystem="2.16.840.1.113883.6.96" codeSystemName="SNOMED-CT" codeSystemVersion="2009" displayName="Burn of Skin">
   <qualifier inverted="false">
      <name code="246112005" codeSystem="2.16.840.1.113883.6.96"
            displayName="Severity" />     
      <value code="24484000" codeSystem="2.16.840.1.113883.6.96"
            displayName="Severe" />
   </qualifier>
   <qualifier inverted="false">
      <name code="363698007" codeSystem="2.16.840.1.113883.6.96"
            displayName="Finding Site" />
      <value code="113185004" codeSystem="2.16.840.1.113883.6.96"
             displayName="Skin Between fourth and fifth toes">
          <qualifier inverted="false">             
              <name code="272741003" codeSystem="2.16.840.1.113883.6.96"
                    displayName="Laterality" />
              <value code="7771000" codeSystem="2.16.840.1.113883.6.96"
                     displayName="Left Side" />
          </qualifier>     
      </value>
   
</qualifier>
</code>

But as I mentioned previously, CR is not supported in DT R2. So the question arises, "How do I qualify a code in HL7v3 DT R2?". Well, the answer is not so simple. In DT R2, the concepts for SNOMED terms are described using an expression language defined by IHTSDO. The SNOMED expression for our scenario is:

284196006|Burn of Skin|:{246112005|Severity|=24484000|Severe|,363698007|Finding Site|=(113185004|Skin Between fourth and fifth toes|:272741003|Laterality|=7771000|Left|)}

Intuitive right? Not really. So how do I represent this in a CD instance? Well, the answer is really ugly, and in my opinion violates first normal form (I will post an opinion post later about my thoughts of using 1NF in XML and how I think standards bodies seem to have forgotten it).

Anyways, so what is this supposed to look like in DT R2? The answer is below:

<code code="284196006:{246112005=24484000,363698007=(113185004:272741003=7771000)}"
      codeSystem="2.16.840.1.113883.6.96"
      codeSystemName="SNOMED-CT"
      codeSystemVersion="2009">
    <displayName value="Burn of Skin"/>
</code>

I warned you it wasn't pretty. So how do you get Everest to format a concept descriptor like this? Well, the answer is simple, change this line of code:

// Old Line: formatter.GraphAides.Add(typeof(MARC.Everest.Formatters.XML.Datatypes.R1.DatatypeFormatter));

formatter.GraphAides.Add(typeof(MARC.Everest.Formatters.XML.Datatypes.R2.DatatypeR2Formatter));

And Everest will automatically handle the generation of these expressions for SNOMED concepts. Parsing? It is the same. Everest 1.0's data types R2 formatter has been developed so that you are shielded from having to understand the complexities of SNOMED expressions.

As a matter of fact, when parsing a SNOMED concept with an SNOMED expression, Everest will construct the appropriate hierarchy of concept roles for you.

What do I think of this change in HL7? I think it was pointless, and simply over-complicates processing of XML instances. In my opinion, there is no need to mix the hierarchical language of SNOMED expressions as an attribute within the hierarcal container of XML. At least a framework like Everest has enough logic in the formatting of codes to shield the developer from changes like this.

Next time, I'm going to blog about a good change in R2, changes in the continuous set expression data types (SXPR).