Skip to content

Commit 3b379c8

Browse files
Optimize CQL library evaluation and consequently measure evaluation (#688)
* First attempt to flip measure and subject looping for population based measure reports. * Document new unit test failures. * Abandon previous experiment. Push up evaluate measure callers to the service layers for single and multi measures and collect data. Leave old evaluate measure code within processor for the time being as there are still unit test failures otherwise, hopefully limited to single measure evaluation. * Capture discussion with Justin and requirements but lots of compile errors. * Put in place changes agreed with Justin and all tests except one are passing. * Fix CollectData unit test by making resolveParameterMap work with a mutable list. * Spotless. Delete some cruft. * Cleanup and new TODOs. * Fix compile errors. * Small tweaks and TODOs. * Overhaul CompositeEvaluationResultsPerMeasure to include errors as well as successful results, and enhancing it to mutate a given MeasureDef with errors should they exist. Start cleaning up. Spotless. * Cleanup. Spotless. * Spotless. * Handle new MeasureProcessorUtils#setMeasurePeriod, ensure all callers call it correctly, and get rid of duplicate methods. * Clean up todos and ensure library checking is done in only one place. * More refactoring and resolve TODOs. * Resolve sonar warnings. * Reverse a sonar change that caused errors. Fix other sonar issues. Tests pass now. * Initialize the CqlEngine context at the beginning, pass it to down to the CQL library evaluation, and then to the measure evaluation. * More cleanup of dead code, commented out code, and dead parameters. * Fix compile error. Fix more sonar. Spotless. Get rid of unused constructor param. * Get rid of legacy CQL evaluation and have DSTU3 use the new code. * Fix sonar and reverse some minor changes. * Fix DSTU3 tests and resolve last sonar. * More refactoring to DRY and to remove unused fields. * Small refactoring for multi-measure service. * Optimize performance of CompositeEvaluationResultsPerMeasure. * Spotless. * Small refactoring. * Spotless. * Replace all java.annotation imports with jakarta.annotation. * Add comment about new approach, as per code review. * Slight fix to algorithm and TODO for CQL fix. * Start integrating with new CQL branch with caching optimizations. * Use the same made-up version of CQL that the CQL branch is using. * Low risk changes including adding having CompositeEvaluationResultsPerMeasure handle adding multiple errors at once and a TODO to get rid of a record once fixes are complete. * Grab changes from branch to integrate with CQL changes to validate library identifiers. Spotless. * Fix compile error. * Experiment more with algorithms. Thus far, if we pass more than one library identifier at a time, we cause definite failures in MultiMeasureServiceTest and intermittent failures on at least one other test if we run the whole module test suite. * Flip to the new code from the old code in MeasureProcessorUtils. Add a TODO about what needs to be done to the test to make it pass under the new code. Add a TODO about another unexpected failure as well. * Assert that the mulitmeasure "allsubjects" tests still pass but contain InvalidInterval errors. * Leverage multi-measure error handling code added to CQL. Spotless. Still one test failing that I need to figure out. * Set custom version in the pom for now. * Add TODO. Reverse a bunch of logging changes. Ensure we don't queue up an evaluation result if the library did not evaluate successfully. * Attempt to set up a successful complex layer library test but no success yet. * Add unit test. * Change approach to 1B testing to filter based on meta profiles. Add meta profiles to all encounters and change IDs accordingly. Adjust CQLs. Start trying to assert stratifiers from tests. * Update to a new stub version ahead of the current master version of clinical-reasoning. Make R4PopulationBasisValidator more error tolerant, especially if CQL returns Iterators instead of Lists. Enhance MultiMeasure test util. * Make final changes to MultiLibEvalComplexCqlTest. Add back some logging. * First set of refactoring changes. * More cleanup. * Start merging old and new code bases. Clean up more cruft. * More refactoring to merge old and new code branches. More cleanup. * Small tweaks and integrate with latest CQL branch changes. * Add temporary logging for db queries. * Set up complex $evaluate test, which doesn't yet exercise multiple libraries. * Adapt to new CQL result handling code, notably for error and exception handling. * Adapt to new CQL exception handling code. Comment out RepositoryLoggingProxy. * Adapt to new CQL library resolution/compilation code. Add a test that captures the scenario we're trying to optimize for. Start getting rid of logs. * Fix test input directory structures to conform to new standard. Add unversioned local files. * Fix some TODOs including tests, logging, and javadoc. * Address rest of TODOs and add another test. * Sync up with pom versions from master. Clean up more cruft. * Change CQL version to 3.28.0. * Fix tests for new evaluatedMeasures() logic. * Fix capitalization --------- Co-authored-by: JP <jonathan.percival@smilecdr.com> Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
1 parent 61cd1a8 commit 3b379c8

100 files changed

Lines changed: 2598 additions & 274 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) {
8282
}
8383

