Convert your FHIR JSON -> XML and back here. The CDA Book is sometimes listed for Kindle here and it is also SHIPPING from Amazon! See here for Errata.

Monday, December 5, 2016

Why roll your own?

Why should a developer use their own code when there's a ton of high quality open source already out there?

I hear a lot of different reasons:

  1. It's too hard to learn someone else's stuff.
  2. How do I know it is good?
  3. How will we maintain it?  If there are bugs, I have to be able to fix them.
  4. It doesn't fit our design.
All of these are good reasons IF in fact, they are good reasons.  Too few do the work to figure this out.
  1. It is easier to write and understand your own code base than it is to understand what someone else did.  But most people don't even try.  Look folks, this is why they pay you the big bucks ... to understand complex stuff.
  2. You have to look at it.  And I mean really look.  There are a lot of ways to find good stuff.  Is anyone else using it? That's a good sign.  Is it frequently updated? Another good one.  Does it have a vibrant community around it? Another good one.  If the last update was two years ago, you may be SOOL (it's an acronym that basically means out of luck).
  3. That's one of the cool things about maintenance if you find good Open Source because a vibrant open source community will both accept and fix bugs.  And if you are serious (and I mean truly serious) about #1, and choose that source base, you'll also become a contributor back to it.  So not only can you adopt the fixes of others, but you can also make your own fixes.
  4. This can often be a problem.  How flexible is your stack?  How many more dependencies can you take?  If you have a good stack, you are much more likely to find open source that fits onto it without additional dependencies.  You may be challenged around upgrades stack components though, and that's where contributing back becomes important.
What's the value here for adopting open source?
  • Time To Market -- Adopting somebody else's code can let you focus on other things that are necessary to bring your product to market.
  • Maintenance -- It isn't just new development that you could be saving, it might also be on maintenance.  A good open source project will also maintain the solution for you.  And that has tremendous value.  Software costs are often quoted as 20% development, 80% maintenance.
  • Reputation -- Using good open source can enhance the reputation of your own products (if the open source base has a good reputation).  Contributing back can also be a significant reputation enhancer for your organization.
How does this fit in with standards?  A lot of open source projects integrate using standards. Some even BECOME standards (e.g., Schematron).  That's one of the values of standards.

Tomorrow, I'll talk about some cool new open source projects for CDA.

   -- Keith




Wednesday, November 30, 2016

Implementing Partial CDA Validation

In Partial Rejection and Levels of Validity in CDA (or anything else for that matter) I discussed levels of validation of CDA content.  Now I have to make that real.  There are two different ways to go about it.  As you might recall, here are a few of the levels in the partial validation hierarchy:

Level 0: Totally bogus content.  Is this even XML?
Level 1: The CDA Header is valid.
Level 2a: Level 1 + the narrative content is valid according to the CDA Schema
Level 2b: Level 2 + the LOINC codes for documents and sections are recognized as valid.

The first level is just doing an XML Parse without validation.  This will ensure content is well-formed XML.  If you fail this test, no need to go further.

