Monday, June 28, 2021

Help Adrian Get Treatment

I'm asking this at the request of my eldest adult child, going by Aeowolfe these days.  Some of you may have met him a few years back or seen him help me out in some videos for Health IT in the past.

My eldest child's partner has been suffering from chronic pain since mid-2017.  The onset of the disease occurred while they were at College were my eldest met them.  Since then, they lost the support of their family, have subsequently been diagnosed with Fibromyalgia, hypermobility disorder (similar to Ehlers-Danlos syndrome, only missing direct family member diagnosis to match the clinical criteria), and endometriosis.  Our family has been providing support in various ways, including helping them get signed up for Mass Health and disability through Social Security (which took over 3 years to get to).

They've found a treatment plan they think will help (Ketamine-Lydocaine transfusions), but they cannot afford them at their present income, and insurance doesn't yet cover this treatment because it is new (there have been several clinical trials with positive results).  So my eldest has started a gofundme for Adrian.  If you are able and willing to contribute you will have both my thanks, and that of my eldest.



Wednesday, June 16, 2021

Creating your FHIR Artifact JIRA Specification

When you go to ballot, or any form of publication for a FHIR IG through HL7, you have to provide an XML document that defines the pages and artifacts that the HL7 Jira system will use for reporting issues or comments on the specification.

If you are smart, you create an initial template in the HL7/JIRA-spec-artifacts page when you start your project.  If you aren't, you wait a while and then create it.

The initial version should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<specification
  gitUrl="https://github.com/HL7/fhir-project-mhealth"
  url="https://hl7.org/fhir/uv/mhealth-framework"
  ciUrl="http://build.fhir.org/ig/HL7/fhir-project-mhealth"
  defaultWorkgroup="mh" defaultVersion="0.1"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="../schemas/specification.xsd">
  <version code="0.1"/>
  <artifactPageExtension value="-definitions"/>
  <artifactPageExtension value="-examples"/>
  <artifactPageExtension value="-mappings"/>
  <page name="(NA)" key="NA"/>
  <page name="(many)" key="many"/>
  <page name="(profiles)" key="profiles"/>
</specification>

But you will need to add lines for each artifact (Profile, ValueSet, et cetera) and page (.md or .html file) that you generate.

  <artifact name="Artifact Title" 
    key="artifact-id" id="artifact-filename-without-extensions"/>
  <page name="Table of Contents" key="toc"/>

This can  be awfully tedious, but the IG Publisher can create an updated one for you, although if  you haven't created one in JIRA-spec-artificact, for some reason it doesn't seem to create an initial one for you.  I'm not clear on why it doesn't, but I found a workaround.

What you do is create that initial version, and copy it to your templates folder, naming it jiraspec.xml.  Then you run the IG Builder without a vocabulary server.
C:\myproject>  JAVA -jar "..\%publisher_jar%" -ig ig.ini -tx n/a

Telling the IG Builder that you don't have a vocabular server makes it assume that you do NOT have an internet connection, and so it also doesn't try to get your templates or copy the current JIRA Spec over that file I had you create above.  Now when you run the IG Builder, it will create an initial JIRA spec file for you, which you can then generate a pull request to https://github.com/HL7/JIRA-Spec-Artifacts/xml.

Once you've finished, you can find the created specification in your project in template/jira.xml, which you can then rename appropriately and send a pull request to the JIRA-spec-artifacts page.

Ideally, this specification configuration could be more automated in a FHIR IG Build, but for now what we have works.  It's just a bit of a pain.


Wednesday, June 9, 2021

The Interop needle in 2030


