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.

Thursday, April 19, 2018

Additional SMART on FHIR Authorizations


I sent something similar to this to the HL7 Security workgroup today:

This strikes me as an interesting use case for SMART or similar OAuth2 based authentication.

The use case is thus:

Through an API, a user wants to include certain kind of information into the record.  A given example might be an uncoded medication.  The provider organization wants to ensure that any uncoded medications have a SEPARATE provider authorization before entry, specific to the API call that attempted the request.  For example, in a system that I am familiar with, if using the EHR client, when an uncoded medication is attempted to be entered, the provider is requested to verify their intent (via a button click).  In another case, to “sign” a document, it requires them to re-enter their password information.

An authorization protocol that might work with a server and 3rd party applications wishing to adhere to these rules would be one in which:

API call which failed due to these sorts of rules would generate a user override authorization request token and pass it back to the caller in an extension to the OperationOutcome resource.  Such a token may also need to be communicated in an out-of-band manner to an authorization endpoint (but could also just be a signed JWT).  The code associated with the OperationOutcome might indicate that additional user authorization is required.

The application would call the authorization server with this token to request user authorization to complete the API request via a web-based UI that used redirection much in the same way that SMART on FHIR does today.  It might also include its current user's access token, although that seems unnecessary since the token supplied by the application would seem to be enough validation of the user.

The authorization server would interact with the user to request their authorization to complete the request.  On receipt of the user authorization (either by clicking yes, or reauthenticating, or whatever), it would provide an override token to the calling application.  Such token could then be passed in to a repeat of the API call.  The API call would find the same problem, but having been given the override token, would be able to accept the request.

Such a protocol might also include in the given override request token a signal with regard to the degree of user reauthentication/authorization necessary.  For example, many systems that have a physician “sign” a document, require the physician to reauthenticate to the application, whereas other override responses are simply a button click (e.g., uncoded medication).

An application that is aware of these special cases might also want to have a way to short circuit the API failure part of the request loop, and preemptively obtain provider authorization.



Your thoughts?  Is this a reasonable thing to consider?



   Keith

Monday, April 16, 2018

If I'm baffled by this, how can patients possibly manage?

I have passwords I use about once a quarter for five different health accounts so that I can track what is going on with myself and my family.  Two Patient portals, one mail-order prescription company, my insurer, my HSA and God alone only knows what else.  I sit through five different sets of menu options whenever I have to call these people one the phone.

Sometimes I can access my children's data, sometimes I cannot.  Usually the latter unless my children have remembered to allow me access (which they willingly do), because one is an adult and the other is over 13.  I could probably have any person in my house claim to be them when I call one of these organizations AND FINALLY GET to a human, but always make sure to have them available.

In trying to follow up on a refill where I know the drug, the dose, the quantity, the ordering provider, and even the providers order number, I cannot get access to whether an order has been placed.  My children's healthcare providers are almost as baffled as I am by the systems they must use.

If trained and aware people are challenged by this system, how could the average patient cope?  How would a disabled patient have any ****ing chance of working through this?  This ISN'T about technology.  Technology could solve these problems.  It's about interpretations of policy that make it difficult for anyone to do anything even remotely difficult.  It's about appropriate use of technology that makes my life better, not yours.  It's NOT about paying attention to your stuff first instead of mine.

Does anyone EVER LIVE test tree menus on automated call systems?  I've had to enter account number and zip code twice, enter my phone number (even though I'm not blocking them from receiving that information), and then when I finally get to a human, repeat the same information, and then be routed to the right person to help me and repeat it AGAIN. Then I have to go through a standard set of three questions they ask on every call that have NOTHING to do with the reason I'm calling.  Yes, this is my address, no it has not changed, and yes you have my current information.  Where is the patient service here?  It's not in my immediate service to make sure you have up to date information, that is NOT my #1 reason for calling you today.  Can we solve my problem first before we make sure that everything else is ok?

My God, today, when I'm home sick and still have to deal with their mess, my patience for how patients are treated is 0, and I just want to )_*(&*(&%*!~ scream.

Payers and providers: here are some things that you can do to fix your systems:

  1. Make your call 911 message as short and direct as possible.  I understand and appreciate why it is there, but I don't call you when I should call 911, and having to wait for you to draw it out drives me crazy.
  2. Let me hit the button I know I need before you finish your first message.  It should be: If you are a current patient/member please press 1.  Get me in the door quickly to solving MY problem.
  3. Solve my problem first, then yours (e.g., has my contact info changed), and NOT the other way around.
  4. If I ask for an operator or hit 0, take me to an operator, not another menu.
  5. Yes, I know you have an online system, IT DIDN'T HELP ME, which is why I'm calling you.  Don't frustrate me further with that information until I'm on the path to queue I need, or at least let me get there quickly.  Making me wait through that message doesn't solve my problem.
  6. Make it easy for me and my family to allow me (or some other individual) to act on their behalf.
  7. Thank you for trying to protect my privacy.  Stop gloating about the great job you are doing and help me solve my problem.  Another message I want to hear AFTER I'm in the right queue and have to wait for the next available operator.


   Keith

I swear I want one of those answering machines I can program myself, just to make you feel my pain when you call me.

Thank you for calling the Boone residence.  If you are a family friend you have called the wrong unpublished number.  Please hang up and call the other number we gave you, or text the person you wish to reach.  If you are a member of the police or fire or other public safety department, please press 1.  If you are a vendor with whom we have an account please press 2. If you are a vendor or charitable society that we have not contacted, please hang up and do not call again.  This phone is on a do not call list, and repeated attempts to call this number will result in criminal prosecution. 3. If you are a healthcare provider, a health insurer, a pharmacy or PBM program with whom a member of this household has an affiliation please press 8 now.  *8*  If you need immediate ...

