diff --git a/src/main/java/App.java b/src/main/java/App.java index 423d9e2b90..a876e6df48 100644 --- a/src/main/java/App.java +++ b/src/main/java/App.java @@ -104,6 +104,7 @@ public static void main(String[] args) throws Exception { } else if (currArg.equalsIgnoreCase("-p")) { String value = argsQ.poll(); options.population = Integer.parseInt(value); + Config.set("generate.default_population", value); } else if (currArg.equalsIgnoreCase("-o")) { String value = argsQ.poll(); options.overflow = Boolean.parseBoolean(value); @@ -132,10 +133,7 @@ public static void main(String[] args) throws Exception { String value = argsQ.poll(); File configFile = new File(value); Config.load(configFile); - // Any options that are automatically set by reading the configuration - // file during options initialization need to be reset here. - options.population = Config.getAsInteger("generate.default_population", 1); - options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1); + resetOptionsFromConfig(options); } else if (currArg.equalsIgnoreCase("-d")) { String value = argsQ.poll(); File localModuleDir = new File(value); @@ -252,6 +250,7 @@ public static void main(String[] args) throws Exception { } Config.set(configSetting, value); + resetOptionsFromConfig(options); } else if (options.state == null) { options.state = currArg; } else { @@ -271,6 +270,13 @@ public static void main(String[] args) throws Exception { generator.run(); } } + + private static void resetOptionsFromConfig(Generator.GeneratorOptions options) { + // Any options that are automatically set by reading the configuration + // file during options initialization need to be reset here. + options.population = Config.getAsInteger("generate.default_population", 1); + options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1); + } private static boolean validateConfig(Generator.GeneratorOptions options, boolean overrideFutureDateError) { 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 f980585e12..f3d930fe15 100644 --- a/src/main/java/org/mitre/synthea/export/flexporter/Actions.java +++ b/src/main/java/org/mitre/synthea/export/flexporter/Actions.java @@ -649,13 +649,9 @@ private static void dateFilter(Bundle bundle, String minDateStr, String maxDateS * Cascade (current), Delete reference field but leave object, Do nothing * * @param bundle FHIR Bundle to filter - * @param list List of resource types to delete, other types not listed will be kept + * @param list List of resource types or FHIRPath to delete, other types not listed will be kept */ public static void deleteResources(Bundle bundle, List list) { - // TODO: make this FHIRPath instead of just straight resource types - - Set resourceTypesToDelete = new HashSet<>(list); - Set deletedResourceIDs = new HashSet<>(); Iterator itr = bundle.getEntry().iterator(); @@ -665,10 +661,15 @@ public static void deleteResources(Bundle bundle, List list) { Resource resource = entry.getResource(); String resourceType = resource.getResourceType().toString(); - if (resourceTypesToDelete.contains(resourceType)) { - deletedResourceIDs.add(resource.getId()); - itr.remove(); + + for (String applicability : list) { + if (applicability.equals(resourceType) || FhirPathUtils.appliesToResource(resource, applicability)) { + deletedResourceIDs.add(resource.getId()); + itr.remove(); + break; + } } + } if (!deletedResourceIDs.isEmpty()) { diff --git a/src/main/java/org/mitre/synthea/modules/OphthalmicNoteModule.java b/src/main/java/org/mitre/synthea/modules/OphthalmicNoteModule.java index e8ed16e384..dc1bc2ca29 100644 --- a/src/main/java/org/mitre/synthea/modules/OphthalmicNoteModule.java +++ b/src/main/java/org/mitre/synthea/modules/OphthalmicNoteModule.java @@ -1,12 +1,15 @@ package org.mitre.synthea.modules; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import org.mitre.synthea.engine.Module; import org.mitre.synthea.export.ExportHelper; import org.mitre.synthea.world.agents.Person; import org.mitre.synthea.world.concepts.HealthRecord.Encounter; import org.mitre.synthea.world.concepts.HealthRecord.Observation; +import org.mitre.synthea.world.concepts.HealthRecord.Procedure; public class OphthalmicNoteModule extends Module { @@ -48,6 +51,8 @@ public boolean process(Person person, long time) { } else { encounterNote.append("Followup exam with ") .append(person.attributes.get(Person.NAME)) + .append(' ') + .append(ExportHelper.dateFromTimestamp(time)) .append('\n'); firstObservedDrStage = (Integer) person.attributes.get("first_observed_dr_stage"); @@ -56,34 +61,157 @@ public boolean process(Person person, long time) { HashMap drStageFirstObservedDate = (HashMap) person.attributes.get("dr_stage_to_first_observed_date"); if (drStageFirstObservedDate == null) { drStageFirstObservedDate = new HashMap<>(); + person.attributes.put("dr_stage_to_first_observed_date", drStageFirstObservedDate); } + boolean stable = true; if (!drStageFirstObservedDate.containsKey(drStage)) { drStageFirstObservedDate.put(drStage, currEncounter.start); + stable = false; } if (drStage == 0) { encounterNote.append("No current signs of diabetic retinopathy, both eyes\n"); - } else if (firstObservedDrStage > 0) { - encounterNote.append(STAGES[firstObservedDrStage]).append(" OU\n"); + } else { + boolean first = true; + for (int i = 1 ; i <= 4 ; i++) { + Long observed = drStageFirstObservedDate.get(i); + if (observed != null) { + if (first) { + encounterNote.append(STAGES[i]).append(" OU ").append(ExportHelper.dateFromTimestamp(observed)).append('\n'); + first = false; + } else { + encounterNote.append("Progressed to " + STAGES[i] + " " + ExportHelper.dateFromTimestamp(observed)).append('\n'); + } + } + } + } + + Procedure panRetinalLaser = (Procedure) person.attributes.get("panretinal_laser"); + List gridLaserHistory = (List)person.attributes.get("grid_laser_history"); + if (gridLaserHistory == null) { + gridLaserHistory = new ArrayList<>(); + person.attributes.put("grid_laser_history", gridLaserHistory); + } + + if (panRetinalLaser != null || !gridLaserHistory.isEmpty()) { + encounterNote.append("Procedure History:\n"); + if (panRetinalLaser != null) { + encounterNote.append("Panretinal laser ").append(ExportHelper.dateFromTimestamp(panRetinalLaser.start)).append('\n'); + } else { + encounterNote.append("Grid laser"); + for (Procedure g : gridLaserHistory) { + encounterNote.append(", ").append(ExportHelper.dateFromTimestamp(g.start)); + } + encounterNote.append('\n'); + } + } + + double va = (double) person.attributes.get("visual_acuity_logmar"); + // logmar to 20/x = 20*10^(logmar) + long denom = Math.round(20 * Math.pow(10, va)); + encounterNote.append("Visual Acuity: 20/").append(denom).append(" OD, 20/").append(denom).append(" OS\n"); + + int iop = (int) person.attributes.get("intraocular_pressure"); + encounterNote.append("Intraocular Pressure (IOP): ").append(iop).append(" mmHg OD, ").append(iop).append(" mmHg OS\n"); + + // an example from chatGPT + encounterNote.append("Pupils: Equal, round, reactive to light and accommodation\n"); + encounterNote.append("Extraocular Movements: Full and smooth\n"); + encounterNote.append("Confrontation Visual Fields: Full to finger count OU\n"); + encounterNote.append("Anterior Segment: Normal lids, lashes, and conjunctiva OU. Cornea clear OU. Anterior chamber deep and quiet OU. Iris normal architecture OU. Lens clear OU.\n"); + encounterNote.append("Dilated Fundus Examination:\n"); + encounterNote.append("Optic Disc: Pink, well-defined margins, cup-to-disc ratio 0.3 OU\n"); + + boolean edema = (boolean) person.attributes.getOrDefault("macular_edema", false); + + if (edema) { + // TODO encounterNote.append("Macula: Flat, no edema or exudates OU\n"); + } else { + encounterNote.append("Macula: Flat, no edema or exudates OU\n"); } - if (drStage > firstObservedDrStage) { -// encounterNote.append("Progressed to " ) + encounterNote.append("Vessels: Attenuated arterioles with some copper wiring changes OU. No neovascularization noted.\n"); + encounterNote.append("Periphery: No tears, holes, or detachments OU"); + + Procedure firstAntiVEGF = (Procedure)person.attributes.get("first_anti_vegf"); + + for (Procedure p : currEncounter.procedures) { + switch (p.type) { + // primary code + + // visual acuity + case "16830007": + break; + + // IOP + case "252832004": + break; + + // Slit-lamp biomicroscopy + case "55468007": + break; + + // Gonioscopy + case "389153003": + break; + + // Fundoscopy + case "314971001": + break; + + // Examination of the peripheral retina and vitreous + case "722161008": + break; + + // OCT + case "700070005": + break; + + // Panretinal laser + case "413180006": + person.attributes.put("panretinal_laser", p); + break; + + // Grid laser + case "397539000": + gridLaserHistory.add(p); + break; + + // Anti VEGF + case "1004045004": + if (firstAntiVEGF == null) { + + firstAntiVEGF = p; + person.attributes.put("first_anti_vegf", firstAntiVEGF); + } + break; + + } } Observation hba1c = person.record.getLatestObservation("4548-4"); if (((Double)hba1c.value) > 6.5 && person.rand() > .7) { if (drStage == 0) { - encounterNote.append("Discussed the nature of DM and its potential effects on the eye in detail with the patient. Discussed the importance of maintaining strict glycemic control to avoid developing retinopathy."); + encounterNote.append("Discussed the nature of DM and its potential effects on the eye in detail with the patient. Discussed the importance of maintaining strict glycemic control to avoid developing retinopathy.\n"); } else { - encounterNote.append("I discussed the effects of elevated glucose on the eye, and the importance of strict control in preventing progression of retinopathy."); + encounterNote.append("I discussed the effects of elevated glucose on the eye, and the importance of strict control in preventing progression of retinopathy.\n"); } } + + if (prevEncounter == null) { + encounterNote.append("Discussed the signs of vision changes that require immediate medical attention.\n"); + } + + // TODO: line about follow-up currEncounter.note = encounterNote.toString(); +// System.out.println(currEncounter.note); +// System.out.println("\n"); + + person.attributes.put("previous_ophthalmic_encounter", currEncounter); // note return options here, see State$CallSubmodule diff --git a/src/main/resources/modules/diabetic_retinopathy_treatment.json b/src/main/resources/modules/diabetic_retinopathy_treatment.json index 983a45798d..442f39a744 100644 --- a/src/main/resources/modules/diabetic_retinopathy_treatment.json +++ b/src/main/resources/modules/diabetic_retinopathy_treatment.json @@ -410,7 +410,7 @@ "codes": [ { "system": "SNOMED-CT", - "code": 389153003, + "code": 413180006, "display": "Pan retinal photocoagulation for diabetes (procedure)" } ], @@ -488,7 +488,7 @@ "display": "proparacaine hydrochloride 5 MG/ML Ophthalmic Solution" } ], - "direct_transition": "Panretinal Laser", + "direct_transition": "End_Drops_Panretinal", "administration": true }, "Numbing_Drops_Grid_Laser": { @@ -500,7 +500,7 @@ "display": "proparacaine hydrochloride 5 MG/ML Ophthalmic Solution" } ], - "direct_transition": "Grid_Laser", + "direct_transition": "End_Drops_Grid", "administration": true }, "Numbing_Drops_AntiVEGF_Injection": { @@ -512,7 +512,7 @@ "display": "proparacaine hydrochloride 5 MG/ML Ophthalmic Solution" } ], - "direct_transition": "AntiVEGF_Therapy_Med", + "direct_transition": "End_Drops_AntiVEGF", "administration": true }, "Dilation": { @@ -524,7 +524,8 @@ "display": "tropicamide 5 MG/ML Ophthalmic Solution" } ], - "direct_transition": "Gonioscopy" + "direct_transition": "End_Dilation_Med", + "administration": true }, "Guard_for_Vision_Impact": { "type": "Guard", @@ -607,6 +608,26 @@ } ], "direct_transition": "Progress_DR_Vitals" + }, + "End_Drops_Panretinal": { + "type": "MedicationEnd", + "direct_transition": "Panretinal Laser", + "medication_order": "Numbing_Drops_Panretinal_Laser" + }, + "End_Drops_AntiVEGF": { + "type": "MedicationEnd", + "direct_transition": "AntiVEGF_Therapy_Med", + "medication_order": "Numbing_Drops_AntiVEGF_Injection" + }, + "End_Drops_Grid": { + "type": "MedicationEnd", + "direct_transition": "Grid_Laser", + "medication_order": "Numbing_Drops_Grid_Laser" + }, + "End_Dilation_Med": { + "type": "MedicationEnd", + "direct_transition": "Gonioscopy", + "medication_order": "Dilation" } }, "gmf_version": 2 diff --git a/src/main/resources/modules/eye/intraocular_pressure.json b/src/main/resources/modules/eye/intraocular_pressure.json index 02989c8288..41701e20cf 100644 --- a/src/main/resources/modules/eye/intraocular_pressure.json +++ b/src/main/resources/modules/eye/intraocular_pressure.json @@ -27,29 +27,7 @@ } }, "unit": "minutes", - "direct_transition": "IOP_Results" - }, - "IOP_Results": { - "type": "Observation", - "category": "vital-signs", - "unit": "mm[Hg]", - "codes": [ - { - "system": "SNOMED-CT", - "code": "41633001", - "display": "Intraocular pressure (observable entity)" - }, - { - "system": "LOINC", - "code": "56844-4", - "display": "Intraocular pressure of Eye" - } - ], - "direct_transition": "High IOP Meds", - "remarks": [ - "Note code 1 is actually SNOMED" - ], - "attribute": "intraocular_pressure" + "direct_transition": "IOP_Results_Left" }, "Timolol": { "type": "MedicationOrder", @@ -119,6 +97,50 @@ "type": "SetAttribute", "attribute": "eye_pressure_med", "direct_transition": "Terminal" + }, + "IOP_Results_Left": { + "type": "Observation", + "category": "vital-signs", + "unit": "mm[Hg]", + "codes": [ + { + "system": "SNOMED-CT", + "code": "41633001", + "display": "Intraocular pressure (observable entity)" + }, + { + "system": "LOINC", + "code": "79893-4", + "display": "Left eye Intraocular pressure" + } + ], + "direct_transition": "IOP_Results_Right", + "remarks": [ + "Note code 1 is actually SNOMED" + ], + "attribute": "intraocular_pressure" + }, + "IOP_Results_Right": { + "type": "Observation", + "category": "vital-signs", + "unit": "mm[Hg]", + "codes": [ + { + "system": "SNOMED-CT", + "code": "41633001", + "display": "Intraocular pressure (observable entity)" + }, + { + "system": "LOINC", + "code": "79892-6", + "display": "Right eye Intraocular pressure" + } + ], + "direct_transition": "High IOP Meds", + "remarks": [ + "Note code 1 is actually SNOMED" + ], + "attribute": "intraocular_pressure" } }, "gmf_version": 2 diff --git a/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java b/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java index b28b93a3e8..ba098c2094 100644 --- a/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java +++ b/src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java @@ -502,6 +502,25 @@ public void testDeleteResources() throws Exception { assertEquals(0, countProvenance); } + + @Test + public void testDeleteResourcesByFhirPath() throws Exception { + Bundle b = new Bundle(); + + MedicationRequest m = new MedicationRequest(); + CodeableConcept cc = new CodeableConcept(); + cc.addCoding().setCode("1191013"); + m.setMedication(cc); + b.addEntry().setResource(m); + Map action = getActionByName("testDeleteResourcesByFhirPath"); + + Actions.applyAction(b, action, null, null); + + long countProvenance = b.getEntry().stream() + .filter(bec -> bec.getResource().getResourceType() == ResourceType.MedicationRequest).count(); + + assertEquals(0, countProvenance); + } @Test public void testDeleteResourcesCascade() throws Exception { diff --git a/src/test/resources/flexporter/eyes_on_fhir.yaml b/src/test/resources/flexporter/eyes_on_fhir.yaml index 8fdb35fcd8..beb53cb914 100644 --- a/src/test/resources/flexporter/eyes_on_fhir.yaml +++ b/src/test/resources/flexporter/eyes_on_fhir.yaml @@ -15,6 +15,17 @@ actions: - profile: http://hl7.org/fhir/uv/eyecare/StructureDefinition/diagnostic-report-oct-macula applicability: DiagnosticReport.code.coding.where($this.code = '57119-0') + + - name: Delete MedicationRequests for drops + # numbing and dilation drops used in the clinic aren't prescribed. + # delete the MedicationRequests and just leave the MedicationAdministrations + delete_resources: + # tropicamide / dilation drops + - MedicationRequest.medication.coding.where($this.code = '313521') + # proparacaine / numbing drops + - MedicationRequest.medication.coding.where($this.code = '1191013') + + - name: Set Missing Values set_values: - applicability: Observation.code.coding.where($this.code = '422673001') diff --git a/src/test/resources/flexporter/test_mapping.yaml b/src/test/resources/flexporter/test_mapping.yaml index 470de64a5e..87394cda33 100644 --- a/src/test/resources/flexporter/test_mapping.yaml +++ b/src/test/resources/flexporter/test_mapping.yaml @@ -301,6 +301,10 @@ actions: delete_resources: - Provenance + - name: testDeleteResourcesByFhirPath + delete_resources: + - MedicationRequest.medication.coding.where($this.code = '1191013') + - name: testDeleteResourcesCascade delete_resources: - Patient \ No newline at end of file