ONC has been asking about Health Interoperability Outcomes for 2030.  Some sample statements they'd like the answers to include:

  • “Because of interoperability, ______ before/by 2030.”
  • “Because of interoperability, before/by 2030 [who] will [what].”

    In Why We'll Never Have Interoperability, I note that the goal posts are always moving, bars are ever being raised, the needle just keeps going around and around.  So I thought it would be interested to look at this from the viewpoint of "what will we be concerned about" in 2030 as the next steps, rather than focusing on what we've accomplished.

    Given all of that, these are the problems I think we will we still be trying to solve:

    1. Making challenging content understandable to the average patient.
    2. Making standards of care (e.g., care guidelines) measurable and computable.
    3. Understanding the actual cost of care.
    4. Crossing domain boundaries (care, payment, social services, public health, emergency medical services, research).  Each of these domains is still widely separated with respect to standards
    5. Few of these domains have progressed as far with APIs as simple as FHIR.
    6. Handling variations in dialects of FHIR as supported by different vendors.
    And these are the problems that I think we will have made headway on:
    1. FHIR will have become ubiquitous in hospital and ambulatory practice interfaces between healthcare systems and devices.  It will become available in not just EHR systems, but also departmental, laboratory, medical devices, revenue cycle, and to some degree, imaging systems (although DICOM will retain significant dominance).
    2. Patient facing APIs will be ubiquitous.
    3. Visit scheduling, and much of the pre-visit "paper-work" will be done via the web for most patients.
    4. FHIR will have crossed into the payer space, and HIPAA transaction standards, invading some of the territory previously owned by X12 and NCPDP.
    5. We will start to see FHIR transition into other healthcare related domains (e.g., EMS reporting, social services), but adoption will be limited.



    Monday, May 24, 2021

    A Source Code Profiler for FHIRPath

    I wrote my first source code profiler in 1992, mostly as an exercise to play with but also because back then I didn't have the money for a professional one, and wrote about it in Dr. Dobb's Journal.  That was a sampling profiler, and used the timer interrupt on a PC to determine where in the code the application was running.  Since then, I've had to write my own profiler several times professionally for different reasons and purposes.  Once I wrote some code to instrument some BASIC code and profile it.  That code was running a web site for a major hardware vendor's web store (having been flown onsite to address a critical sporadic outage).  While the profiler didn't show what was causing the outage, it did demonstrate that the application our company had developed wasn't the source of the problem, and I later DID determine what the cause was and we fixed it.  I was given my first handheld computing device as a reward for that save.  And I've been called to develop profiling tools for other custom languages and application implementations as well.

    The focus of this week's FHIR Connectathon for SANER was performance and scaling.  We finished up at noon yesterday, but I was already wire to the rim with caffeine, so I looked for something else interesting to do.  As a result, I instrumented my version of the FHIR Path engine (it's basically a copy of the HAPI engine with some tweaks for use as a SANER Measure Computer) to profile the Measure code.

    There are three places in SANER where a FHIRPath is evaluated to compute a measure:

    1. Measure.group.population which produces the counts for the population.
    2. Measure.group.stratifer which produces the strata for each element in the population.
    3. Measure.supplementalData which specifies how to collect supplemental data for each element in a population.
    Each of these may have an associated FHIRPath expression, and of course, I want to know how the expression performs.

    The FHIRPath engine produces a parse tree of ExpressionNode items.  The tree is walked by FHIRPathEngine.evaluate functions.  What I did was simply added some code to trace the execution times in those functions in my FHIRPath engine code.

    The core class and methods to support tracing in my engine is below.  Note, this is hacked Proof-of-Concept code I wrote in about an hour.  Documentation and everything else is coming.

       class Trace {
            String expr;
            long start, end, children;
            ExpressionNode node;
            int parent;
            int index;

            private Trace(String expr, int index) {
                this.expr = expr;
                this.index = index;
                this.start = System.nanoTime();
                this.parent = -1;
                Object o = new Object();
                o.toString();
            }

            private Trace(int parent, int index, ExpressionNode node) {
                this.parent = parent;
                this.expr = (parent == -1 ? null : traces.get(parent).expr);
                this.index = index;
                this.node = node;
                this.start = System.nanoTime();
            }

            public void done() {
                this.end = System.nanoTime();
                long childTime = end - start;
                top = this.parent;
                if (top != -1) {
                    traces.get(top).children += childTime;
                }
            }

            public void done(ExpressionNode node) {
                this.node = node;
                done();
            }

            public String toString() {
                StringBuilder b = new StringBuilder();
                if (parent == -1) {
                    b.append(",,,,,,,\"").append(expr == null ? " " : expr.toString().replace("\"", "\"\"")).append("\"\n");
                }
                b.append("@").append(Integer.toHexString(expr.hashCode())).append(",").append(index).append(",")
                    .append(parent).append(",").append(start).append(",").append(end).append(",")
                    .append(end - start - children).append(",\"").append(node.toString().replace("\"", "\"\""))
                    .append("\"");
                return b.toString();
            }

            public void setExpression(String expression) {
                this.expr = expression;
            }
        }

        public void dump(File f, boolean append) throws IOException {
            try (FileWriter w = new FileWriter(f, StandardCharsets.UTF_8, append); PrintWriter pw = new PrintWriter(w);) {
                pw.println("Node,Expr,Index,Parent,Start,End,Time,Expression");
                for (Trace t : traces) {
                    pw.println(t);
                }
            }
            traces.clear();
        }

        private Trace trace(String path) {
            if (!TRACE) {
                return dummy;
            }
            Trace trace = new Trace(path, traces.size());
            traces.add(trace);
            top = traces.size() - 1;
            return trace;
        }

        private Trace trace(ExpressionNode node) {
            if (!TRACE) {
                return dummy;
            }
            Trace trace = new Trace(top, traces.size(), node);
            traces.add(trace);
            top = traces.size() - 1;
            return trace;
        }

    Sprinkled in key places in functions that actually execute the profiling code are the statements (in a slightly larger font) in the code below:

        public List<Base> evaluate(Base base, ExpressionNode expressionNode) throws FHIRException {
            Trace trace = trace(expressionNode);
            try {
                List<Base> list = new ArrayList<Base>();
                if (base != null)
                    list.add(base);
                log = new StringBuilder();
                return execute(new ExecutionContext(null, base != null && base.isResource() ? base : null,
                    base != null && base.isResource() ? base : null, base, null, base), 
                    list, expressionNode, true);
            } finally {
                trace.done();
            }
        }

    What goes on is that the call to trace(expressionNode) creates a new timestamped execution record to a stack of records which also points to the record of the calling method [often at the top of the stack].  Then, when the function is finished, trace.done() simply adds the ending time stamp, accumulates the time of this call to an accumulator for child times in the parent record, and returns the stack pointer to the parent's record.

    When a user of the engine calls the dump() method with a file, the execution data is dumped to that file in CSV format.  Records look something like this:

    Expr         Index Parent Start         End         Time Expression
    @63efb167 1 -1 611441841022000 611451511487500 4711600 findAll('Encounter', ...
    @791ea926 4 3 611441845767800 611441845782600 13400 'Encounter'
    @791ea926 5 4 611441845771000 611441845772400 1400 'Encounter'
    @6344cac3 6 3 611441845783900 611441847333500 12700 including('subject', 'diagnosis', ...
    @6344cac3 7 6 611441845785400 611441847322300 1533300 including('subject', 'diagnosis', ...
    @27688320 8 7 611441845786100 611441845787600 1200 'subject'
    @27688320 9 8 611441845786600 611441845786900 300 'subject'
    @39126a23 10 7 611441845788000 611441845789200 1000 'diagnosis'
    @39126a23 11 10 611441845788600 611441845788800 200 'diagnosis'
    @35c9a3be 12 7 611441845789500 611441845790400 700 'reasonReference'
    @35c9a3be 13 12 611441845789900 611441845790100 200 'reasonReference'


    The expression identifies the unique expression from the parse tree being evaluated.  The index and parent describe the evaluation tree (which may evaluate the same expression multiple times with different inputs).  The start and end time reflect the timestamps (in nanoseconds from an arbitrary base).  Time represents the elapsed time (again in nanoseconds).  The expression gives the full text of the evaluated expression.

    You can take this data into a spreadsheet, create a pivot table report, and come up with useful reports on those parts of the expression taking the longest, as shown below.

    Expression   Time (ms)  Executions
    onServers(%Base).exists()         9145.7723         282
    onServers(%Base).where(...         405.8266           2
    $total | $this     47.0435          94
    %ReportingPeriod.start - 1 'year'   10.4663         423


    I normalized times to milliseconds so that the result would be more meaningful.  This shows that the retrieval of primary data takes about 9.1 seconds, and a secondary retrieval about 0.4 seeconds, while everything else is 50ms or less.

    It also demonstrates some opportunities for optimization in the FHIRPath engine.  For example the 10ms taken on the the multiple evaluations of the common sub-expression:
      %ReportingPeriod.start  - 1 'year' 

    This is likely more costly than it needs to be.  Since the FHIRPath engine today does nothing with common subexpression elimination, the profile shows the cost associated with re-evaluating these in the parse.  That's probably an opportunity for a whole other post.

        Keith


    Tuesday, May 18, 2021

    SANER Scales

    This week's HL7 Connectathon 27 SANER Track is all about scalability.  For this Connectathon, we brought over 2 million clinical resources worth of test data for 2600+ patients, transported from Synthea's 100K patient COVID-19 test data set from Massachusetts into Cook County, Illinois, transposed in time to January of 2021 instead of March of 2020, across 15 hospital locations.  Because these patients were transported from the past, they aren't vaccinated, don't have access to Remdesivir, and probably too many are taking HCQ, but it was a good enough set to test the scalability of SANER.

    My chief concern was computing MeasureReport on a daily basis, overnight, and whether that was going to be a huge data load for hospitals.  As it turns out, I'm able to put those concerns to rest (for me at least).

    We computed 465 MeasureReport resources, one for each of 15 hospitals over the 31 days of January, using realistic hospital loads drawn from current statistics reported by the Illinois Department of Health.

    Each measure report communicated around 240 (average) supplemental data elements (FHIR Resources) providing additional data to support stratification and analytics, which about 40 times what would actually be needed if just communicating metrics.

    All told, this represented about 465Mb of uncompressed, pretty printed FHIR Resources in XML format, or about 23Mb of data compressed using GZIP.

    Best, yet, I was able to collect data from the cloud, compute measures, store them locally and transmit all the data for all days for all hospitals to a server in the Cloud in about 11 minutes on a pretty high-end Dell Laptop (6 cores, 3.6Ghz Burst, 32Gb of RAM).

    I've still got some bugs to look into which might slow things down once fixed (mostly on stratification), but with 12 virtual processors running, this load barely touched my machine.  Overall, CPU utilization was at a pretty steady 20%, and network bandwidth also nowhere near saturated.  My home office gets about 150-200Mb down, 20Mb up, I barely touched it.

    I can process the data for a single hospital day in 10-20 seconds depending on the number of patients.  It's realistic to assume that more frequent, semi-real-time situational awareness measure evaluation and reporting is not only feasible, but also practical.

    Most of the measures we have examined are written in a form that supports daily computation.  We'll probably have to experiment with measures designed for more frequent evaluation.

       Keith

    * We keep hemming and hawing about near-real-time measures, and I've finally decided to call them semi-real-time, to clarify that they could be several minutes out of date, but still orders of magnitude better than daily.  With enough concentration, semi-real-time could in fact become near-real-time (so long as the data sources themselves are frequently updated).


    After doing some more tweaking I'm actually:
    1. Overwhelming my server so hard it requires a restart to come back to life.  I really need to get my server set up in a production ready way.
    2. Running a bit slower but getting more data (so now it's taking about 28 second a hospital on average).

    Tuesday, May 11, 2021

    Tracking Supplies via SANER

    One of the outstanding challenges yet to be demonstrated at a Connectathon is the ability of SANER to report on supplies.

    Some common examples include N95 Respirators (a.k.a. Surgical Masks), Ventilator supplies (e.g., tubing, connectors, et cetera), gloves, gowns and cleaning supplies.

    How would you track these materials in ways that would enable an inventory system to track them?  

    Honestly, this is an area that I've spent very little time with, but is one focus area I plan to study for the upcoming May HL7 Connectathon.

    What I already know:

    All of this knowledge is captured, registered, and publicly available.  Some possibly for purchase, some freely accessible, some even with an API.

    So, if you have UPC numbers or GMDN codes, or GUDID data, you might actually be able to create a value set that you can get to from the item codes used to describe supplies in the hospital inventory control systems.




    Monday, April 19, 2021

    Make Vaccine Scheduling "Scheduler Friendly"

    Every family I know has (at least) that one person who has to have the planning calendar, and that other person who keeps track of all the important documents, and that other person that they call on for healthcare related stuff, and finally, the computer geek.  And they may all reside in the same person.  One of these is very likely the COVID-19 vaccine scheduler.

    As I think about how vaccines are opening up, and my own experience in Massachusetts scheduling vaccines for my family, here are some of my experiences:

    1. I have to enter the same insurance data each time for each different person I'm making an appointment for.  If only there was a standard for the layout and OCR font for insurance cards, or better yet, even a standard bar-code or QR format for insurance information, it could have made my life so much easier.

    2. I could never schedule more than one person at the same time, even if there are two or three qualifying individuals that I need to schedule for at the same time (and appointments open).  This resulted in me making 2 or 3 different appointments for a two groups of people who each had to travel over 30 minutes to a total of 5 different locations during two different enrollment periods. In one case, I fat fingered the first appointment date, which meant I had to reschedule one of the appointments, which led to a three week delay in getting a replacement appointment.
    I've seen six different scheduling interfaces (four for drug-stores, two for my state public health sites), not one of them is really designed for the person in the family who does the scheduling for most of the family members.  These same changes could readily enable others who volunteer to assist others in scheduling work more efficiently.

    There are balancing factors. Making it easy for one person to schedule multiple appointments at the same time and location would benefit families, but single individuals living alone would be disadvantaged by such a system.  But if there are enough vaccines (and appointments) to go around, this would be less of a problem.

    We're likely going to be scheduling shots for some time yet.  We've only gotten shots into the arms of about half of the US population, and these aren't likely to be the last COVID-19 shots that have to be given.  Booster shots are expected by some vaccine manufacturers.