Thank you for contact the Boone family.  If you are a healthcare provider or pharmacy please press 1.  If you are a payer or PBM please ... *1*

Please enter the extension of the person you wish to reach.  If you would like a dial by name directory, please press 1.  Or press 0 for immediate... *0*

Then we'll ask for you name, phone number and 10 digit NPI number, and if you don't have it immediately, we'll wait a few minutes and then ask you to hang up and call us back when you have it.

How will that work for you?



And to follow up, today, I had to call my homeowners.  They knew who I was BEFORE I told them.  Their claims specialist already had my policy available because they KNEW who was calling them.  And then contacts a water damage specialist for me who called me withing 15 minutes for a same day appointment.  Thank you Liberty Mutual for getting it COMPLETELY right.

Sunday, April 8, 2018

In which I finally figure out dates and timestamps in FHIR

The key is in the last sentence of my last post (half the reason I do these things is to think out loud).  Two DIFFERENT things.  A timestamp is a date and a timestamp.  A date is just a date.  When you compare a date to a timestamp, you are comparing ONLY the date aspect.  When comparing times, you are getting into the timestamp aspect.

User expectations met, problem resolved.  I may have to think about saving the date only representation of a timestamp for efficiency reasons.

    Keith

Right, Wrong and Right again about TimeZones in FHIR

So I am back to dates again.  I forgot to check in the change for setting the servers default time zone, and almost all my unit tests failed on a build machine located outside of my home zone.  Dang it.  I KNEW that would happen.

So I made the fix (following the advice of reasonably smart people), and set the server's default time zone to UTC.  And my date tests are still failing.

