3724 cql mmr (#3732)

* #3724 added test and test resources

* #3724 adding summary report by one practitioner

* #3724 fixed some formatting and data type mistakes

* #3728 passing on practitioner parameter

* #3724 added test and test resources

* #3724 adding summary report by one practitioner

* #3724 fixed some formatting and data type mistakes

* #3724 added tests for measure by practitioner

* #3724 minor formatting and refactoring

* #3724 deleted unused cql files

* #3724 added TODO

* #3724 adding comment and changelog

Co-authored-by: Anna <anna@MacBook-Pro.local>
This commit is contained in:
alackerbauer 2022-06-30 09:10:55 +02:00 committed by GitHub
parent 3068eb38c4
commit 117c1d0891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1287 additions and 8 deletions

View File

@ -0,0 +1,4 @@
---
type: add
issue: 3724
title: "Adding some test cases for CQL measures on immunization as well as some testing methods to support easier changes of CQL in unit tests."

View File

@ -79,11 +79,11 @@ public class MeasureEvaluation {
this.measurementPeriod = measurementPeriod;
}
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId, RequestDetails theRequestDetails) {
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId, String thePractitionerRef, RequestDetails theRequestDetails) {
logger.info("Generating individual report");
if (patientId == null) {
return evaluatePopulationMeasure(measure, context, theRequestDetails);
return evaluatePopulationMeasure(measure, context, thePractitionerRef, theRequestDetails);
}
Iterable<Object> patientRetrieve = provider.retrieve("Patient", "id", patientId, "Patient", null, null, null,
@ -126,11 +126,11 @@ public class MeasureEvaluation {
return patients;
}
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context, RequestDetails theRequestDetails) {
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context, String thePractitionerRef, RequestDetails theRequestDetails) {
logger.info("Generating summary report");
List<Patient> patients = thePractitionerRef == null ? getAllPatients(theRequestDetails) : getPractitionerPatients(thePractitionerRef, theRequestDetails);
boolean isSingle = false;
return evaluate(measure, context, getAllPatients(theRequestDetails), MeasureReport.MeasureReportType.SUMMARY, isSingle);
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.SUMMARY, isSingle);
}
@SuppressWarnings("unchecked")

View File

