-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #49 from DBCG/fix-48-source-data-measure-evaluation
#48 - source data measure evaluation
- Loading branch information
Showing
7 changed files
with
903 additions
and
135 deletions.
There are no files selected for viewing
159 changes: 159 additions & 0 deletions
159
src/main/java/org/opencds/cqf/evaluation/MeasureEvaluation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package org.opencds.cqf.evaluation; | ||
|
||
import ca.uhn.fhir.jpa.dao.SearchParameterMap; | ||
import ca.uhn.fhir.rest.api.server.IBundleProvider; | ||
import ca.uhn.fhir.rest.param.ReferenceParam; | ||
import org.hl7.fhir.dstu3.model.*; | ||
import org.hl7.fhir.instance.model.api.IBaseResource; | ||
import org.opencds.cqf.builders.MeasureReportBuilder; | ||
import org.opencds.cqf.cql.data.DataProvider; | ||
import org.opencds.cqf.cql.execution.Context; | ||
import org.opencds.cqf.cql.runtime.Interval; | ||
import org.opencds.cqf.helpers.FhirMeasureBundler; | ||
import org.opencds.cqf.providers.JpaDataProvider; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.*; | ||
|
||
public class MeasureEvaluation { | ||
|
||
private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluation.class); | ||
|
||
private DataProvider provider; | ||
private Interval measurementPeriod; | ||
|
||
public MeasureEvaluation(DataProvider provider, Interval measurementPeriod) { | ||
this.provider = provider; | ||
this.measurementPeriod = measurementPeriod; | ||
} | ||
|
||
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId) { | ||
logger.info("Generating individual report"); | ||
|
||
if (patientId == null) { | ||
return evaluatePopulationMeasure(measure, context); | ||
} | ||
|
||
Iterable<Object> patientRetrieve = provider.retrieve("Patient", patientId, "Patient", null, null, null, null, null, null, null, null); | ||
Patient patient = null; | ||
if (patientRetrieve.iterator().hasNext()) { | ||
patient = (Patient) patientRetrieve.iterator().next(); | ||
} | ||
|
||
return evaluate(measure, context, patient == null ? Collections.emptyList() : Collections.singletonList(patient), MeasureReport.MeasureReportType.INDIVIDUAL); | ||
} | ||
|
||
public MeasureReport evaluatePatientListMeasure(Measure measure, Context context, String practitionerRef) | ||
{ | ||
logger.info("Generating patient-list report"); | ||
|
||
List<Patient> patients = new ArrayList<>(); | ||
if (practitionerRef != null) { | ||
SearchParameterMap map = new SearchParameterMap(); | ||
map.add( | ||
"general-practitioner", | ||
new ReferenceParam( | ||
practitionerRef.startsWith("Practitioner/") | ||
? practitionerRef | ||
: "Practitioner/" + practitionerRef | ||
) | ||
); | ||
|
||
if (provider instanceof JpaDataProvider) { | ||
IBundleProvider patientProvider = ((JpaDataProvider) provider).resolveResourceProvider("Patient").getDao().search(map); | ||
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size()); | ||
patientList.forEach(x -> patients.add((Patient) x)); | ||
} | ||
} | ||
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.PATIENTLIST); | ||
} | ||
|
||
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context) { | ||
logger.info("Generating summary report"); | ||
|
||
List<Patient> patients = new ArrayList<>(); | ||
if (provider instanceof JpaDataProvider) { | ||
IBundleProvider patientProvider = ((JpaDataProvider) provider).resolveResourceProvider("Patient").getDao().search(new SearchParameterMap()); | ||
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size()); | ||
patientList.forEach(x -> patients.add((Patient) x)); | ||
} | ||
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.SUMMARY); | ||
} | ||
|
||
private MeasureReport evaluate(Measure measure, Context context, List<Patient> patients, MeasureReport.MeasureReportType type) | ||
{ | ||
MeasureReportBuilder reportBuilder = new MeasureReportBuilder(); | ||
reportBuilder.buildStatus("complete"); | ||
reportBuilder.buildType(type); | ||
reportBuilder.buildMeasureReference(measure.getIdElement().getValue()); | ||
if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) { | ||
reportBuilder.buildPatientReference(patients.get(0).getIdElement().getValue()); | ||
} | ||
reportBuilder.buildPeriod(measurementPeriod); | ||
|
||
MeasureReport report = reportBuilder.build(); | ||
|
||
List<Patient> initialPopulation = getInitalPopulation(measure, patients, context); | ||
HashMap<String,Resource> resources = new HashMap<>(); | ||
|
||
for (Measure.MeasureGroupComponent group : measure.getGroup()) { | ||
MeasureReport.MeasureReportGroupComponent reportGroup = new MeasureReport.MeasureReportGroupComponent(); | ||
reportGroup.setIdentifier(group.getIdentifier()); | ||
report.getGroup().add(reportGroup); | ||
|
||
for (Measure.MeasureGroupPopulationComponent pop : group.getPopulation()) { | ||
int count = 0; | ||
// Worried about performance here with big populations... | ||
for (Patient patient : initialPopulation) { | ||
context.setContextValue("Patient", patient.getIdElement().getIdPart()); | ||
Object result = context.resolveExpressionRef(pop.getCriteria()).evaluate(context); | ||
if (result instanceof Boolean) { | ||
count += (Boolean) result ? 1 : 0; | ||
} | ||
else if (result instanceof Iterable) { | ||
for (Object item : (Iterable) result) { | ||
count++; | ||
if (item instanceof Resource) { | ||
resources.put(((Resource) item).getId(), (Resource) item); | ||
} | ||
} | ||
} | ||
} | ||
MeasureReport.MeasureReportGroupPopulationComponent populationReport = new MeasureReport.MeasureReportGroupPopulationComponent(); | ||
populationReport.setCount(count); | ||
populationReport.setCode(pop.getCode()); | ||
populationReport.setIdentifier(pop.getIdentifier()); | ||
reportGroup.getPopulation().add(populationReport); | ||
} | ||
} | ||
|
||
FhirMeasureBundler bundler = new FhirMeasureBundler(); | ||
org.hl7.fhir.dstu3.model.Bundle evaluatedResources = bundler.bundle(resources.values()); | ||
evaluatedResources.setId(UUID.randomUUID().toString()); | ||
report.setEvaluatedResources(new Reference('#' + evaluatedResources.getId())); | ||
report.addContained(evaluatedResources); | ||
return report; | ||
} | ||
|
||
private List<Patient> getInitalPopulation(Measure measure, List<Patient> population, Context context) { | ||
List<Patient> initalPop = new ArrayList<>(); | ||
for (Measure.MeasureGroupComponent group : measure.getGroup()) { | ||
for (Measure.MeasureGroupPopulationComponent pop : group.getPopulation()) { | ||
if (pop.getCode().getCodingFirstRep().getCode().equals("initial-population")) { | ||
for (Patient patient : population) { | ||
context.setContextValue("Patient", patient.getIdElement().getIdPart()); | ||
Object result = context.resolveExpressionRef(pop.getCriteria()).evaluate(context); | ||
if (result == null) { | ||
continue; | ||
} | ||
if ((Boolean) result) { | ||
initalPop.add(patient); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return initalPop; | ||
} | ||
} |
210 changes: 210 additions & 0 deletions
210
src/main/java/org/opencds/cqf/helpers/DataProviderHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
package org.opencds.cqf.helpers; | ||
|
||
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; | ||
import ca.uhn.fhir.model.dstu2.composite.CodingDt; | ||
import ca.uhn.fhir.model.dstu2.composite.PeriodDt; | ||
import ca.uhn.fhir.model.primitive.CodeDt; | ||
import ca.uhn.fhir.model.primitive.DateDt; | ||
import ca.uhn.fhir.model.primitive.DateTimeDt; | ||
import ca.uhn.fhir.model.primitive.InstantDt; | ||
import org.hl7.fhir.dstu3.model.*; | ||
import org.opencds.cqf.cql.runtime.Code; | ||
import org.opencds.cqf.cql.runtime.DateTime; | ||
import org.opencds.cqf.cql.runtime.Interval; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Date; | ||
import java.util.List; | ||
|
||
public class DataProviderHelper { | ||
// Get lowest precision from list of date objects | ||
public static String getPrecision(List<Object> dateObjects) { | ||
int precision = 7; | ||
for (Object dateObject : dateObjects) { | ||
if (dateObject instanceof Interval) { | ||
DateTime start = (DateTime) ((Interval) dateObject).getStart(); | ||
DateTime end = (DateTime) ((Interval) dateObject).getEnd(); | ||
|
||
if (start != null) { | ||
if (precision > start.getPartial().size()) { | ||
precision = start.getPartial().size(); | ||
} | ||
} | ||
if (end != null) { | ||
if (precision > end.getPartial().size()) { | ||
precision = end.getPartial().size(); | ||
} | ||
} | ||
} else { | ||
precision = ((DateTime) dateObject).getPartial().size(); | ||
} | ||
} | ||
|
||
switch (precision) { | ||
case 1: return "year"; | ||
case 2: return "month"; | ||
case 3: return "day"; | ||
case 4: return "hour"; | ||
case 5: return "minute"; | ||
case 6: return "second"; | ||
default: return "millisecond"; | ||
} | ||
} | ||
|
||
public static Object getDstu2DateTime(Object dateObject) { | ||
if (dateObject instanceof DateDt) { | ||
return DateTime.fromJavaDate(((DateDt) dateObject).getValue()); | ||
} else if (dateObject instanceof DateTimeDt) { | ||
return DateTime.fromJavaDate(((DateTimeDt) dateObject).getValue()); | ||
} else if (dateObject instanceof InstantDt) { | ||
return DateTime.fromJavaDate(((InstantDt) dateObject).getValue()); | ||
} else if (dateObject instanceof PeriodDt) { | ||
return new Interval( | ||
DateTime.fromJavaDate(((PeriodDt) dateObject).getStart()), true, | ||
DateTime.fromJavaDate(((PeriodDt) dateObject).getEnd()), true | ||
); | ||
} | ||
return dateObject; | ||
} | ||
|
||
public static Object getDstu2Code(Object codeObject) { | ||
if (codeObject instanceof CodeDt) { | ||
return ((CodeDt) codeObject).getValue(); | ||
} else if (codeObject instanceof CodingDt) { | ||
return new Code().withSystem(((CodingDt) codeObject).getSystem()).withCode(((CodingDt) codeObject).getCode()); | ||
} | ||
else if (codeObject instanceof CodeableConceptDt) { | ||
List<Code> codes = new ArrayList<>(); | ||
for (CodingDt coding : ((CodeableConceptDt) codeObject).getCoding()) { | ||
codes.add((Code) getDstu2Code(coding)); | ||
} | ||
return codes; | ||
} | ||
return codeObject; | ||
} | ||
|
||
public static Object getStu3DateTime(Object dateObject) { | ||
if (dateObject instanceof Date) { | ||
return DateTime.fromJavaDate((Date) dateObject); | ||
} else if (dateObject instanceof DateTimeType) { | ||
return DateTime.fromJavaDate(((DateTimeType) dateObject).getValue()); | ||
} else if (dateObject instanceof InstantType) { | ||
return DateTime.fromJavaDate(((InstantType) dateObject).getValue()); | ||
} else if (dateObject instanceof Period) { | ||
return new Interval( | ||
DateTime.fromJavaDate(((Period) dateObject).getStart()), true, | ||
DateTime.fromJavaDate(((Period) dateObject).getEnd()), true | ||
); | ||
} | ||
return dateObject; | ||
} | ||
|
||
public static Object getStu3Code(Object codeObject) { | ||
if (codeObject instanceof CodeType) { | ||
return ((CodeType) codeObject).getValue(); | ||
} else if (codeObject instanceof Coding) { | ||
return new Code().withSystem(((Coding) codeObject).getSystem()).withCode(((Coding) codeObject).getCode()); | ||
} | ||
else if (codeObject instanceof CodeableConcept) { | ||
List<Code> codes = new ArrayList<>(); | ||
for (Coding coding : ((CodeableConcept) codeObject).getCoding()) { | ||
codes.add((Code) getStu3Code(coding)); | ||
} | ||
return codes; | ||
} | ||
return codeObject; | ||
} | ||
|
||
public static boolean checkCodeMembership(Iterable<Code> codes, Object codeObject) { | ||
// for now, just checking whether code values are equal... TODO - add intelligent checks for system and version | ||
for (Code code : codes) { | ||
if (codeObject instanceof String && code.getCode().equals(codeObject)) { | ||
return true; | ||
} | ||
else if (codeObject instanceof Code && code.getCode().equals(((Code) codeObject).getCode())) { | ||
return true; | ||
} | ||
else if (codeObject instanceof Iterable) { | ||
for (Object obj : (Iterable) codeObject) { | ||
if (code.getCode().equals(((Code) obj).getCode())) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
public static String convertStu3PathToSearchParam(String type, String path) { | ||
path = path.replace(".value", ""); | ||
switch (type) { | ||
case "AllergyIntolerance": | ||
if (path.equals("clinicalStatus")) return "clinical-status"; | ||
else if (path.contains("substance")) return "code"; | ||
else if (path.equals("assertedDate")) return "date"; | ||
else if (path.equals("lastOccurrence")) return "last-date"; | ||
else if (path.startsWith("reaction")) { | ||
if (path.endsWith("manifestation")) return "manifestation"; | ||
else if (path.endsWith("onset")) return "onset"; | ||
else if (path.endsWith("exposureRoute")) return "route"; | ||
else if (path.endsWith("severity")) return "severity"; | ||
} | ||
else if (path.equals("verificationStatus")) return "verification-status"; | ||
break; | ||
case "Claim": | ||
if (path.contains("careTeam")) return "care-team"; | ||
else if (path.contains("payee")) return "payee"; | ||
break; | ||
case "Condition": | ||
if (path.equals("abatementDateTime")) return "abatement-date"; | ||
else if (path.equals("abatementPeriod")) return "abatement-date"; | ||
else if (path.equals("abatementRange")) return "abatement-age"; | ||
else if (path.equals("onsetDateTime")) return "onset-date"; | ||
else if (path.equals("onsetPeriod")) return "onset-date"; | ||
else if (path.equals("onsetRange")) return "onset-age"; | ||
break; | ||
case "MedicationRequest": | ||
if (path.equals("authoredOn")) return "authoredon"; | ||
else if (path.equals("medicationCodeableConcept")) return "code"; | ||
else if (path.equals("medicationReference")) return "medication"; | ||
else if (path.contains("event")) return "date"; | ||
else if (path.contains("performer")) return "intended-dispenser"; | ||
else if (path.contains("requester")) return "requester"; | ||
break; | ||
case "NutritionOrder": | ||
if (path.contains("additiveType")) return "additive"; | ||
else if (path.equals("dateTime")) return "datetime"; | ||
else if (path.contains("baseFormulaType")) return "formula"; | ||
else if (path.contains("oralDiet")) return "oraldiet"; | ||
else if (path.equals("orderer")) return "provider"; | ||
else if (path.contains("supplement")) return "supplement"; | ||
break; | ||
case "ProcedureRequest": | ||
if (path.equals("authoredOn")) return "authored"; | ||
else if (path.equals("basedOn")) return "based-on"; | ||
else if (path.equals("bodySite")) return "body-site"; | ||
else if (path.equals("context")) return "encounter"; | ||
else if (path.equals("performerType")) return "performer-type"; | ||
else if (path.contains("requester")) return "requester"; | ||
break; | ||
case "ReferralRequest": | ||
if (path.equals("authoredOn")) return "authored"; | ||
else if (path.equals("basedOn")) return "based-on"; | ||
else if (path.equals("context")) return "encounter"; | ||
else if (path.equals("groupIdentifier")) return "group-identifier"; | ||
else if (path.equals("occurrence")) return "occurrence-date"; | ||
else if (path.contains("requester")) return "requester"; | ||
else if (path.equals("serviceRequested")) return "service"; | ||
break; | ||
case "VisionPrescription": | ||
if (path.equals("dateWritten")) return "datewritten"; | ||
break; | ||
default: | ||
if (path.startsWith("effective")) return "date"; | ||
else if (path.equals("period")) return "date"; | ||
else if (path.equals("vaccineCode")) return "vaccine-code"; | ||
break; | ||
} | ||
return path.replace('.', '-').toLowerCase(); | ||
} | ||
} |
Oops, something went wrong.