The next level validates everything up through nonXMLBody or structuredBody.  This is easy.  Craft a new CDA Schema by editing POCD_MT000040.xsd as follows (delete struck out material and insert underlined material):

  <xs:complexType name="POCD_MT000040.ClinicalDocument">
    <xs:sequence>
      <xs:element name="realmCode" type="CS" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="typeId" type="POCD_MT000040.InfrastructureRoot.typeId"/>
      <xs:element name="templateId" type="II" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="id" type="II"/>
      <xs:element name="code" type="CE"/>
      <xs:element name="title" type="ST" minOccurs="0"/>
      <xs:element name="effectiveTime" type="TS"/>
      <xs:element name="confidentialityCode" type="CE"/>
      <xs:element name="languageCode" type="CS" minOccurs="0"/>
      <xs:element name="setId" type="II" minOccurs="0"/>
      <xs:element name="versionNumber" type="INT" minOccurs="0"/>
      <xs:element name="copyTime" type="TS" minOccurs="0"/>
      <xs:element name="recordTarget" type="POCD_MT000040.RecordTarget" maxOccurs="unbounded"/>
      <xs:element name="author" type="POCD_MT000040.Author" maxOccurs="unbounded"/>
      <xs:element name="dataEnterer" type="POCD_MT000040.DataEnterer" minOccurs="0"/>
      <xs:element name="informant" type="POCD_MT000040.Informant12" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="custodian" type="POCD_MT000040.Custodian"/>
      <xs:element name="informationRecipient" type="POCD_MT000040.InformationRecipient" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="legalAuthenticator" type="POCD_MT000040.LegalAuthenticator" minOccurs="0"/>
      <xs:element name="authenticator" type="POCD_MT000040.Authenticator" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="participant" type="POCD_MT000040.Participant1" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="inFulfillmentOf" type="POCD_MT000040.InFulfillmentOf" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="documentationOf" type="POCD_MT000040.DocumentationOf" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="relatedDocument" type="POCD_MT000040.RelatedDocument" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="authorization" type="POCD_MT000040.Authorization" minOccurs="0" maxOccurs="unbounded"/>
      <xs:element name="componentOf" type="POCD_MT000040.Component1" minOccurs="0"/>
      <xs:element name="component" type="POCD_MT000040.Component2"/>
      <xs:any processContents/>
    </xs:sequence>
    <xs:attribute name="nullFlavor" type="NullFlavor" use="optional"/>
    <xs:attribute name="classCode" type="ActClinicalDocument" use="optional" fixed="DOCCLIN"/>
    <xs:attribute name="moodCode" type="ActMood" use="optional" fixed="EVN"/>
  </xs:complexType>

This will result in the schema processor ignoring anything after the CDA Header.  Or will it? Actually, this will fail, as the schema now violates the Unique Particle Attribution constraint of XML Schema 1.0.  However, if you could be sure that componentOf would be present, setting minOccurs="1" on that declaration resolves the problem.  But not every CCDA requires that, and so that little fix won't work.  OK, what if we change the definition of that last component so that it can contain anything?  Yep, that works.

It should look something like this:
  <xs:complexType name="POCD_MT000040.Component2">
    <xs:sequence>
      <xs:any processContents="skip" />
    </xs:sequence>
    <xs:anyAttribute processContents="skip"/>
  </xs:complexType>

So, now <component> can contain any sort of well formed XML content, and your "header validator" won't care.

An alternative implementation would use a specialized XSL identity template with some exceptions to skip any unrecognized content after componentOf, and simply delete the component element definition in POCD_MT000040.ClinicalDocument.

The next challenge is validating narrative only content.  For that, you want to tweak section definitions within the document so that you don't care about validating any content that isn't <text>, <title> or perhaps <code> within <section>, and that you validate subsection content.

That's a bit trickier.  For this case, you could define a <component> element at the top level which would be overriden by specializations of <component> defined within the header or entries (which you really don't care about), but which would be processed when matched by <xs:any processContents='lax'>.  However, rather than do that, my recommendation would be to create a specialized identity template that copies only what you want to validate within sections, and skips anything you don't care to validate.  Then you can just use the standard CDA Schema to validate the content without any changes (because all content within a section is optional according to the schema).

In that way, what you've just done is eliminated the potentially invalid content.  There's extra value there, because now what you have is a transform of the original content which, if "narrative valid", is probably safe to keep around for viewing and transformation by a stylesheet.

That identity template is a simple exercise in software engineering.  I'll leave it to the interested reader to figure it out.

Oh, one final thing: Don't be dumb and validate in easy to hard order.  Validate in the other order, because it will cost less in processing time for good documents.  Let the bad ones pay the performance penalty for multiple validation stages.

   -- Keith



Monday, November 14, 2016

Good Interoperability works like a shotgun, but with a single bullet

As an implementer these days, I don't have the luxury of building one-off solutions.  I have to be able to take components and put them together in multiple ways to solve multiple problems.  CDA was the way we used to do this in IHE Patient Care Coordination in the past, where a single section or entry could be used in multiple documents to support multiple use cases.  In fact, if I look at the number of IHE profiles that use the problems, medications and allergies sections we first created, I get at least a dozen+ CDA documents which use those.  They became the foundation of our work for many years.