Here's the failure:  There's a record that updated on 2017-06-26T17:51:45.233-07:00 (the source of that record is on the left coast, it doesn't matter where the server is).

And I query for it by asking for records updated on that date using _lastUpdated=2017-06-26.

What should happen?

Well, everyone I asked who answered without much thought say that these should match. But they don't.

Because 2017-06-26T17:51:45.233-07:00 is 2017-06-27T00:51:45.233Z in UTC.  And clearly 2017-06-26 is not the same date once you transform the timestamp to UTC, and therefore _lastUpdated=2017-06-26 fails.

The principle of least surprise should apply, and if we use a fixed time zone for the server, it surely doesn't.  Especially a case where the server time zone isn't one my users care about.

So, the conclusion I've come to is, when comparing two dates:

IF both specify precision, the comparison proceeds based on the time as reflected by each date, as it was specified.  There's no problem here.  If neither specifies a time zone, any time zone will do, as long as it is the same for both.

Here's the tricky bit: if A does not specify a precision but B does, the comparison should be based on a common time frame, either A specified a TimeZone and B didn't, or A didn't and B did.  In the presence of a time zone in A or B but NOT both, they should be compared using the same zone.  Otherwise, someone is going to get an unexpected result.

It's that odd duck case where the user asked for a particular date, the server has a specific date and time zone specified for the date (which may be different from where the server itself is located), and boom.

Otherwise, imagine what could happen.  Any test without a time zone is by necessity, imprecise.  I think it's better to specify what should happen in these cases quite specifically, so that time based comparisons don't surprise anyone.

Date has a legal function, time stamps a technical one.  Comparing a DateTime might be used for either case.  So, in my view, even though it seems weird, one has to consider that a DateTime comparison WILL behave differently when using 2017-06-27T00:00+00:00 as compared to 2017-06-27.  The questions are different.  The first is about time, the second about a date, and the two are DIFFERENT but related things.

Ah well.  Another day goes by.

     Keith

P.S. A similar problem will show up when comparing durations. Did it happen in the last two days or in the last 48 hours are two DIFFERENT questions, just as when I speak to my colleagues in India yesterday, today and tomorrow mean different things to each of us, depending on where we are in the world.  Let's not even talk about business days ...

Friday, April 6, 2018

The FHIRWalker Interface

The DOM has the TreeWalker interface, I would suggest that FHIR would benefit from a FhirWalker interface which would allow a resource, datatype or component to be walked in a traversal and processed in certain ways.  Definition of such an interface would benefit many.  Already such interfaces about in implementation code, e.g., in HAPI there are multiple parsers and readers that do the very thing that my FhirWalker interface would support.

Two such walkers would be valuable, one which walked a structure definition for a prospective resource, and the other which walked an actual resource.

Such walkers could be used for reporting, query implementation, et cetera.

I've personally written three, four, (I've now lost count) of these for various purposes.  I got fed up with writing one more and wrote a Walker class for myself.  It looks something like this (though I must admit to some artistic license in the names of things.

interface FhirWalker() {

   // Step describes the interface of the methods that can be
   // performed with the Victim of the FhirWalk.  It's not really
   // used in any way by walk or walkBackwards, but might find 
   // some use in implementations.
   interface Step implements BiFunction<String, Any, Boolean>;

   // Walk defines the interface for a traversal. Like 
   interface Walk implements BiConsumer<Any, Victim>;

   interface Victim {
       // tremble is called before walking on any coal.
       // if tremble returns true, the processor chickened out.
       boolean tremble(String path, Any coal) throws FhirWalkerTrippedException;

       // stumble is called on anything that caused a problem.
       // think of it an an exception handler.  If it returns
       // true, the victim recovered.  Otherwise, the victim 
       // fell down and help needs to be called (an exception thrown)
       // immediately.
       boolean stumble(String path, Any coal) throws FhirWalkerTrippedException;

       // burned is called after walking on a coal.
       boolean burned(String path, Any coalthrows FhirWalkerTrippedException;
   }

   public class FhirWalkerTrippedException {
       public FhirWalkerTrippedException(
           String exclamation, Any coalStumbledUpon, Throwable excuse
       );
   }

   // Walk the FHIR tree in order
   public void walk(Any startingCoal, Victim victim) 
      throws FhirWalkerTrippedException;

   // Walk the FHIR tree in reverse order
   public void walkBackwards(Any startingCoal, Victim victim)
      throws FhirWalkerTrippedException;

   public void walkCanonically(Any startingCoal, Victim victim)
      throws FhirWalkerTrippedException;
}


The walk() method implements a NLRN traversal, calling tremble before processing a node, then processing the children left to right, then calling burned after processing the children.  The walkBackwards() method implements an NRLN traversal, calling tremble first, and burned last. 

Tremble and burned have the same signature even though the return on burned is ignored (once burned it twice shy?)  The reason for that is so that a function used for burned in one place can be
used for tremble in a different traversal.

For what it is worth, something implementing the FhirWalker interface need not traverse an entire tree of a FHIR Resource.  It could implement a traversal of selected subnodes.  For example, a DataTypeWalker might only call tremble and burned on FHIR Data types found, a PrimitiveWalker only on primitives.  A whole host of walkers could be created with great benefit.

Somewhere in the back of my head is a BlindfoldedWalker but I don't know where that's going.

Anyway, enough amusement for the day.  I have my own walker now, time to go for a stroll.

   Keith


Tuesday, April 3, 2018

FHIR date Equality in a Global Environment

Under what circumstances can someone ask a question that can mean one thing in one part of the country and yet something entirely different in another?  When what they are talking about is time.  Just ask your colleague from the left or right coast what time it is, and you'll see they come up with a different answer than you do.

Here's the challenge:  A FHIR Server hosts a number of resources, and each of them have an associated timestamp in Resource.meta.lastUpdated.  So what should you get when you ask for resources that have been updated recently?  Well, frankly it depends upon how you ask.

If you give a date only with no time zone, then the answer you SHOULD get back is the based on the date you supply, as interpreted in the context of the resource that has the timestamp.

Consider: Resource A has timestamp given in terms of Eastern Standard Time.  Resource B has the same timestamp, but using Pacific Standard Time.  Resource C has the same timestamp, but using India Standard Time.  If C was updated today, is it also true that A and B were?  Not for certain, at least according to my thinking.

The answer is, it depends.  First of all, it cannot matter to the server where you are if you don't tell it, so if you give a query based on a date (and thus without a timestamp), it will have to use what it knows, which is whatever timezone it uses for local reference.  Some servers may set local reference time to UTC, others while most would use the local time zone, yet others may have standardized on the local time at wherever headquarters happens to be.  Even so, the variations don't matter, what matters is if you say nothing, the server has to interpret the value. If you do happen to tell the server where you are (e.g., by giving a timestamp with a timezone -- and thus hours and minutes at the very least), then it should interpret time according to what you told it.  So far, so good.

Next up.  But what about what the resource say?  Why does this matter you might ask.  Well, the server and the resource may not agree on what should be in the timezone of a timestamp.  In fact, if your server is just a raw FHIR repository of data, with some applications storing data, and others reading it, then it is the application which decides the timezone associated with the timestamp.  So, if you are with me so far:

Resource A was created a T in EST, B at T - 3 hours measured in PST, and C at T + 10:30 hours.  A time span measuring 13 and 1/2 hours.  More than half the time of the day, the crossover from one day to the next will be between resource B and Resource C's location.  And Resource A will be on one side or the other (it can get worse: If you have your resources on Baker Island, New Zealand and the Line Islands, where for a couple of hours, each could be stamped with a different day).  So NOW what?

If comparing by date alone, the resource date could actually be the one to rule.  If comparing my day to the resource's day, where we disagree on what the time zone is, how is a body (or a server) to compare the two.  We could agree to use the servers time as the point of arbitrage, and the problem would be solved. 

Or would it? How do you commit baseline test results for your server codes unit tests when you have developers in all three zones running them on a local server?

**** if I know anything other than a completely arbitrary answer.  If you have a better one, I'd love to hear it.

    Keith





Monday, April 2, 2018

:not in the presence of multiple values in FHIR

When a resource can have multiple values of a particular type (e.g., identifier, code, _tag, _security), one of the questions that has come up for me was the interpretation of :not.

This is made more challenging by virtue of the fact that different implementations handle it differently.

From a use case perspective, which is more interesting:

Given that patients often have multiple identifiers would you rather than:

Patient?identifier:not=999999999

Return a patient that did not have any identifier=999999999, or would you rather it returned a patient if any one of their identifiers was not 999999999?  Frankly, I want the former.

Similarly for code, if Condition has an ICD-10 and SNOMED code, would you ask for Conditions that are not SNOMED code for Heart Attack to include all conditions that had any other code, or that ensured NO value of Code was the SNOMED code for heart attack.  I want the latter.

This is the rationale behind a recent clarification in FHIR STU4, coming hot and heavy off the presses (see GET [base]/Composition?section:not=48765-2).

I'm all in favor of this, although I must admit to having had some struggles interpreting it.  That's what ballots are for though.

     Keith


Wednesday, March 28, 2018

FHIR workflow is much Better now

Working in an environment where you have many communities collaborating on a project, it is sometimes difficult to ensure that there's overall coordination among all of the moving parts.  A particular example that comes to mind in regard to FHIR was the degree of variation among the
various workflow resources:

In DSTU 2 we had these key ones (I include Procedure because it is a record of what had been done which shares many of the characteristics as the record of what was asked to be done).
  • DiagnosticOrder
  • ProcedureRequest
  • Procedure
  • ReferralRequest

Some of my findings:
The code associated with workflow could be named type or code or item.code
Date of the order had different names event-date (with a specific code identifying which date was the order placement date), data, or orderedOn.
The references to the placer of the request and fulfiller of the request were identified in different ways.

These are the who, what, and when of the workflow for things which FHIR acknowledged were somewhat arbitrary distinctions (the difference between a referral and a procedure and a diagnostic test) worked differently depending on which one you needed to use, EVEN though, you could never actually be sure which was the correct one.

STU3 addressed most of these issues, and the FHIR STANDARD (because that's going to happen soon) will do even more.  The good news is that the right governance was in place to address this issue as we tested this out, and corrections were applied.

As for me, I'm seriously considering how to adopt some search aliases for current STU2 based APIs to ensure that the APIs will do what people meant them to.

     Keith

Wednesday, February 28, 2018

Hello CQL

So you want to learn CQL.  So do I, so I thought I'd probably write a book about it ;-)

Somewhere in the book will need to be the CQL Hello World program, which I'll repeat below for the uninitiated:

define Result: 'Hello World!'

CQL doesn't have assignment statements.  You define things and having defined them, you can later refer to them.  But that's it.  Values are never changed by the program.

That's an essential feature of declarative programming.

By being side effect free, CQL programs can be implemented by an executor in whatever order makes the most sense to optimize performance.  Another commonly used language that works this way is XSLT, which might explain why I like CQL. 

CQL has four primitive data types: Boolean, Integer, Decimal and String, along with the not quite primitive DateTime and Time types.  Boolean uses the traditional true and false values.
It also has complex data types including Quantity, Code, Concept, ValueSet and CodeSystem. 
Beyond that, everything else is either a complex class referencing an information model, or is defined in a Tuple.  And then there is null, which isn't a data type.

Strings are sequences of characters wrapped in single quotes.  Special characters are escaped using \ as in the C and Java language families with all the common escapes and Unicode.

Double quotes are reserved for named identifiers associated with complex things (Code, Concept, ValueSet, and CodeSystem).

Math is math.  Logic is three-valued.  Time is complicated, but less so in CQL than anything else.  CQL moves time from being a great big ball of timey-wimey stuff into linear progression that allows non-time lords to express logic within it.

One of the chapters will have to be about the history of CQL.  In "A theory of everything" written in 2013 I quickly listed some of that history.  Later history includes FHIR, QUICK, QICore, and some other bits and bobbles.  The meeting described in that post reads to me much like the begats in the Bible, and CQL may in fact be the messiah for CDS.  But right now it probably still has to spend its 40 days (or is it weeks, hopefully not months or years) in the wilderness.

Five years.  This is probably the second time in my life where I sat down and looked at a piece of health IT history and went oh shit.  Was it really that long ago?

Anyway, I probably am going to write that book, but don't expect it soon. I still have a lot to learn.

     Keith


Tuesday, February 27, 2018

Logic in the Presence of Unknowns Just Isn't

... logical?  ... executing? ... or as in my case, even vaguely working to my expectations.

CQL today has to work in the presence of unknown values.  We call these nulls.  Null has this weird property of taking over everything in tri-value oriented languages (those where null is expected), and blowing up everything in bi-value oriented languages (those where null is not so much expected).

How can you tell if your language is oriented towards tri-valuedlogic, or bi-valued logic?  Well, the simple answer is what happens when you compare null OR true.  If the answer is that an exception is thrown, you are definitely dealing with bi-valued, and if you get true, then you are dealing with tri-valued, and if you get null, someone screwed up.

So what happens when you try to build a language interpreter for a tri-valued logic system (like say CQL) in a language that is generally bi-valued (say Java).  Some problems around null values.  In the real world, null is a thing.  It happens.  People don't fill out all the fields in a form, some values are simply unknown, or dependent on workflow that hasn't happened yet.  But we still have to reason with it.

Here are some interesting things you need to think about:
When you sort a list of objects based on a field, where do the objects go where the field is null?  XSLT got this right by making a decision, even if you don't like it.  So the behavior is defined.
"The set of sort key values (after any conversion) is first divided into two categories: empty values, and ordinary values. The empty sort key values represent those items where the sort key value is an empty sequence. These values are considered for sorting purposes to be equal to each other, but less than any other value. The remaining values are classified as ordinary values."
CQL doesn't actually cover this case.  Here's what it has to say about sorting:
"After the iterative clauses are executed for each element of the query source, the sort clause, if present, specifies a sort order for the final output. This step simply involves sorting the output of the iterative steps by the conditions defined in the sort clause. This may involve sorting by a particular element of the result tuples, or it may simply involve sorting the resulting list by the defined comparison for the data type (for example, if the result of the query is simply a list of integers)."
So NOW what?  Well, I think a minor adjustment much like what XSL had to say is in order here.

Type conversion is another issue. If you have a defined process for converting from one type to another, then you should also have a defined process for converting null things that might have been a basic data type into other null things that could be a different data type.  For example, the null string should convert to the null date.

Taking type conversion a step further, the string "asdf192832340asdfa8" when converted to a date might in fact return a null value to indicate their is no conversion.  Or it could raise an error.  That's a decision that needs deciding.

What happens when you union or intersect two lists where the list itself is null?  At the very least the behavior needs to be defined.  To see where the problem lies, consider the following:

List<String> l = null;

Is l an empty list, or simply null?  People who build collections are in the habit of returning an empty collection rather than null, but sometimes the collection builder itself returns null because it perhaps doesn't even understand the type of null at execution time.  That's actually OK, just return Collections.EMPTY_LIST (which happens to be pretty much identical to Collections.EMPTY_SET).

Life gets dicey around nulls.  There are no easy rules, you have to think about it.

BTW: This isn't a dissing CQL. I quite like the language.  But then again, I've been known to write tremendous volumes of code in XSL as well, so that isn't necessarily great praise from someone sane ;-).  I'm simply reporting on some of the challenges I'm having in the hopes that they can be fixed, and that others trying to use it can watch out for the hidden (you might even say unknown) pitfalls that are still being worked out.

   Keith

Friday, February 23, 2018

How workflow can affect data and reasoning

Man-scratching-head
I've been playing around a bit with the Clinical Query Language lately.  One of the interesting challenges I had to solve was to deal with some logic defined with one particular data representation in mind with a different representation.

To simplify the problem, I'll look at something that's pretty typical.
Consider the patient history form, a section commonly appearing on the "Clipboard" given to new patients:

Has anyone in your family ever had:
Cancer              [ ?]
Hypertension    [   ]
Stroke               [X]

You'll note here that the patient might not use the form the way it was intended due to uncertaintly about one of the answers. So perhaps it might later be changed to:

Has anyone in your family ever had:
Cancer               Yes   No  Unknown
Hypertension     Yes   No  Unknown
Stroke                Yes   No  Unknown

When encoding this information, there are number of ways to store it in the EHR system. If using precoordinated terms, you can simply list SNOMED CT expressions for all the positive items. This is one way to encode the information. However, pre-coordination of all possible cases doesn't exist in any singular vocabulary.  You cannot say in a single SNOMED CT term that you don't know if the patient has a family history of cancer, but it can be stated in a post-coordinated SNOMED CT expression. So this kind of result is often captured in question/answer form.

There are at least three additional ways to codify this information in question/answer form:
  1. You can codify the overall question, and give a list of codified answers.
    Q1: Family History? A: Hypertension
    This tells you nothing about cancer or stroke.
  2. You can codify each individual question, and list the answers as a yes/no for each checked / unchecked box respectively.
    Q1: Family History of Cancer? A: No
    Q2: Family History of Hypertension? A: No
    Q3: Family History of Stroke? A: Yes
    This doesn't capture the uncertainty about cancer.
  3. You can codify each individual question, and list the answers as a yes/no or unknown.
    Q1: Family History of Cancer? A: Unknown
    Q2: Family History of Hypertension? A: No
    Q3: Family History of Stroke? A: Yes
    This captures the fine detail across the board.

For an application to be able to reason with the data, you have to consider the various ways in which the question could be asked, and how to detect the appropriate response.

The challenge with clinical decision support and quality measurement is then to determine how to map the questions you have answers to into the questions the decision support is asking.  Sometimes, there isn't a clean match (as for cases 1 and possibly 2 above, as well as coding using precoordinated terms).

In CQL, you can easily map codes used to answer questions into a particular form, making it rather easy to change the code systems.

Code systems and codes are specified symbolically, as in:

codesystem "SNOMEDCT": 'http://snomed.info/sct'
code "FH of Cancer": '275937001' from "SNOMEDCT" 

To change from from SNOMEDCT to ICD-10, you might use:
codesystem "ICD10": 'http://hl7.org/fhir/sid/icd-10-cm'
code "FH of Cancer":  'Z80.9' from "ICD10" 


You could also create a single value set containing both codes.  Presently, CQL does not have a way to define a value set, only to reference them (they are defined elsewhere).  
valueset "FH of Cancer": 'some OID or URL reference'

You logic would then only need to address one thing: "FH of Cancer", either as a singular code, or a value set.  

You might cheat here and use a Concept as a way to create a faux valueset across different terminologies, but this is actually discouraged in CQL.  It might be better to create a CQL list of codes and use the contains() expression to determine if the code you have found is in the list of codes.

This doesn't get at negation/unknown logic that might also be needed in formulations 2 and 3 above.  To do that, you can define a function that checks for varying formats, and might also use various value sets for "Yes"/"No"/"Unknown" as possible variations.

When you get right down to it though, the decision that determines how reasoning needs to be done is often taken long before the reasoning is ever implemented.  And you cannot expect that to change quickly, because simply changing the form from the first example to the second can take quite a bit of time in a provider organization.

CQL goes a long way towards making clinical decision and quality measure logic reusable and mappable to provider workflows, but it is still missing a few pieces to make it truly easy to separate logic from data.



Monday, February 12, 2018

Mapping from a FHIR Composition to an XDS DocumentEntry

Rene Spronk asked this one on one of the HL7 lists ... basically the question is what is the mapping from XDSDocumentEntry to Composition.  I give the mapping backwards, how do you produce an XDS DocumentEntry from a composition, rather than forwards, but it can be used in either direction.

XDSDocumentEntry Composition ClinicalDocument
formatCode   (meta.profile) ClinicalDocument.templateId
uniqueId    (meta.versionId)
languageCode   (language) ClinicalDocument.languageCode
    identifier .setId
    status n/a
classCode (mapped from type), typeCode     type .code
    class n/a
patientId, sourcePatientId, sourcePatientInfo     subject .recordTarget
healthcareFacilityType, practiceSettingCode
via reference to encounter/location/physicalType and encounter/location/type
    encounter .componentOf.encompassingEncounter
creationTime      date .effectiveTime
authorInstitution, authorSpecialty, authorPerson, authorRole
via reference to author resource
    author .author.assignedAuthor
title      title .title
confidentialityCode      confidentiality .confidentialityCode
    attester .authenticator/.legalAuthenticator
        mode implied by .authenticator/.legalAuthenticator
        time .authenticator.time
legalAuthenticator          party .authenticator.assignedEnttty
    custodian .custodian.assignedCustodian
    relatesTo .relatedDocument
parentDocumentRelationship          code .relatedDocument.typeCode
parentDocumentId          target[x] .relatedDocument.id
    event .documentationOf.serviceEvent
eventCodeList          code .code
        period .effectiveTime
serviceStartTime             low
serviceStopTime             high

Tuesday, January 30, 2018

Nominations are now being Accepted for the Ad Hoc Harley Award


It's always been true that I am open to others making recommendations for the Ad Hoc Harley award, and in fact, several have been awarded based on the recommendations of others.  In fact, the very first one was created based on a recommendation that I do something for a deserving individual.

I can't keep with everyone who might deserve an Ad-Hoc Harley award these days, but want to continue the tradition with more frequency than I've been able to lately.

So, feel free to e-mail me (click the e-mail me link to the right) your nomination, or if you are at the HL7 WGM this week, catch me at break today or tomorrow (or during Q5 in the appropriate bar).

I'm looking for two things:

  • Someone who hasn't previously been recognized for their service.
  • A unique contribution to Health IT and/or Health IT Standards
Everything else is open, and well kinda Ad Hoc ...

   Keith



Getting Water from a FHIR Operation

Water drop impact on a water-surface - (1)
According to the FHIR Operation specification (STU2 and 3 say the same thing):


If there is only one out parameter, which is a Resource with the parameter name "return" then the parameter format is not used, and the response is simply the resource itself.

So, if you want to return non-FHIR content in your operation, return a Binary resource. The FHIR Server (if properly implemented) would interpret the resource as content to be returned in the usual web way.  

I've verified that the HAPI 3.0 Server does in fact operate correctly when you return a Binary resource for an operation.

So, if you want your FHIR operation to return a CDA document, you can get it to do so.  Heck, if you really wanted to, you could get it to return V3 or even better (sic) a V2 message.  DICOM anyone?

   Keith

Thursday, January 25, 2018

Architecture is about all the nouns

People, organization, technology, and processes. They're nouns.

These are the nouns that makes an architecture succeed.  A good architect can design a beautiful building (a thing).  But a great one will design a building that also works well in its environment.

As a software architect, I've had to learn this the hard way.  It's not just about making the technology (another thing) do what it needs from scratch in a green field.  It's about making it easy to transition from the way it was done to a better way, a more efficient way.

The very first XDS implementations listed documents for patients and gave people a way to search for documents and list them out in various orders on the screen, and view them.  This is the way processes (ideas) at that time often worked worked, because documents were "different" from data. 

Today, we use XDS (and its variants) in all kinds of different ways, where the document is simply payload for other activity and where the views of what was communicated in the document as shown by the user interface is many and varied, and sometimes completely under the covers.  This is architecture that not only fits into its environment, but one which improves it significantly.

XDS worked so well in part because it enabled people to work in familiar ways (using existing processes) at the same time as it enabled existing processes.  It also worked well because it enabled NEW processes to be developed that could be delivered over time.  It overcame objection to change because it eliminated steps, rather than adding new ones or changing existing ones.  It also worked WITH the technology that already existed.

Understanding the people, the organizations and their processes is just as critical to making technology work as understanding the technology.

   Keith

Thursday, January 18, 2018

CQL for Me, or what not to do at an IHE Connectathon

Testing22222
If you think you are going to implement a brand new feature during Connectathon week, then you probably are about as brain dead as our poster child for accidents waiting to happen on my right.  Having said that, my job for this week is to at least get started on implementing a clinical quality language execution engine into my FHIR Server.

So much for brains, right?  One of the biggest challenges I've had is that while there's plenty of open source implementations out there, there's not a whole lot of documentation on how to use them.  And while as a language, CQL is certainly easy enough to read, there's some very basic assumptions about a programming language that are left completely unstated in the standard.

"What does hello world look like in CQL?" is one of those questions that I hadn't found (nor could Google) anyone addressing.

That's a really good question.  The answer isn't to be found in the CQL Specification, but I did eventually figure it out.  It looks like this:

define result: 'Hello world!'

Pretty simple.  The execution of a CQL program basically produces a list of results, where each result has a name and a value.  This makes it really ideal to use a JSON representation for output, by the way.  There's also a type (associated with the value), and a location associated with the definition that you can get out of the engine I'm using (which makes it really handy for debugging).

This was my DUH moment last night.  Now it doesn't have to be yours.  I think I'm about to join that open source community as a doc writer (at least to start off).

   Keith

Tuesday, January 16, 2018

Converting STU2 to STU3 in HAPI on FHIR

AC Converter Adapter USA-EU
So, you've built your HAPI FHIR Server, and if you started when I did, you may have even started with STU2.  But these days IHE profiles are in STU3.  What's a guy to do?  Well, if that guy is an engineer, the first thing you WON'T DO is rewrite your server from scratch.  For PIXm, PDQm, QEDm and likely even MHD (although I haven't checked that profile out yet), the simple answer is to front end your STU2 services with an STU3 facade.

The pattern for such a facade is fairly simple:

public class MySTU3ResourceProvider  {
  private final MySTU2ResourceProvider stu2Provider;
  private VersionConvertor_10_30 converter = 
     new VersionConvertor_10_30(new NullVersionConverterAdvisor30());

  public MySTU3ResourceProvider(MySTU2ResourceProvider stu2Provider) {
     this.stu2Provider = stu2Provider;
  }

  @Search() Bundle doQuery(...) {
     try {
       // call the previous method and get the bundle response
       return coverter.convertResource(stu2Provider.doQuery(...));
     } catch (BaseServerResponseException ex) {
       // We got a "normal" exception from the processing.
       Object o = 
           converter.convertOperationOutcome(
               (OperationOutcome) ex.getOperationOutcome());
             ex.setOperationOutcome((IBaseOperationOutcome) o);
       throw ex;
     } catch (FHIRException fex) {
       throw new InternalErrorException(fex);
     }
   }
   @Read Resource doRead() {
     try {
       // call the previous method and get the bundle response
       return coverter.convertResource(stu2Provider.doRead(...));
     } catch (BaseServerResponseException ex) {
       Object o = 
           converter.convertOperationOutcome(
               (OperationOutcome) ex.getOperationOutcome());
             ex.setOperationOutcome((IBaseOperationOutcome) o);
       throw ex;
     } catch (FHIRException fex) {
       throw new InternalErrorException(fex);
     }
   }
}

One of the things I learned this week was that I needed to add the conversion trick to handle any cases where the original method throws something derived from BaseServerResponseException.

This is great.  Until you discover that for some reason, convertMedicationOrder hasn't been implemented, and you need to turn your previous MedicationOrder into a MedicationRequest.

So, buckle down and write convertMedicationRequest(), or use the code I wrote below.  Here's how I started: It took me about 3 minutes to find that the code had been commented out.  It took me another 15 or so to code it up, and a few more to test and tweak it.  All told, about 30 minutes. To skip to the chase, here's the code (it's not pretty, but it seems to compile and run):

VersionConvertor_10_30 converter = new VersionConvertor_10_30(new NullVersionConverterAdvisor30()) {
    
      public org.hl7.fhir.dstu3.model.Resource convertResource(org.hl7.fhir.instance.model.Resource src) throws FHIRException {
      if (src instanceof org.hl7.fhir.instance.model.MedicationOrder) {
      return convertMedicationOrder((org.hl7.fhir.instance.model.MedicationOrder)src);
      }
      return super.convertResource(src);
      }
      
      public org.hl7.fhir.dstu3.model.MedicationRequest convertMedicationOrder(org.hl7.fhir.instance.model.MedicationOrder src) throws FHIRException {
      if (src == null || src.isEmpty())
        return null;
      org.hl7.fhir.dstu3.model.MedicationRequest tgt = new org.hl7.fhir.dstu3.model.MedicationRequest();
      copyDomainResource(src, tgt);
      for (org.hl7.fhir.instance.model.Identifier t : src.getIdentifier())
        tgt.addIdentifier(convertIdentifier(t));
      tgt.setStatus(MedicationRequestStatus.fromCode(src.getStatus().toCode()));
      tgt.setMedication(convertType(src.getMedication()));
      tgt.setSubject(convertReference(src.getPatient()));
      tgt.setContext(convertReference(src.getEncounter()));
      if (src.hasDateWritten())
        tgt.setAuthoredOn(src.getDateWritten());
      tgt.getRequester().setAgent(convertReference(src.getPrescriber()));
      if (src.hasReasonCodeableConcept())
        tgt.addReasonCode(convertCodeableConcept(src.getReasonCodeableConcept()));
      if (src.hasReasonReference())
        tgt.addReasonReference(convertReference(src.getReasonReference()));
      if (src.hasNote())
        tgt.addNote().setText(src.getNote());
      for (org.hl7.fhir.instance.model.MedicationOrder.MedicationOrderDosageInstructionComponent t : src.getDosageInstruction())
        tgt.addDosageInstruction(convertMedicationOrderDosageInstructionComponent(t));
      if (src.hasDispenseRequest()) {
      MedicationRequestDispenseRequestComponent dr = tgt.getDispenseRequest();
      MedicationOrderDispenseRequestComponent sr = src.getDispenseRequest();
      if (sr.hasValidityPeriod()) 
      dr.setValidityPeriod(convertPeriod(sr.getValidityPeriod()));
      if (sr.hasNumberOfRepeatsAllowed())
      dr.setNumberOfRepeatsAllowed(sr.getNumberOfRepeatsAllowed());
      if (sr.hasQuantity()) 
      dr.setQuantity(convertSimpleQuantity(sr.getQuantity()));
      if (sr.hasExpectedSupplyDuration()) 
      dr.setExpectedSupplyDuration(convertDuration(sr.getExpectedSupplyDuration()));
      }
      if (src.hasSubstitution()) 
      tgt.getSubstitution().setReason(convertCodeableConcept(src.getSubstitution().getReason()));
      tgt.setPriorPrescription(convertReference(src.getPriorPrescription()));
      return tgt;
    }


    };

This week is all about getting things close enough.  Afterwards I get to bring the code back to turn it into production.  But I'm guessing that I'm not the only one wanting to convert STU2 MedicationOrder resources into STU3 MedicationRequest resources.  So, here you go.

   Keith

P.S.  As to why this is commented out, my suspicion is that the code generator that is producing the converter from the FHIR Specification hasn't quite been tuned to deal with Resource name changes yet.

Thursday, January 11, 2018

Twenty things I've learned about the ED as a Chronic Pain Patient's Advocate

Cbh emergencydepartment1
I've been spending a good bit of time as a patient advocate for a soon-to-be family member over the last month (and prior).  There were 7 ED visits, 2 ambulance rides and 3 911 calls due to untreated pain in the month of November.  It took 3 ED visits to get a diagnosis for their acute pain (Endometriosis), and some forward motion toward treatment, and another 4 since then while waiting for follow-up visits (see The Patient is Waiting ... in Constant Pain).  We've been in 2 hospitals, and 9 ED beds, 15% of the beds in the local ED.  I almost want to start carving initials.

Here's the thing, for certain conditions causing acute, level 9 and above pain, it's hard to get a diagnosis, and difficult to get consistent treatment.  Having an advocate has been crucial to making any sort of progress. When a patient is in crippling pain, it's damn near impossible for them to have an intelligent discussion about what is going on. When, as a result of ongoing chronic pain, other issues arise such as anxiety or depression, it's damn near impossible for them to get respect as a patient. Finally, when one of your diagnosis is "questionable" (e.g., Fibromyalgia) in the mind of the doctor (even though as an ED physician they are not the specialist who provided that diagnosis), it's challenging to even get good treatment (see It's not real).

What I learned in November:
  1. Be aware that if you show up on a weekend during or after any holiday, life is going to have some extra suckitude in it.  These are busy times and this is "high season" in the ED.  If you can, call ahead, better yet, have a doctor do it. 
  2. Coming in by ambulance will get the patient into an ED room immediately, but they may be moved later (many EDs reserve specific rooms for ambulance arrivals).  
  3. Treatment for pain CAN start in the ambulance if the ambulance is appropriately staffed and willing.  My local ambulance staff are fantastic (but I also live in a town where everyone knows everyone else), others are probably good but don't know us. Our local guys called the nearby paramedics from one town over to meet them en route, and having relief start in the Ambulance is huge!  
  4. If the patient isn't dying on the table, be prepared to wait with them ... in acute pain, for some time.  Remember that the ED doc's purpose is first to stabilize you, if you are in pain but not obviously dying, and your heart rate and blood pressure aren't super critical, you are probably at a level of stable that means you won't get as much attention as you'd like.
  5. When the patient is in pain, don't be afraid to let them cry or make noise, and to keep the door open. They'll get attention faster.
  6. Wait obviously, ask for attention gently, be aware that the folks you are working with are probably crazy busy, and will work better with you if you can be gentle (if insistent) with them.  
  7. If you have a specific need, ask for it (from the right person).  A pillow, water, a straw can be gotten by just about anyone, but the doctor is the worst person to ask these for.  Turning the monitor alert off requires someone trained to do that (doctor or nurse usually).
  8. If the monitor is beeping but nobody is responding, press the call button. It's faster than asking someone.  If there's a problem with over-alerting on the monitor, get them to address it.  Monitor beeps are purposefully attention getting, which can increase patient agitation -- not a good thing for a patient in pain.
  9. Come in with a clinical, rather than psychiatric reason for visit.
  10. Know the lingo. The more you speak "medical", the better they listen.
  11. Talk about what your expectation for the visit are, and get answers about what the doctor's expectations are.  Ask for times, let them know you are seeking approximate answers so that you know what to expect, rather than precise answers, and that you know they are busy and that stuff happens.
  12. You are rolling the dice on providers to be seen.  If you have a clue about what kind of provider, or know a name of someone in the hospital you have seen before, use it, ask to see them if they are available. When the shift changes, the attending doctor and plan may change too, be prepared for that.
  13. Have the medical proxy paperwork in your hands. A power of attorney is better than a medical proxy form if it allows you to take more charge (Most proxy forms address "patient is unable", but a PoA can simply designate you with certain authority).  Introduce yourself to staff by your relationship, both personal and legal to the patient.  Use the personal first, but place some emphasis on the legal authority if you have it.
  14. Keep track of the patient records, and have them on hand, electronically if at all possible.  We use MyChart extensively because it is available through all of our providers.  If you (or the patient) can state dates for relevant events, their medications including dose, and all of their conditions and allergies, you'll get more regard from the staff.
  15. If you have a choice of hospitals, consider them carefully.  My local regional hospital is beautifully laid out with modern equipment and a new facility, but the care provided by the staff is inconsistent.  On the other hand, our visit to the nearby academic medical center left me quite sad about the state of their physical infrastructure, but the one visit made there was FAST, provided excellent care and pain relief within 90 minutes AND to a degree I hadn't seen in the regional facility.  We were in and out in about 5 hours, which for us was about three times faster than the regional.  From a facilities perspective, I'd rather be in the regional, but from a care perspective, if we have a choice, we'll go back to the academic facility.
  16. The ambulance will take you to the nearest hospital, which is NOT necessarily the one you want to go to.  If hospital choice makes a difference, consider your options carefully.  Acute pain is not always life threatening, but when it is, you want to be in the ambulance.
  17. If the patient has a wheelchair or other mobility device, bring it with you. It helps to get attention needed, and it can make getting there easier.
  18. Keep a bag packed for the visit; we now have a go bag packed.  It includes plenty of water, some form of food for snacks: Oreo cookies are a favorite, but we also include protein bars, pudding in favorite flavors, and Slim Jims. The food is for both you and the patient if they are allowed to eat something. 
  19. Include a phone charger in your go bag.  Bring your own soft fuzzy blanket and whatever else will bring you comfort.  Dress in easy to access clothing, bring an extra pair of underwear.
  20. As an advocate, know a wide variety of ways to deal with pain.  Distraction helps.  Usually by the time you are headed to the ED, you've already tried everything you know.  Here are some things I've discovered.  Deep breathing is essential. Distraction works: Tell good stories, engage in pleasant and fun conversation, have the patient tell you a favorite story. Use a variety of relaxation techniques -- techniques that have already been practiced several times work better usually. Gentle massage is good.  A warm shower can sometimes help reduce pain.  If the patient is locked in pain, is having muscle spasms only makes it worse.  Do what you can to get them to relax.  
#15 was probably the most important thing I learned in November.