Skip to content

Commit

Permalink
Evaluate measures in-process
Browse files Browse the repository at this point in the history
  • Loading branch information
smailliwcs committed Feb 22, 2024
1 parent 1f6a6cc commit 1799a09
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 136 deletions.
9 changes: 9 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

<dependency>
<groupId>org.opencds.cqf.fhir</groupId>
<artifactId>cqf-fhir-cr</artifactId>
</dependency>
<dependency>
<groupId>org.opencds.cqf.fhir</groupId>
<artifactId>cqf-fhir-jackson</artifactId>
<type>pom</type>
</dependency>
</dependencies>

<build>
Expand Down
33 changes: 2 additions & 31 deletions api/src/main/java/com/lantanagroup/link/api/ApiInit.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.lantanagroup.link.FhirContextProvider;
import com.lantanagroup.link.FhirDataProvider;
import com.lantanagroup.link.config.api.ApiConfig;
Expand All @@ -12,7 +11,6 @@
import com.lantanagroup.link.db.model.tenant.Tenant;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -35,8 +33,8 @@ private boolean checkPrerequisites() {
logger.info("Checking that API prerequisite services are available. maxRetry: {}, retryWait: {}", config.getMaxRetry(), config.getRetryWait());

boolean allServicesAvailable = false;
boolean terminologyServiceAvailable = false;
boolean evaluationServiceAvailable = false;
boolean terminologyServiceAvailable = StringUtils.isEmpty(config.getTerminologyService());
boolean evaluationServiceAvailable = true; // In-process evaluation is always "available"

for (int retry = 0; config.getMaxRetry() == null || retry <= config.getMaxRetry(); retry++) {
// Check terminology service availability
Expand All @@ -49,17 +47,6 @@ private boolean checkPrerequisites() {
}
}

// Check evaluation service availability
if (!evaluationServiceAvailable) {
try {
new FhirDataProvider(config.getEvaluationService()).getClient().capabilities().ofType(CapabilityStatement.class).execute();
evaluationServiceAvailable = true;
} catch (BaseServerResponseException e) {
logger.error(String.format("Could not connect to evaluation service %s (%s)", config.getEvaluationService(), e));
}
}


// Check if all services are now available
allServicesAvailable = terminologyServiceAvailable && evaluationServiceAvailable;
if (allServicesAvailable) {
Expand Down Expand Up @@ -144,21 +131,5 @@ public void init() {
if (!this.checkPrerequisites()) {
throw new IllegalStateException("Prerequisite services check failed. Cannot continue API initialization.");
}

ensureSupplementalDataSearchParameter();
}

private void ensureSupplementalDataSearchParameter() {
logger.info("Requesting evaluation of nonexistent measure to ensure supplemental-data search parameter exists");
try {
FhirContextProvider.getFhirContext().newRestfulGenericClient(this.config.getEvaluationService())
.operation()
.onInstance("Measure/nonexistent-measure")
.named("$evaluate-measure")
.withNoParameters(Parameters.class)
.execute();
} catch (ResourceNotFoundException e) {
logger.info("Caught 404 as expected");
}
}
}
39 changes: 39 additions & 0 deletions api/src/main/java/com/lantanagroup/link/api/MeasureDef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.lantanagroup.link.api;

import com.lantanagroup.link.StreamUtils;
import lombok.Getter;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Measure;

import java.util.List;
import java.util.stream.Collectors;

@Getter
public class MeasureDef {
private final Measure measure;
private final List<IBaseResource> resources;

public MeasureDef(Bundle bundle) {
measure = bundle.getEntry().stream()
.map(Bundle.BundleEntryComponent::getResource)
.filter(resource -> resource instanceof Measure)
.map(resource -> (Measure) resource)
.reduce(StreamUtils::toOnlyElement)
.orElseThrow();

// Ensure all populations have an ID
for (Measure.MeasureGroupComponent group : measure.getGroup()) {
for (Measure.MeasureGroupPopulationComponent population : group.getPopulation()) {
if (!population.hasId()) {
population.setId(population.getCode().getCodingFirstRep().getCode());
}
}
}

resources = bundle.getEntry().stream()
.map(Bundle.BundleEntryComponent::getResource)
.filter(resource -> !(resource instanceof Measure))
.collect(Collectors.toList());
}
}
35 changes: 6 additions & 29 deletions api/src/main/java/com/lantanagroup/link/api/MeasureEvaluator.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.lantanagroup.link.api;

import com.lantanagroup.link.Constants;
import com.lantanagroup.link.FhirDataProvider;
import com.lantanagroup.link.Helper;
import com.lantanagroup.link.ReportIdHelper;
import com.lantanagroup.link.config.api.ApiConfig;
Expand All @@ -25,9 +24,11 @@ public class MeasureEvaluator {
private String patientId;
private StopwatchManager stopwatchManager;
private TenantService tenantService;
private MeasureService measureService;

private MeasureEvaluator(TenantService tenantService, StopwatchManager stopwatchManager, ReportCriteria criteria, ReportContext reportContext, ReportContext.MeasureContext measureContext, ApiConfig config, String patientId) {
private MeasureEvaluator(TenantService tenantService, MeasureService measureService, StopwatchManager stopwatchManager, ReportCriteria criteria, ReportContext reportContext, ReportContext.MeasureContext measureContext, ApiConfig config, String patientId) {
this.tenantService = tenantService;
this.measureService = measureService;
this.stopwatchManager = stopwatchManager;
this.criteria = criteria;
this.reportContext = reportContext;
Expand All @@ -36,21 +37,11 @@ private MeasureEvaluator(TenantService tenantService, StopwatchManager stopwatch
this.patientId = patientId;
}

public static MeasureReport generateMeasureReport(TenantService tenantService, StopwatchManager stopwatchManager, ReportCriteria criteria, ReportContext reportContext, ReportContext.MeasureContext measureContext, ApiConfig config, PatientOfInterestModel patientOfInterest) {
MeasureEvaluator evaluator = new MeasureEvaluator(tenantService, stopwatchManager, criteria, reportContext, measureContext, config, patientOfInterest.getId());
public static MeasureReport generateMeasureReport(TenantService tenantService, MeasureService measureService, StopwatchManager stopwatchManager, ReportCriteria criteria, ReportContext reportContext, ReportContext.MeasureContext measureContext, ApiConfig config, PatientOfInterestModel patientOfInterest) {
MeasureEvaluator evaluator = new MeasureEvaluator(tenantService, measureService, stopwatchManager, criteria, reportContext, measureContext, config, patientOfInterest.getId());
return evaluator.generateMeasureReport();
}

private static Endpoint getTerminologyEndpoint(ApiConfig config) {
Endpoint terminologyEndpoint = new Endpoint();
terminologyEndpoint.setStatus(Endpoint.EndpointStatus.ACTIVE);
terminologyEndpoint.setConnectionType(new Coding());
terminologyEndpoint.getConnectionType().setSystem(Constants.TerminologyEndpointSystem);
terminologyEndpoint.getConnectionType().setCode(Constants.TerminologyEndpointCode);
terminologyEndpoint.setAddress(config.getTerminologyService());
return terminologyEndpoint;
}

private MeasureReport generateMeasureReport() {
MeasureReport measureReport;
String patientDataBundleId = ReportIdHelper.getPatientDataBundleId(reportContext.getMasterIdentifierValue(), patientId);
Expand All @@ -71,23 +62,9 @@ private MeasureReport generateMeasureReport() {

logger.info("Executing $evaluate-measure for measure: {}, start: {}, end: {}, patient: {}, resources: {}", measureId, start, end, patientId, patientBundle.getEntry().size());

Parameters parameters = new Parameters();
parameters.addParameter().setName("periodStart").setValue(new StringType(start));
parameters.addParameter().setName("periodEnd").setValue(new StringType(end));
parameters.addParameter().setName("subject").setValue(new StringType(patientId));
parameters.addParameter().setName("additionalData").setResource(patientBundle);
if (!this.config.getEvaluationService().equals(this.config.getTerminologyService())) {
Endpoint terminologyEndpoint = getTerminologyEndpoint(this.config);
parameters.addParameter().setName("terminologyEndpoint").setResource(terminologyEndpoint);
logger.info("evaluate-measure is being executed with the terminologyEndpoint parameter.");
}

logger.info(String.format("Evaluating measure for patient %s and measure %s", patientId, measureId));

FhirDataProvider fhirDataProvider = new FhirDataProvider(this.config.getEvaluationService());
//noinspection unused
try (Stopwatch stopwatch = this.stopwatchManager.start(Constants.TASK_MEASURE, Constants.CATEGORY_EVALUATE)) {
measureReport = fhirDataProvider.getMeasureReport(measureId, parameters);
measureReport = measureService.evaluate(this.criteria.getPeriodStart(), this.criteria.getPeriodEnd(), patientId, patientBundle);
}

// TODO: commenting out this code because the narrative text isn't being generated, will need to look into this
Expand Down
79 changes: 79 additions & 0 deletions api/src/main/java/com/lantanagroup/link/api/MeasureService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.lantanagroup.link.api;

import com.lantanagroup.link.Constants;
import com.lantanagroup.link.FhirContextProvider;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.MeasureReport;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureService;
import org.opencds.cqf.fhir.utility.monad.Eithers;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;

public class MeasureService {
private final MeasureDef measureDef;
private final R4MeasureService measureService;
private final Endpoint terminologyEndpoint;

public MeasureService(Bundle measureDefBundle, String terminologyService) {
measureDef = new MeasureDef(measureDefBundle);
Repository repository = new InMemoryFhirRepository(FhirContextProvider.getFhirContext());
for (IBaseResource resource : measureDef.getResources()) {
repository.update(resource);
}
MeasureEvaluationOptions options = MeasureEvaluationOptions.defaultOptions();
EvaluationSettings evaluationSettings = options.getEvaluationSettings();
evaluationSettings.getTerminologySettings()
.setValuesetPreExpansionMode(TerminologySettings.VALUESET_PRE_EXPANSION_MODE.USE_IF_PRESENT)
.setValuesetExpansionMode(TerminologySettings.VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION)
.setValuesetMembershipMode(TerminologySettings.VALUESET_MEMBERSHIP_MODE.USE_EXPANSION)
.setCodeLookupMode(TerminologySettings.CODE_LOOKUP_MODE.USE_CODESYSTEM_URL);
evaluationSettings.getRetrieveSettings()
.setTerminologyParameterMode(RetrieveSettings.TERMINOLOGY_FILTER_MODE.FILTER_IN_MEMORY)
.setSearchParameterMode(RetrieveSettings.SEARCH_FILTER_MODE.FILTER_IN_MEMORY)
.setProfileMode(RetrieveSettings.PROFILE_MODE.DECLARED);
measureService = new R4MeasureService(repository, options);
terminologyEndpoint = getTerminologyEndpoint(terminologyService);
}

private static Endpoint getTerminologyEndpoint(String terminologyService) {
if (StringUtils.isEmpty(terminologyService)) {
return null;
}
Endpoint endpoint = new Endpoint();
endpoint.setStatus(Endpoint.EndpointStatus.ACTIVE);
endpoint.setConnectionType(new Coding());
endpoint.getConnectionType().setSystem(Constants.TerminologyEndpointSystem);
endpoint.getConnectionType().setCode(Constants.TerminologyEndpointCode);
endpoint.setAddress(terminologyService);
return endpoint;
}

public void preCompile() {
evaluate(null, null, null, null);
}

public MeasureReport evaluate(String periodStart, String periodEnd, String subject, Bundle additionalData) {
return measureService.evaluate(
Eithers.forRight3(measureDef.getMeasure()),
periodStart,
periodEnd,
null,
subject,
null,
null,
terminologyEndpoint,
null,
additionalData,
null,
null,
null);
}
}
11 changes: 5 additions & 6 deletions api/src/main/java/com/lantanagroup/link/api/ReportGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ public ReportGenerator(SharedService sharedService, TenantService tenantService,
* This method accepts a list of patients and generates an individual measure report for each patient.
*/
public void generate(QueryPhase queryPhase) throws ExecutionException, InterruptedException {
if (this.config.getEvaluationService() == null) {
throw new IllegalStateException("api.evaluation-service has not been configured");
}
MeasureService measureService = new MeasureService(measureContext.getReportDefBundle(), config.getTerminologyService());
measureService.preCompile();
logger.info("Patient list is : " + measureContext.getPatientsOfInterest(queryPhase).size());
ForkJoinPool forkJoinPool = config.getMeasureEvaluationThreads() != null
? new ForkJoinPool(config.getMeasureEvaluationThreads())
Expand All @@ -75,7 +74,7 @@ public void generate(QueryPhase queryPhase) throws ExecutionException, Interrupt
return;
}
try {
MeasureReport measureReport = generate(patient);
MeasureReport measureReport = generate(measureService, patient);
synchronized (this) {
measureContext.getPatientReportsByPatientId().put(patient.getId(), measureReport);
}
Expand All @@ -95,7 +94,7 @@ public void generate(QueryPhase queryPhase) throws ExecutionException, Interrupt
}
}

private MeasureReport generate(PatientOfInterestModel patient) {
private MeasureReport generate(MeasureService measureService, PatientOfInterestModel patient) {
String measureReportId = ReportIdHelper.getPatientMeasureReportId(measureContext.getReportId(), patient.getId());
PatientMeasureReport patientMeasureReport = new PatientMeasureReport();
patientMeasureReport.setId(measureReportId);
Expand All @@ -105,7 +104,7 @@ private MeasureReport generate(PatientOfInterestModel patient) {

logger.info("Generating measure report for patient " + patient);

MeasureReport measureReport = MeasureEvaluator.generateMeasureReport(this.tenantService, this.stopwatchManager, criteria, reportContext, measureContext, config, patient);
MeasureReport measureReport = MeasureEvaluator.generateMeasureReport(this.tenantService, measureService, this.stopwatchManager, criteria, reportContext, measureContext, config, patient);
measureReport.setId(measureReportId);
patientMeasureReport.setMeasureReport(measureReport);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public String getTenantJsonSchema() {

@GetMapping
public ApiVersionModel getVersionInfo() {
return Helper.getVersionInfo(this.apiConfig.getEvaluationService());
return Helper.getVersionInfo();
}

@GetMapping("/info")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,6 @@ public void initializeBinder(WebDataBinder binder) {
binder.setDisallowedFields();
}

/**
* Executes the measure bundle on the evaluation service (cqf-ruler)
*/
private void executeBundle(Bundle bundle) {
FhirDataProvider fhirDataProvider = new FhirDataProvider(this.apiConfig.getEvaluationService());

bundle.setType(Bundle.BundleType.BATCH);
bundle.getEntry().forEach(entry -> {
entry.setRequest(new Bundle.BundleEntryRequestComponent());
entry.getRequest()
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl(entry.getResource().getResourceType().toString() + "/" + entry.getResource().getIdElement().getIdPart());
});

logger.info("Loading measure definition {} on eval service {}", bundle.getIdElement().getIdPart(), this.apiConfig.getEvaluationService());
fhirDataProvider.transaction(bundle);
}

private Bundle getBundleFromUrl(String url) throws URISyntaxException {
URI uri = new URI(Helper.sanitizeUrl(url));

Expand Down Expand Up @@ -126,8 +108,6 @@ public void createOrUpdateMeasureDef(@RequestBody(required = false) Bundle bundl
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Measure bundle contains resources without ids");
}

this.executeBundle(bundle);

MeasureDefinition measureDefinition = this.sharedService.getMeasureDefinition(bundle.getIdElement().getIdPart());

if (measureDefinition == null) {
Expand Down
9 changes: 1 addition & 8 deletions core/src/main/java/com/lantanagroup/link/FhirHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public static boolean hasNonzeroPopulationCount(MeasureReport measureReport) {
}

public static Device getDevice(ApiConfig apiConfig) {
ApiVersionModel apiVersionModel = Helper.getVersionInfo(apiConfig.getEvaluationService());
ApiVersionModel apiVersionModel = Helper.getVersionInfo();
Device device = new Device();
device.addDeviceName().setName(apiConfig.getName());
device.getDeviceNameFirstRep().setType(Device.DeviceNameType.USERFRIENDLYNAME);
Expand All @@ -267,13 +267,6 @@ public static Device getDevice(ApiConfig apiConfig) {
.setValue(apiVersionModel.getCommit());
}

if (StringUtils.isNotEmpty(apiVersionModel.getCqfVersion())) {
device.addVersion()
.setType(new CodeableConcept().addCoding(new Coding().setCode("version").setSystem(Constants.LinkDeviceVersionCodeSystem)))
.setComponent(new Identifier().setValue("cqf-ruler"))
.setValue(apiVersionModel.getCqfVersion());
}

return device;
}

Expand Down
Loading

0 comments on commit 1799a09

Please sign in to comment.