The same is becoming even more true now with HL7 FHIR.  Each FHIR resource can be used for multiple use cases, and the resources can be put together in multiple ways to achieve a solution.  If I want to build a flowsheet, I can use the Observation resource.  A graph? The observation resource.  A CDS Intervenion? I might want to access the Observation resource.  And it's the same resource (though perhaps with slightly different constraints for different uses).

No longer do I have to concern myself about different models, schema, et cetera, just because how I want to use the thing has changed.

So often, we have limited resources.  We want a shotgun, but all we get is a sling with a single stone. We get one shot at this.  With FHIR, I can line all my ducks up in a row and smack them down with that single stone.  It's not just two birds (use cases), but as many as I can line up.  And in fact, I don't even have to line them up all that much.  Perhaps what I have in FHIR is a flamethrower.

   Keith

Wednesday, November 9, 2016

Accidental Interoperability

I've been spending a great deal of time on the implementation side, which doesn't let me say much here.  However, I had a recent experience where I saw a feature one team was demonstrating in which I could actually integrate one of my interop components to supply additional functionality.

Quite simply, when you build interop components right, this sort of accidental interop shows up all the time.  It's really nice when it does too, because you can create a lot of value through it with very little engineering investment.

Lego could have spent less time on their very simple building block, but because of the attention they spent, there are SO many more ways to connect those blocks, some of them I am certain were never originally intended.

Getting into that component based mind-set can that enables accidental interop is sometimes quite challenging.  All too often we focus on very specific problems and fail to consider how what we are building can be done in a more general way.  When we get that focused, it often enables us to deliver sooner, because we are focused on the singular use case, and can take short cuts to optimize for our use case.  At the same time, that hyper-focus prevents us from looking at slightly more general solutions that might have broader use.

All too often I've been told those more generalized solutions are "Scope expansions", because they don't fit the use case, and the benefits of generalization aren't immediately experienced for the specific use can I'm asked to solve for. Yet my own experience tells me that the value I get out of more general solutions is well worth the additional engineering attention.  It may not help THIS use case, but when I can apply the same solution to the next use case that comes along, then I've got a clear win.  Remember Avian flu?  That threat turned out to be a bust yet CDC spent a good bit of money on a solution for that use case.  Could they use any of it for Swine flu?  Yeah, you really don't want to know the answer to that.




Thursday, November 3, 2016

Patient matching and restricted charts

Patient matching is a tricky area.  Name, birth date and gender are insufficient in a region to match a patient sufficiently for all patients.  For example, one Zip code in Chicago contains enough John Smith's that the likelyhood of an identity collision occuring within a practice in thta region is statistically significant, about 1 in 20 chance of occuring for some patient.  And John Smith is only the thirteenth most popular name in that region.

So you need other identifiers or differentiators to get a better match.

Some organizations have business rules about matching that only allows them to expose patient data to other providers if they get one and only one match.  Thye also have business rules about not displaying any data for patients whose charts are restricted outside their practice. 

Combine these two issues and you have a tricky challenge that is easy to get wrong.

How do you implement the patient identity search? Do you search for only patients with unrestricted charts, or do you search with both but only display if you get unrestricted results?  You have to search with restricted patients included!  Consider: If an identity collision occurs, regardless of whether it occurs for a patient with a restricted chart or not, you still have to detect it!  If you were to just search the unrestricted results, and there were two John Smith's whose identity collided, when someone tried to access data for the one with the restricted chart they would get data for the wrong John Smith 100% of the time.

So, you cannot restrict the identity search, but you have to restrict what it reports.  

