Skip to content

Commit

Permalink
flexporter enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
dehall committed Dec 7, 2023
1 parent 85673df commit 8ded418
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/main/java/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Check warning on line 212 in src/main/java/App.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/App.java#L212

Added line #L212 was not covered by tests
} else {
throw new FileNotFoundException(String.format(
"Specified flexporter mapping file (%s) does not exist", value));
Expand Down
67 changes: 64 additions & 3 deletions src/main/java/org/mitre/synthea/export/flexporter/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -269,9 +272,13 @@ public static void createResource(Bundle bundle, List<Map<String, Object>> 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());

Check warning on line 279 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L278-L279

Added lines #L278 - L279 were not covered by tests
// TODO: copy any other fields over?
}

basedOnResources.add(dummyEncounter);
}
Expand Down Expand Up @@ -328,6 +335,7 @@ public static void createResource(Bundle bundle, List<Map<String, Object>> resou
BundleEntryComponent newEntry = bundle.addEntry();

newEntry.setResource(createdResource);
newEntry.setFullUrl("urn:uuid:" + createdResource.getId());

if (bundle.getType().equals(BundleType.TRANSACTION)) {
BundleEntryRequestComponent request = newEntry.getRequest();
Expand All @@ -351,6 +359,59 @@ public static void createResource(Bundle bundle, List<Map<String, Object>> 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();

Check warning on line 372 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L372

Added line #L372 was not covered by tests
for (BundleEntryComponent entry : bundle.getEntry()) {
Resource r = entry.getResource();

Check warning on line 374 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L374

Added line #L374 was not covered by tests
if (r instanceof Encounter && r.getId().equals(uuid)) {
encounterAtThatState = (Encounter) r;
break;

Check warning on line 377 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L376-L377

Added lines #L376 - L377 were not covered by tests
}
}

Check warning on line 379 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L379

Added line #L379 was not covered by tests

} 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();

Check warning on line 391 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L388-L391

Added lines #L388 - L391 were not covered by tests

Date endDt = period.getEnd();

Check warning on line 393 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L393

Added line #L393 was not covered by tests

if (endDt == null) {
// the Encounter is still open
if (stateEntered.isAfter(encStart) || stateEntered.isEqual(encStart)) {
encounterAtThatState = (Encounter) r;
break;

Check warning on line 399 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L398-L399

Added lines #L398 - L399 were not covered by tests
}
} else {
LocalDateTime encEnd = endDt.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime();

Check warning on line 402 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L402

Added line #L402 was not covered by tests
if ((stateEntered.isAfter(encStart) || stateEntered.isEqual(encStart))
&& (stateEntered.isBefore(encEnd) || stateEntered.isEqual(encEnd))) {
encounterAtThatState = (Encounter) r;
break;

Check warning on line 406 in src/main/java/org/mitre/synthea/export/flexporter/Actions.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/mitre/synthea/export/flexporter/Actions.java#L405-L406

Added lines #L405 - L406 were not covered by tests
}
}
}
}
}
return encounterAtThatState;
}


private static Map<String, Object> createFhirPathMapping(List<Map<String, Object>> fields,
Bundle sourceBundle, Resource sourceResource, Person person,
Expand Down
11 changes: 7 additions & 4 deletions src/main/resources/modules/heart/acs_discharge_meds.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -41,7 +40,7 @@
},
"distributions": [
{
"transition": "BB Check",
"transition": "No_Aspirin",
"distribution": 1
}
]
Expand All @@ -53,7 +52,7 @@
"distribution": 0.98
},
{
"transition": "BB Check",
"transition": "No_Aspirin",
"distribution": 0.02
}
]
Expand Down Expand Up @@ -315,6 +314,10 @@
},
"Terminal": {
"type": "Terminal"
},
"No_Aspirin": {
"type": "Simple",
"direct_transition": "BB Check"
}
},
"gmf_version": 2
Expand Down
4 changes: 0 additions & 4 deletions src/test/resources/flexporter/qicore_minimal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ actions:
applicability: Claim



- name: Set Missing Values
set_values:
- applicability: Immunization
Expand All @@ -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])



105 changes: 105 additions & 0 deletions src/test/resources/flexporter/qicore_withnotdone.yaml
Original file line number Diff line number Diff line change
@@ -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"





0 comments on commit 8ded418

Please sign in to comment.