From 8ded418271003d9aa3f57cf031511fb345588dee Mon Sep 17 00:00:00 2001 From: Dylan Hall Date: Thu, 7 Dec 2023 14:50:41 -0500 Subject: [PATCH] flexporter enhancements --- src/main/java/App.java | 2 + .../synthea/export/flexporter/Actions.java | 67 ++++++++++- .../modules/heart/acs_discharge_meds.json | 11 +- .../resources/flexporter/qicore_minimal.yaml | 4 - .../flexporter/qicore_withnotdone.yaml | 105 ++++++++++++++++++ 5 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 src/test/resources/flexporter/qicore_withnotdone.yaml diff --git a/src/main/java/App.java b/src/main/java/App.java index e790479a45..ada4a83e1e 100644 --- a/src/main/java/App.java +++ b/src/main/java/App.java @@ -208,6 +208,8 @@ public static void main(String[] args) throws Exception { if (flexporterMappingFile.exists()) { Mapping mapping = Mapping.parseMapping(flexporterMappingFile); exportOptions.addFlexporterMapping(mapping); + // disable the graalVM warning when FlexporterJavascriptContext is instantiated + System.getProperties().setProperty("polyglot.engine.WarnInterpreterOnly", "false"); } else { throw new FileNotFoundException(String.format( "Specified flexporter mapping file (%s) does not exist", value)); diff --git a/src/main/java/org/mitre/synthea/export/flexporter/Actions.java b/src/main/java/org/mitre/synthea/export/flexporter/Actions.java index d9d308aee4..69333145ea 100644 --- a/src/main/java/org/mitre/synthea/export/flexporter/Actions.java +++ b/src/main/java/org/mitre/synthea/export/flexporter/Actions.java @@ -3,7 +3,9 @@ import ca.uhn.fhir.parser.IParser; import java.time.Duration; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.temporal.TemporalAmount; import java.util.ArrayList; import java.util.Collections; @@ -28,6 +30,7 @@ import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.mitre.synthea.engine.State; import org.mitre.synthea.export.FhirR4; @@ -269,9 +272,13 @@ public static void createResource(Bundle bundle, List> resou period.setEndElement(new DateTimeType(new Date(exited))); } dummyEncounter.setPeriod(period); - // TODO: figure out what encounter, if any, was active at this time - // and set the dummy.partOf as a reference to it - // dummyEncounter.setPartOf(new Reference("urn:uuid:" + enc.uuid.toString())); + + Encounter encounterAtThatState = findEncounterAtState(instance, bundle); + if (encounterAtThatState != null) { + dummyEncounter.setPartOf(new Reference("urn:uuid:" + encounterAtThatState.getId())); + dummyEncounter.setParticipant(encounterAtThatState.getParticipant()); + // TODO: copy any other fields over? + } basedOnResources.add(dummyEncounter); } @@ -328,6 +335,7 @@ public static void createResource(Bundle bundle, List> resou BundleEntryComponent newEntry = bundle.addEntry(); newEntry.setResource(createdResource); + newEntry.setFullUrl("urn:uuid:" + createdResource.getId()); if (bundle.getType().equals(BundleType.TRANSACTION)) { BundleEntryRequestComponent request = newEntry.getRequest(); @@ -351,6 +359,59 @@ public static void createResource(Bundle bundle, List> resou } } + /** + * Helper method to find the Encounter that was active as of the given State. + * If the state type is Encounter, returns the Encounter it started. + * May return null if there was no Encounter active when the state was hit, + * or may return surprising results if the encounter was active in a different module. + */ + private static Encounter findEncounterAtState(State state, Bundle bundle) { + Encounter encounterAtThatState = null; + + if (state instanceof State.Encounter) { + String uuid = state.entry.uuid.toString(); + for (BundleEntryComponent entry : bundle.getEntry()) { + Resource r = entry.getResource(); + if (r instanceof Encounter && r.getId().equals(uuid)) { + encounterAtThatState = (Encounter) r; + break; + } + } + + } else if (state.entered != null) { + LocalDateTime stateEntered = + Instant.ofEpochMilli(state.entered).atOffset(ZoneOffset.UTC).toLocalDateTime(); + + for (BundleEntryComponent entry : bundle.getEntry()) { + Resource r = entry.getResource(); + if (r instanceof Encounter) { + Encounter currEnc = (Encounter)r; + Period period = currEnc.getPeriod(); + Date startDt = period.getStart(); + LocalDateTime encStart = startDt.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime(); + + Date endDt = period.getEnd(); + + if (endDt == null) { + // the Encounter is still open + if (stateEntered.isAfter(encStart) || stateEntered.isEqual(encStart)) { + encounterAtThatState = (Encounter) r; + break; + } + } else { + LocalDateTime encEnd = endDt.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime(); + if ((stateEntered.isAfter(encStart) || stateEntered.isEqual(encStart)) + && (stateEntered.isBefore(encEnd) || stateEntered.isEqual(encEnd))) { + encounterAtThatState = (Encounter) r; + break; + } + } + } + } + } + return encounterAtThatState; + } + private static Map createFhirPathMapping(List> fields, Bundle sourceBundle, Resource sourceResource, Person person, diff --git a/src/main/resources/modules/heart/acs_discharge_meds.json b/src/main/resources/modules/heart/acs_discharge_meds.json index 3edbb47157..a5fc773282 100644 --- a/src/main/resources/modules/heart/acs_discharge_meds.json +++ b/src/main/resources/modules/heart/acs_discharge_meds.json @@ -11,14 +11,13 @@ }, "Aspirin Check": { "type": "Simple", - "remarks": ["TODO: fix to Active Allergy after merging"], "complex_transition": [ { "condition": { "condition_type": "Or", "conditions": [ { - "condition_type": "Active Condition", + "condition_type": "Active Allergy", "codes": [ { "system": "RxNorm", @@ -41,7 +40,7 @@ }, "distributions": [ { - "transition": "BB Check", + "transition": "No_Aspirin", "distribution": 1 } ] @@ -53,7 +52,7 @@ "distribution": 0.98 }, { - "transition": "BB Check", + "transition": "No_Aspirin", "distribution": 0.02 } ] @@ -315,6 +314,10 @@ }, "Terminal": { "type": "Terminal" + }, + "No_Aspirin": { + "type": "Simple", + "direct_transition": "BB Check" } }, "gmf_version": 2 diff --git a/src/test/resources/flexporter/qicore_minimal.yaml b/src/test/resources/flexporter/qicore_minimal.yaml index 822469e153..aa5e92c466 100644 --- a/src/test/resources/flexporter/qicore_minimal.yaml +++ b/src/test/resources/flexporter/qicore_minimal.yaml @@ -44,7 +44,6 @@ actions: applicability: Claim - - name: Set Missing Values set_values: - applicability: Immunization @@ -64,6 +63,3 @@ actions: fields: - location: Procedure.extension.where(url='http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-recorded').valueDateTime value: $getField([Procedure.performed]) - - - diff --git a/src/test/resources/flexporter/qicore_withnotdone.yaml b/src/test/resources/flexporter/qicore_withnotdone.yaml new file mode 100644 index 0000000000..2baa2d6956 --- /dev/null +++ b/src/test/resources/flexporter/qicore_withnotdone.yaml @@ -0,0 +1,105 @@ +--- +# name is just a friendly name for this mapping +name: QI Core With "Not Done" Instances + +# applicability determines whether this mapping applies to a given file. +# for now the assumption is 1 file = 1 synthea patient bundle. +applicability: true + +actions: + - name: Apply Profiles + # v1: define specific profiles and an applicability statement on when to apply them + # v1.1: allow specifying a field from the profile to key off of (ex. mCode TNMPrimaryTumorCategory.code) + # maybe v2 will automatically infer? + # some of the challenges to keep in mind: + # - what if the resource doesn't conform to the profile yet? + # we should make sure we can take other actions before applying profiles, + # or manually specify where to apply profiles so that we can apply other fixes based on profile later. + profiles: + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient + applicability: Patient + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter + applicability: Encounter + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition-encounter-diagnosis + applicability: Condition + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observation + applicability: Observation + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedure + applicability: Procedure + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest + applicability: MedicationRequest + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-immunization + applicability: Immunization + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-careplan + applicability: CarePlan + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-imagingstudy + applicability: ImagingStudy + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-device + applicability: Device + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-practitioner + applicability: Practitioner + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-allergyintolerance + applicability: AllergyIntolerance + - profile: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-claim + applicability: Claim + + + - name: Set Missing Values + set_values: + - applicability: Immunization + fields: + - location: Immunization.recorded + value: $getField([Immunization.occurrence]) + # TODO: occurrence is a choice type, + # it would be nice to put "occurrenceDateTime" here + # since that's what's actually in the JSON + # but that doesn't seem to work with HAPI's FhirPath + + - applicability: Procedure.performed.ofType(Period) + fields: + - location: Procedure.extension.where(url='http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-recorded').valueDateTime + value: $getField([Procedure.performed.start]) + - applicability: Procedure.performed.ofType(dateTime) + fields: + - location: Procedure.extension.where(url='http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-recorded').valueDateTime + value: $getField([Procedure.performed]) + + - name: QICore NotDone Instances + create_resource: + - resourceType: MedicationRequest + based_on: + module: Myocardial Infarction + # note the module has to be a top-level module, + # but the state can be a state name from a submodule + state: No_Aspirin + fields: + - location: MedicationRequest.meta.profile + value: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationnotrequested + - location: MedicationRequest.doNotPerform + value: "true" + - location: MedicationRequest.status + value: completed + - location: MedicationRequest.intent + value: order + - location: MedicationRequest.subject.reference + value: $findRef([Patient]) + - location: MedicationRequest.authoredOn + value: $getField([State.entered]) + # State.entered refers to the start of the based_on state + - location: MedicationRequest.requester.reference + value: $getField([Encounter.participant.individual.reference]) + - location: MedicationRequest.reasonCode.coding + value: + system: http://snomedct.io + code: "183966005" + display: "Drug treatment not indicated (situation)" + - location: MedicationRequest.medicationCodeableConcept.extension + value: + url: http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-notDoneValueSet + valueCanonical: http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.196.11.1211 + # VS is "Aspirin and Other Antiplatelets" + + + + +