This will impact patients whose identity happens to collide with those that have restricted access to their chart.  That is where other identifiers or data (such as Mother's maiden name, email or phone number) can help differentiate patients.





Thursday, October 27, 2016

Partial Rejection and Levels of Validity in CDA (or anything else for that matter)

One of the things that I learned in my Informatics education was that there were many different ways to evaluate validity of something.  It's not a yes/no question, but rather a multi-faceted scale.  Most informaticists are familiarity with a 5 or even 7 point scale used to evaluate quality of evidence.  This is essentially the same idea.

In a recent Structured Documents discussion, the topic of what it means to "reject" an "invalid" CDA document was discussed.  When you look at these terms, they seem like yes/no, binary decisions. But here is how you can turn this into shades of gray:

Level 0: Totally bogus content.  Is this even XML?
Level 1: The CDA Header is valid.
Level 2a: Level 1 + the narrative content is valid according to the CDA Schema
Level 2b: Level 2 + the LOINC codes for documents and sections are recognized as valid.
Level 3a: Level 2 + the entries are schema valid according to CDA.
Level 3b: Level 3a + the codes are recognized.

Here's how a system can respond after making these assessments (note that possibly available actions at higher level include those at the level below):

Level 3b: Discrete data can be imported into the system.
Level 3a: Some data can be coded based on string matching, and for that data which has matching codes, that data can be imported into the system after validation by a healthcare provider.
Level 2b: Narrative only sections (such as Reason for Visit or HPI) can be imported, but no discrete data.
Level 2a: After performing some simple pattern matching, some narrative only sections can be imported, after validation by a healthcare provider.  The document can safely be displayed to the end user.
Level 1: The CDA Header is valid.  The fact that a document has been received for a patient from a particular organization can be displayed to the end user.
Level 0: You might be able to look at some magic numbers in the data to figure out what the heck someone sent you, but there's no way to even assess what patient it was for unless you have that stored in some other metadata (thus Direct+XDM evolves as a good solution for sending files).  You might be able to figure out an appropriate viewer for the content, but even then, there are no guarantees it is safe.

Validity?  It's not a switch.  It's a dial.  And mine goes to 11.  Rejection? That's just a pre-amp filter.




Thursday, October 6, 2016

Preadopting FHIR Release Content

There are many times when you aren't quite ready to adopt a new release, either because it isn't fully baked yet (as for FHIR STU3), or you just aren't ready to suck up a whole release in a development cycle to get the ONE feature you would really like to have.

The FHIR Extension mechanism is a way that you can add new stuff to an existing FHIR release. But how should you use it for problems like those described above?

I'm going to be playing with this in the near future because I have some STU2 stuff going on right now, but I really need some of the fixes to Coverage in STU3.  Right now, STU2 talks about subscriber*, but doesn't actually link the coverage to the member (the actual patient being treated)! STU3 fixes that, and I want to preadopt the fix.

So, how am I going to handle my extensions?  Well, first of all, for every field I add, what I want to do is to name it using the STU3 (Draft) URL (and later to the STU3 URL).  So my extension URL becomes something like http://hl7.org/fhir/2016Sep/coverage-definitions.html#Coverage.beneficiary_x_, and bang, I have a referenceable URL that pretty clearly defines the extension.

What does FHIR say about that?

The url SHALL be a URL, not a URN (e.g. not an OID or a UUID), and it SHALL be the canonical URL of a StructureDefinition that defines the extension.

Apparently what I've done isn't quite right because it doesn't follow the FHIR rules (even though it follows the spirit) of FHIR requirements for extensions. Right? So, NOW what?  Somehow we need to give each extension a name.

Actually, let's see what happens when I plug StructureDefinition into this equation.  I now get:
http://hl7.org/fhir/2016Sep/StructureDefinition/Coverage#Coverage.beneficiary_x_.  Click the link to open where that goes in a new window.  Dang!  Pretty fricken close. StructureDefinition/Coverage produces a redirect that goes to fhir/coverage.html instead of fhir/2016Sep/coverage.html.  So close.

In fact, that's nearly close enough for me.  It seems that if we were to fix the redirect problem on the HL7 Server, this would do exactly what I need.  Note: It's NOT a structure definition that defines an extension, its a structure definition that defines a resource.  But really?  Do I have to point you to a structure definition that defines it AS an extension?  Or can I just do the nearly right thing and get away with it.

I may want to adopt a resource that doesn't even exist yet.  Say I want to use some clinical decision support, but I've invested a bit already in Argonaut stuff based on STU2.  How do I handle that? Fortunately, FHIR has the Basic resource, but I'm going to need to extend the heck out of it.  No problem, I just use the same technique, with gusto, and even better, I could put some automation behind it to populate my stuff.  And so could anyone else.

I wonder what Grahame will think about this?

    Keith

P.S. *  Also broken as defined in STU2, and not yet fixed in STU3 because I am not a patient at my wife's OB/Gyn practice, nor am I a patient at my children's Pediatric practice, but they clearly have me on file as some sort of RelatedPerson.  There's already a tracker for this.