8484
private R4MeasureService buildMeasureService() {
85-
return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator, measureServiceUtils);
85+
return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator);
8686
}
8787

8888
public When when() {

cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.HashSet;
1515
import java.util.List;
1616
import java.util.Map;
17+
import java.util.Objects;
1718
import java.util.Set;
1819
import java.util.stream.Collectors;
1920
import org.apache.commons.lang3.StringUtils;
@@ -25,6 +26,7 @@
2526
import org.hl7.fhir.instance.model.api.IBaseParameters;
2627
import org.opencds.cqf.cql.engine.execution.CqlEngine;
2728
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
29+
import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib;
2830
import org.opencds.cqf.fhir.cql.engine.parameters.CqlFhirParametersConverter;
2931
import org.opencds.cqf.fhir.cql.engine.parameters.CqlParameterDefinition;
3032
import org.opencds.cqf.fhir.utility.CqfExpression;
@@ -325,8 +327,8 @@ public List<IBase> resolveExpression(
325327
return result;
326328
}
327329

328-
public EvaluationResult getEvaluationResult(
329-
VersionedIdentifier id,
330+
public EvaluationResultsForMultiLib getEvaluationResult(
331+
List<VersionedIdentifier> ids,
330332
String patientId,
331333
IBaseParameters parameters,
332334
Map<String, Object> rawParameters,
@@ -336,25 +338,53 @@ public EvaluationResult getEvaluationResult(
336338
@Nullable ZonedDateTime zonedDateTime,
337339
CqlEngine engine) {
338340

339-
if (cqlFhirParametersConverter == null) {
340-
cqlFhirParametersConverter = Engines.getCqlFhirParametersConverter(repository.fhirContext());
341-
}
341+
var cqlFhirParametersConverterToUse = Objects.requireNonNullElseGet(
342+
cqlFhirParametersConverter, () -> Engines.getCqlFhirParametersConverter(repository.fhirContext()));
343+
342344
// engine context built externally of LibraryEngine?
343-
if (engine == null) {
344-
engine = Engines.forRepository(repository, settings, additionalData);
345-
}
345+
var engineToUse = Objects.requireNonNullElseGet(
346+
engine, () -> Engines.forRepository(repository, settings, additionalData));
346347

347-
var evaluationParameters = cqlFhirParametersConverter.toCqlParameters(parameters);
348+
var evaluationParameters = cqlFhirParametersConverterToUse.toCqlParameters(parameters);
348349
if (rawParameters != null && !rawParameters.isEmpty()) {
349350
evaluationParameters.putAll(rawParameters);
350351
}
351352

352-
return engine.evaluate(
353-
new VersionedIdentifier().withId(id.getId()),
353+
var versionlessIdentifiers = ids.stream()
354+
.map(id -> new VersionedIdentifier().withId(id.getId()))
355+
.toList();
356+
357+
return engineToUse.evaluate(
358+
versionlessIdentifiers,
354359
expressions,
355360
buildContextParameter(patientId),
356361
evaluationParameters,
357362
null,
358363
zonedDateTime);
359364
}
365+
366+
public EvaluationResult getEvaluationResult(
367+
VersionedIdentifier id,
368+
String patientId,
369+
IBaseParameters parameters,
370+
Map<String, Object> rawParameters,
371+
IBaseBundle additionalData,
372+
Set<String> expressions,
373+
CqlFhirParametersConverter cqlFhirParametersConverter,
374+
@Nullable ZonedDateTime zonedDateTime,
375+
CqlEngine engine) {
376+
377+
var evaluationResultsForMultiLib = getEvaluationResult(
378+
List.of(id),
379+
patientId,
380+
parameters,
381+
rawParameters,
382+
additionalData,
383+
expressions,
384+
cqlFhirParametersConverter,
385+
zonedDateTime,
386+
engine);
387+
388+
return evaluationResultsForMultiLib.getOnlyResultOrThrow();
389+
}
360390
}

cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
import ca.uhn.fhir.context.FhirContext;
1616
import ca.uhn.fhir.util.BundleBuilder;
17+
import jakarta.annotation.Nonnull;
1718
import java.util.ArrayList;
1819
import java.util.HashMap;
1920
import java.util.List;
2021
import java.util.stream.StreamSupport;
21-
import javax.annotation.Nonnull;
2222
import org.cqframework.cql.cql2elm.StringLibrarySourceProvider;
2323
import org.cqframework.fhir.npm.NpmPackageManager;
2424
import org.cqframework.fhir.npm.NpmProcessor;

cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@
2626
import org.opencds.cqf.fhir.cr.cli.argument.MeasureCommandArgument;
2727
import org.opencds.cqf.fhir.cr.cli.command.CqlCommand.SubjectAndResult;
2828
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
29-
import org.opencds.cqf.fhir.cr.measure.SubjectProviderOptions;
29+
import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils;
3030
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor;
31-
import org.opencds.cqf.fhir.cr.measure.r4.R4RepositorySubjectProvider;
32-
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
3331
import picocli.CommandLine.ArgGroup;
3432
import picocli.CommandLine.Command;
3533

@@ -139,11 +137,7 @@ private static R4MeasureProcessor getR4MeasureProcessor(
139137
evaluationOptions.setApplyScoringSetMembership(false);
140138
evaluationOptions.setEvaluationSettings(evaluationSettings);
141139

142-
return new R4MeasureProcessor(
143-
repository,
144-
evaluationOptions,
145-
new R4RepositorySubjectProvider(new SubjectProviderOptions()),
146-
new R4MeasureServiceUtils(repository));
140+
return new R4MeasureProcessor(repository, evaluationOptions, new MeasureProcessorUtils());
147141
}
148142

149143
private void writeJsonToFile(String json, String patientId, Path path) {

cqf-fhir-cr-cli/src/test/java/org/opencds/cqf/fhir/cr/cli/CliTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ void optionsFailure() {
327327
Main.run(args);
328328

329329
String errOutput = errContent.toString();
330-
assertTrue(errOutput.contains("library FluentFunctions loaded, but had errors"));
330+
assertTrue(errOutput.contains("Library FluentFunctions loaded, but had errors"));
331331
}
332332

333333
@Test

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,8 @@ public class CrR4Config {
4848
R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory(
4949
IRepositoryFactory repositoryFactory,
5050
MeasureEvaluationOptions evaluationOptions,
51-
MeasurePeriodValidator measurePeriodValidator,
52-
R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) {
53-
return rd -> new R4MeasureService(
54-
repositoryFactory.create(rd),
55-
evaluationOptions,
56-
measurePeriodValidator,
57-
r4MeasureServiceUtilsFactory.create(rd));
51+
MeasurePeriodValidator measurePeriodValidator) {
52+
return rd -> new R4MeasureService(repositoryFactory.create(rd), evaluationOptions, measurePeriodValidator);
5853
}
5954

6055
@Bean
@@ -90,8 +85,7 @@ ICollectDataServiceFactory collectDataServiceFactory(
9085
IRepositoryFactory repositoryFactory,
9186
MeasureEvaluationOptions measureEvaluationOptions,
9287
R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) {
93-
return rd -> new R4CollectDataService(
94-
repositoryFactory.create(rd), measureEvaluationOptions, r4MeasureServiceUtilsFactory.create(rd));
88+
return rd -> new R4CollectDataService(repositoryFactory.create(rd), measureEvaluationOptions);
9589
}
9690

9791
@Bean

cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import ca.uhn.fhir.repository.IRepository;
44
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
5+
import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils;
56
import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider;
67
import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureProcessor;
78
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor;
8-
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
99
import org.springframework.context.annotation.Bean;
1010
import org.springframework.context.annotation.ComponentScan;
1111
import org.springframework.context.annotation.Configuration;
@@ -26,8 +26,7 @@ Dstu3MeasureProcessor dstu3MeasureProcessor(
2626
R4MeasureProcessor r4MeasureProcessor(
2727
IRepository repository,
2828
MeasureEvaluationOptions measureEvaluationOptions,
29-
SubjectProvider subjectProvider,
30-
R4MeasureServiceUtils measureServiceUtils) {
31-
return new R4MeasureProcessor(repository, measureEvaluationOptions, subjectProvider, measureServiceUtils);
29+
MeasureProcessorUtils measureProcessorUtils) {
30+
return new R4MeasureProcessor(repository, measureEvaluationOptions, measureProcessorUtils);
3231
}
3332
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.opencds.cqf.fhir.cr.measure.common;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import java.util.ArrayList;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import org.hl7.fhir.instance.model.api.IIdType;
9+
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
10+
11+
/**
12+
* Container meant to hold the results or an early and cached CQL measure evaluation that holds
13+
* two data points:
14+
* <ol>
15+
* <li>To hold the results of a measure evaluation, grouped by measure ID, with each measure evaluation grouped by Subject ID</li>
16+
* <li>To hold any errors that occurred during the evaluation, grouped by measure ID</li>
17+
* </ol>
18+
* These data points are not mutually exclusive, meaning a measure may have both successful results and errors.
19+
* <p/>
20+
* This class also allows the caller to mutate a {@link MeasureDef} with the errors that occurred during the evaluation
21+
*/
22+
public class CompositeEvaluationResultsPerMeasure {
23+
// The same measure may have successful results AND errors, so account for both
24+
private final Map<IIdType, Map<String, EvaluationResult>> resultsPerMeasure;
25+
// We may get several errors for a given measure
26+
private final Map<IIdType, List<String>> errorsPerMeasure;
27+
28+
private CompositeEvaluationResultsPerMeasure(Builder builder) {
29+
30+
var resultsBuilder = ImmutableMap.<IIdType, Map<String, EvaluationResult>>builder();
31+
builder.resultsPerMeasure.forEach((key, value) -> resultsBuilder.put(key, ImmutableMap.copyOf(value)));
32+
resultsPerMeasure = resultsBuilder.build();
33+
34+
var errorsBuilder = ImmutableMap.<IIdType, List<String>>builder();
35+
builder.errorsPerMeasure.forEach((key, value) -> errorsBuilder.put(key, List.copyOf(value)));
36+
errorsPerMeasure = errorsBuilder.build();
37+
}
38+
39+
/**
40+
* Retrieves results and populates errors for a given measure.
41+
* This method uses direct map lookups for efficient data retrieval.
42+
* measureDef will occasionally be prepended with the version, which means we need to parse it into an IIdType which
43+
* is too much work, so pass in the measureId directly
44+
*
45+
* @param measureId the ID of the measure to process
46+
* @param measureDef the MeasureDef to populate with errors
47+
*
48+
* @return a map of evaluation results per subject, or an empty map if none exist
49+
*/
50+
public Map<String, EvaluationResult> processMeasureForSuccessOrFailure(IIdType measureId, MeasureDef measureDef) {
51+
var unqualifiedMeasureId = measureId.toUnqualifiedVersionless();
52+
53+
errorsPerMeasure.getOrDefault(unqualifiedMeasureId, List.of()).forEach(measureDef::addError);
54+
55+
// We are explicitly maintaining the logic of accepting the lack of any sort of results,
56+
// either errors or successes, and returning an empty map.
57+
return resultsPerMeasure.getOrDefault(unqualifiedMeasureId, Map.of());
58+
}
59+
60+
public static Builder builder() {
61+
return new Builder();
62+
}
63+
64+
public static class Builder {
65+
private final Map<IIdType, Map<String, EvaluationResult>> resultsPerMeasure = new HashMap<>();
66+
private final Map<IIdType, List<String>> errorsPerMeasure = new HashMap<>();
67+
68+
public CompositeEvaluationResultsPerMeasure build() {
69+
return new CompositeEvaluationResultsPerMeasure(this);
70+
}
71+
72+
public void addResults(List<IIdType> measureIds, String subjectId, EvaluationResult evaluationResult) {
73+
for (IIdType measureId : measureIds) {
74+
addResult(measureId, subjectId, evaluationResult);
75+
}
76+
}
77+
78+
public void addResult(IIdType measureId, String subjectId, EvaluationResult evaluationResult) {
79+
// if we have no results, we don't need to add anything
80+
if (evaluationResult == null || evaluationResult.expressionResults.isEmpty()) {
81+
return;
82+
}
83+
84+
var resultPerMeasure =
85+
resultsPerMeasure.computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new HashMap<>());
86+
87+
resultPerMeasure.put(subjectId, evaluationResult);
88+
}
89+
90+
public void addErrors(List<? extends IIdType> measureIds, String error) {
91+
if (error == null || error.isEmpty()) {
92+
return;
93+
}
94+
95+
for (IIdType measureId : measureIds) {
96+
addError(measureId, error);
97+
}
98+
}
99+
100+
public void addError(IIdType measureId, String error) {
101+
if (error == null || error.isBlank()) {
102+
return;
103+
}
104+
105+
errorsPerMeasure
106+
.computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new ArrayList<>())
107+
.add(error);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)