@ -104,18 +104,18 @@ public class MeasureOperationsProvider {
if (reportType != null) {
switch (reportType) {
case "patient":
return evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef, theRequestDetails);
return evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef,practitionerRef, theRequestDetails);
case "patient-list":
return evaluator.evaluateSubjectListMeasure(seed.getMeasure(), seed.getContext(), practitionerRef, theRequestDetails);
case "population":
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext(), theRequestDetails);
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext(), practitionerRef, theRequestDetails);
default:
throw new IllegalArgumentException(Msg.code(1664) + "Invalid report type: " + reportType);
}
}
// default report type is patient
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef, theRequestDetails);
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef, practitionerRef, theRequestDetails);
if (productLine != null) {
Extension ext = new Extension();
ext.setUrl("http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine");

View File

@ -0,0 +1,235 @@
package ca.uhn.fhir.cql.r4;
import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.util.BundleUtil;
import org.apache.commons.collections.map.HashedMap;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Base64BinaryType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* This testing class and the used resources are work in progress and will likely be subject to change.
*/
public class CqlMeasureEvaluationR4ImmunizationTest extends BaseCqlR4Test {
Logger ourLog = LoggerFactory.getLogger(CqlMeasureEvaluationR4ImmunizationTest.class);
@Autowired
MeasureOperationsProvider myMeasureOperationsProvider;
private static final String MY_TESTBUNDLE_MMR_SIMPLE = "r4/immunization/testdata-bundles/Testbundle_3Patients_1MMRVaccinated_1PVaccinated_1NoVaccination.json";
private static final String MY_TESTBUNDLE_MMR_INCL_PRACTITIONER = "r4/immunization/testdata-bundles/Testbundle_Patients_By_Practitioner.json";
//overall testing function including bundle manipulation and evaluation and assertion
protected void testMeasureScoresByBundleAndCQLLocation(String theBundleLocation, String theCQLMeasureLocation, String thePractitionerRef, Map<String, Double> theExpectedScores) throws IOException {
//load provided bundle and replace placeholder CQL content of Library with CQL content of provided file location
Bundle bundle = loadBundleFromFileLocationAndManipulate(theBundleLocation, theCQLMeasureLocation);
//we require at least one MeasureReport in the bundle
List<MeasureReport> measureReports = findMeasureReportsOrThrowException(bundle);
//evaluate each measure report of the provided bundle
for (MeasureReport report : measureReports) {
double expectedScore = theExpectedScores.get(report.getIdentifierFirstRep().getValue());
evaluateSingleReport(report, thePractitionerRef, expectedScore);
}
}
//overall testing function including bundle manipulation and evaluation and assertion
protected void testMeasureScoresByBundleAndCQLLocation(String theBundleLocation, String theCQLMeasureLocation, String thePractitionerRef, double theExpectedScore) throws IOException {
//load provided bundle and replace placeholder CQL content of Library with CQL content of provided file location
Bundle bundle = loadBundleFromFileLocationAndManipulate(theBundleLocation, theCQLMeasureLocation);
//we require at least one MeasureReport in the bundle
List<MeasureReport> measureReports = findMeasureReportsOrThrowException(bundle);
//evaluate each measure report of the provided bundle
for (MeasureReport report : measureReports) {
evaluateSingleReport(report, thePractitionerRef, theExpectedScore);
}
}
protected void evaluateSingleReport (MeasureReport theReport, String thePractitionerRef, double theExpectedScore) {
MeasureReport actualEvaluatedReport = this.evaluateMeasure(theReport, thePractitionerRef);
ourLog.info("Score of evaluation: {}", actualEvaluatedReport.getGroupFirstRep().getMeasureScore().getValue());
assertMeasureScore(actualEvaluatedReport, theExpectedScore);
}
//double compare to assert no difference between expected and actual measure score
protected void assertMeasureScore(MeasureReport theReport, double theExpectedScore) {
//find the predefined expected score by looking up the report identifier
double epsilon = 0.000001d;
double actualScore = theReport.getGroupFirstRep().getMeasureScore().getValue().doubleValue();
assertEquals(theExpectedScore, actualScore, epsilon);
}
//evaluates a Measure to produce one certain MeasureReport
protected MeasureReport evaluateMeasure(MeasureReport theMeasureReport, String thePractitionerRef) {
String measureId = this.getMeasureId(theMeasureReport);
String patientId = null;
//only when the type of the MeasureReport is set to 'individual', there is a patient required as a subject (i.e. not for a 'summary' report)
if (theMeasureReport.getSubject().getReference() != null) {
patientId = this.getPatientId(theMeasureReport);
}
String periodStart = this.getPeriodStart(theMeasureReport);
String periodEnd = this.getPeriodEnd(theMeasureReport);
String measureReportIdentifier = theMeasureReport.getIdentifierFirstRep().getValue();
String measureReportType = theMeasureReport.getTypeElement().getValueAsString();
//only when the type of the MeasureReport is set to 'individual', there is a patient required as a subject (i.e. not for a 'summary' report)
if (patientId == null) {
ourLog.info("Evaluating Measure '{}' for MeasureReport '{}' of type '{}': [{} - {}]", measureId, measureReportIdentifier, measureReportType, periodStart, periodEnd);
} else {
ourLog.info("Evaluating Measure '{}' for MeasureReport '{}' of type '{}' for Patient '{}' [{} - {}]", measureId, measureReportIdentifier, measureReportType, patientId, periodStart, periodEnd);
}
return this.myMeasureOperationsProvider.evaluateMeasure(new IdType("Measure", measureId),
periodStart, periodEnd, null,
"patient", patientId,
null, thePractitionerRef, null, null, null, null, myRequestDetails);
}
//helper function to manipulate a test bundle
//loads a bundle from theBundleLocation: requirement: containing 1 exact FHIR Measure
//finds the relevant measure, determines the related Library and replaces the CQL content of that Library with a separate CQL file
//the CQL file defined by theCQLLocation contains regular CQL text and is automatically transformed into base64 content and automatically replaces the Library content
//this process keeps manual testing simple, as the only place to change the CQL logic is by adding a new CQL file (containing no test resources and no other content) and writing a unit test with that CQL file path
protected Bundle loadBundleFromFileLocationAndManipulate(String theBundleLocation, String theCQLLocation) throws IOException {
Bundle bundle = parseBundle(theBundleLocation);
//manipulate Bundle
Measure measure = findExactlyOneMeasuresOrThrowException(bundle);
Library libraryToManipulate = findLibraryById(bundle, measure.getLibrary().get(0).getValue());
replaceCQLOfLibrary(libraryToManipulate, theCQLLocation);
loadBundle(bundle, myRequestDetails);
return bundle;
}
//requirement: test bundle must contain exactly one Measure resource
protected Measure findExactlyOneMeasuresOrThrowException(Bundle theBundle) {
List<Measure> measures = BundleUtil.toListOfResourcesOfType(myFhirContext, theBundle, Measure.class);
if (measures == null || measures.isEmpty()) {
throw new IllegalArgumentException(String.format("No measures found for Bundle %s", theBundle.getId()));
} else if (measures.size() > 1) {
throw new IllegalArgumentException(String.format("Too many measures found for Bundle %s. Only one measure is allowed for this automated testing setup.", theBundle.getId()));
}
return measures.get(0);
}
//requirement: test bundle must contain at least one MeasureReport resource
protected List<MeasureReport> findMeasureReportsOrThrowException(Bundle theBundle) {
List<MeasureReport> reports = BundleUtil.toListOfResourcesOfType(myFhirContext, theBundle, MeasureReport.class);
if (reports == null || reports.isEmpty()) {
throw new IllegalArgumentException(String.format("No measure reports found for Bundle %s", theBundle.getId()));
}
return reports;
}
//returns a Library resource of a bundle by a given ID
protected Library findLibraryById(Bundle theBundle, String theLibraryId) {
List<Library> libraries = BundleUtil.toListOfResourcesOfType(myFhirContext, theBundle, Library.class);
return libraries.stream().filter(lib -> lib.getId().equals(theLibraryId)).findFirst().orElse(null);
}
//the provided Library resource only contains a placeholder CQL
//this function replaces this placeholder CQL with the content of a specified file
protected Library replaceCQLOfLibrary(Library theLibrary, String theCQLFileLocation) throws IOException {
String decodedCQLString = stringFromResource(theCQLFileLocation);
//replace cql in library
String encodedCQLString = Base64.getEncoder().encodeToString(decodedCQLString.getBytes(StandardCharsets.UTF_8));
Base64BinaryType encodedCQLBinary = new Base64BinaryType();
encodedCQLBinary.setValueAsString(encodedCQLString);
theLibrary.getContentFirstRep().setDataElement(encodedCQLBinary);
return theLibrary;
}
// TODO: In R4 the Subject will not necessarily be a Patient.
public String getPatientId(MeasureReport measureReport) {
String[] subjectRefParts = measureReport.getSubject().getReference().split("/");
String patientId = subjectRefParts[subjectRefParts.length - 1];
return patientId;
}
public String getMeasureId(MeasureReport measureReport) {
String[] measureRefParts = measureReport.getMeasure().split("/");
String measureId = measureRefParts[measureRefParts.length - 1];
return measureId;
}
public String getPeriodStart(MeasureReport measureReport) {
Date periodStart = measureReport.getPeriod().getStart();
if (periodStart != null) {
return toDateString(periodStart);
}
return null;
}
public String getPeriodEnd(MeasureReport measureReport) {
Date periodEnd = measureReport.getPeriod().getEnd();
if (periodEnd != null) {
return toDateString(periodEnd);
}
return null;
}
public String toDateString(Date date) {
return new DateTimeType(date).getValueAsString();
}
@Test
public void test_Immunization_MMR_Individual_Vaccinated() throws IOException {
Map<String, Double> expectedScoresByIdentifier = new HashedMap();
//expected result: individual should be in numerator because Patient is MMR vaccinated
expectedScoresByIdentifier.put("measureReportIndividualVaccinatedPatient", 1.0);
//expected result: individual should not be in numerator because Patient is not MMR vaccinated (only Pertussis)
expectedScoresByIdentifier.put("measureReportIndividualNotMMRVaccinatedPatient", 0.0);
//expected result: individual should not be in numerator because Patient is not at all vaccinated (no associated Immmunization resource)
expectedScoresByIdentifier.put("measureReportIndividualNotAtAllVaccinatedPatient", 0.0);
//expected result: summary confirms that 1 out of all 3 patients are MMR immunized
expectedScoresByIdentifier.put("measureReportSummary", 1.0 / 3.0);
//note: all those CQL files specified as the second parameter produce the exact same outcome with the given test resources provided by the first parameter.
//TODO: tests are dependent and will fail if the order is incorrect. --> clean up tests
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_SIMPLE, "r4/immunization/cqls/3-Vaccine-Codes-Defined-By-ValueSet-MMR-Vaccine-Codes.cql", null, expectedScoresByIdentifier);
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_SIMPLE, "r4/immunization/cqls/1-Explicit-Vaccine-Codes-From-Any-System.cql", null, expectedScoresByIdentifier);
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_SIMPLE, "r4/immunization/cqls/2-Explicit-Vaccine-Codes-And-Systems.cql", null, expectedScoresByIdentifier);
}
@Test
public void test_Immunization_ByPractitioner_MMR_Summary() throws IOException {
//half of dreric' s patients (total of 2) are vaccinated, so 1/2 is vaccinated.
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_INCL_PRACTITIONER, "r4/immunization/cqls/3-Vaccine-Codes-Defined-By-ValueSet-MMR-Vaccine-Codes.cql", "Practitioner/dreric", 1.0/2.0);
//of drfrank's patients (total of 1), none are vaccinated
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_INCL_PRACTITIONER, "r4/immunization/cqls/3-Vaccine-Codes-Defined-By-ValueSet-MMR-Vaccine-Codes.cql", "Practitioner/drfrank", 0.0/1.0);
}
@Test
public void test_Immunization_ByAge() throws IOException {
//be aware that this test will fail eventually, because this patient will at some point become one years old (today - birthdate > 1 year)
//this patient is not yet immunized, because too young. so it won't be counted as a denominator patient and therefore the measure score is corrected to have a higher percentage
//of dreric' s patients, all are vaccinated, because the second patient who is not vaccinated yet doesn't meet the age criteria (denominator), so 1/1 is vaccinated.
this.testMeasureScoresByBundleAndCQLLocation(MY_TESTBUNDLE_MMR_INCL_PRACTITIONER, "r4/immunization/cqls/4-Patients-ByAge.cql", "Practitioner/dreric", 1.0/1.0);
}
}

View File

@ -0,0 +1,22 @@
library Retrieve
using FHIR version '4.0.1'
include FHIRHelpers version '4.0.1'
context Patient
define "MMR Vaccinated":
[Immunization] Immu where
(Immu.vaccineCode.coding[0].code=('03')) or
(Immu.vaccineCode.coding[0].code=('94')) or
(Immu.vaccineCode.coding[0].code=('MMR')) or
(Immu.vaccineCode.coding[0].code=('MMRV')) or
(Immu.vaccineCode.coding[0].code=('MMRCSL'))
define "InitialPopulation":
true
define "Denominator":
true
define "Numerator":
exists("MMR Vaccinated")

View File

@ -0,0 +1,32 @@
library Retrieve
using FHIR version '4.0.1'
include FHIRHelpers version '4.0.1'
codesystem VaccineCVX: 'http://hl7.org/fhir/sid/cvx'
codesystem VaccineOID: 'urn:oid:1.2.36.1.2001.1005.17'
context Patient
define "MMR Vaccinated 03":
[Immunization: Code '03' from VaccineCVX] Immu where Immu.status in {'completed'}
define "MMR Vaccinated 94":
[Immunization: Code '94' from VaccineCVX] Immu where Immu.status in {'completed'}
define "MMR Vaccinated MMR":
[Immunization: Code 'MMR' from VaccineOID] Immu where Immu.status in {'completed'}
define "MMR Vaccinated MMRCSL":
[Immunization: Code 'MMRCSL' from VaccineOID] Immu where Immu.status in {'completed'}
define "InitialPopulation":
true
define "Denominator":
true
define "Numerator":
exists("MMR Vaccinated 03") or
exists("MMR Vaccinated 94") or
exists("MMR Vaccinated MMR") or
exists("MMR Vaccinated MMRCSL")

View File

@ -0,0 +1,20 @@
library Retrieve
using FHIR version '4.0.1'
include FHIRHelpers version '4.0.1'
valueset "MMR Vaccinated": 'http://hl7.org/fhir/ValueSet/mmr-vaccine-codes'
context Patient
define "InitialPopulation":
[Patient]
define "Denominator":
[Patient]
define "Numerator":
"Qualifying Immunizations"
define "Qualifying Immunizations":
[Immunization: "MMR Vaccinated"] ValidImmunization
where ValidImmunization.status = 'completed'

View File

@ -0,0 +1,20 @@
library Retrieve
using FHIR version '4.0.1'
include FHIRHelpers version '4.0.1'
valueset "MMRVaccinated": 'http://hl7.org/fhir/ValueSet/mmr-vaccine-codes'
context Patient
define "InitialPopulation":
[Patient]
define "Denominator":
[Patient] myPatient where (myPatient.birthDate before (Today() - 1 year))
define "Numerator":
"QualifyingImmunizations"
define "QualifyingImmunizations":
[Immunization: "MMRVaccinated"] myImmunization
where myImmunization.status = 'completed'