Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3d40fb0
First attempt to flip measure and subject looping for population base…
lukedegruchy Jun 27, 2025
593ede2
Document new unit test failures.
lukedegruchy Jul 2, 2025
bee1e0f
Abandon previous experiment. Push up evaluate measure callers to the…
lukedegruchy Jul 2, 2025
bab5c88
Capture discussion with Justin and requirements but lots of compile e…
lukedegruchy Jul 2, 2025
74a1b97
Put in place changes agreed with Justin and all tests except one are …
lukedegruchy Jul 3, 2025
bc1b716
Fix CollectData unit test by making resolveParameterMap work with a m…
lukedegruchy Jul 3, 2025
07c550b
Spotless. Delete some cruft.
lukedegruchy Jul 3, 2025
f72d57b
Cleanup and new TODOs.
lukedegruchy Jul 3, 2025
aab88f9
Fix compile errors.
lukedegruchy Jul 3, 2025
0c960ff
Small tweaks and TODOs.
lukedegruchy Jul 3, 2025
49f6de3
Overhaul CompositeEvaluationResultsPerMeasure to include errors as we…
lukedegruchy Jul 3, 2025
0d589ed
Cleanup. Spotless.
lukedegruchy Jul 3, 2025
044d14b
Spotless.
lukedegruchy Jul 3, 2025
5af8cdf
Handle new MeasureProcessorUtils#setMeasurePeriod, ensure all callers…
lukedegruchy Jul 3, 2025
c6ce098
Clean up todos and ensure library checking is done in only one place.
lukedegruchy Jul 3, 2025
32a3340
More refactoring and resolve TODOs.
lukedegruchy Jul 3, 2025
957e72c
Resolve sonar warnings.
lukedegruchy Jul 3, 2025
48baa7c
Reverse a sonar change that caused errors. Fix other sonar issues. …
lukedegruchy Jul 3, 2025
14778bb
Initialize the CqlEngine context at the beginning, pass it to down to…
lukedegruchy Jul 3, 2025
4b295c2
More cleanup of dead code, commented out code, and dead parameters.
lukedegruchy Jul 3, 2025
10df06a
Merge remote-tracking branch 'origin/master' into ld-20250627-evaluat…
lukedegruchy Jul 3, 2025
f3b9075
Fix compile error. Fix more sonar. Spotless. Get rid of unused con…
lukedegruchy Jul 3, 2025
d937b34
Get rid of legacy CQL evaluation and have DSTU3 use the new code.
lukedegruchy Jul 4, 2025
626995b
Fix sonar and reverse some minor changes.
lukedegruchy Jul 4, 2025
9a56b55
Fix DSTU3 tests and resolve last sonar.
lukedegruchy Jul 4, 2025
de88779
More refactoring to DRY and to remove unused fields.
lukedegruchy Jul 4, 2025
07a678d
Small refactoring for multi-measure service.
lukedegruchy Jul 4, 2025
c4a8dc1
Optimize performance of CompositeEvaluationResultsPerMeasure.
lukedegruchy Jul 4, 2025
56f23b5
Spotless.
lukedegruchy Jul 4, 2025
c86c2c0
Small refactoring.
lukedegruchy Jul 4, 2025
48f42d5
Spotless.
lukedegruchy Jul 7, 2025
261967a
Replace all java.annotation imports with jakarta.annotation.
lukedegruchy Jul 7, 2025
17953f2
Add comment about new approach, as per code review.
lukedegruchy Jul 7, 2025
9708cfb
Slight fix to algorithm and TODO for CQL fix.
lukedegruchy Jul 8, 2025
902e3a8
Start integrating with new CQL branch with caching optimizations.
lukedegruchy Jul 10, 2025
0a79bc7
Use the same made-up version of CQL that the CQL branch is using.
lukedegruchy Jul 11, 2025
1e87752
Merge remote-tracking branch 'origin/master' into ld-20250627-evaluat…
lukedegruchy Jul 11, 2025
ebccc3b
Low risk changes including adding having CompositeEvaluationResultsPe…
lukedegruchy Jul 14, 2025
cf047b6
Grab changes from branch to integrate with CQL changes to validate li…
lukedegruchy Jul 14, 2025
9226d6b
Fix compile error.
lukedegruchy Jul 14, 2025
45ad38c
Experiment more with algorithms. Thus far, if we pass more than one…
lukedegruchy Jul 15, 2025
4566eb2
Flip to the new code from the old code in MeasureProcessorUtils. Ad…
lukedegruchy Jul 15, 2025
a8afb99
Assert that the mulitmeasure "allsubjects" tests still pass but conta…
lukedegruchy Jul 16, 2025
8e5f3c4
Merge remote-tracking branch 'origin/master' into ld-20250627-evaluat…
lukedegruchy Jul 17, 2025
b95c057
Leverage multi-measure error handling code added to CQL. Spotless. …
lukedegruchy Jul 17, 2025
d21c61a
Set custom version in the pom for now.
lukedegruchy Jul 25, 2025
d4eefb5
Add TODO. Reverse a bunch of logging changes. Ensure we don't queue…
lukedegruchy Jul 25, 2025
0a32fbb
Attempt to set up a successful complex layer library test but no succ…
lukedegruchy Jul 25, 2025
0d83941
Add unit test.
lukedegruchy Jul 25, 2025
61e5025
Change approach to 1B testing to filter based on meta profiles. Add …
lukedegruchy Jul 28, 2025
a0dd3bb
Update to a new stub version ahead of the current master version of c…
lukedegruchy Jul 29, 2025
bc34f4d
Make final changes to MultiLibEvalComplexCqlTest. Add back some logg…
lukedegruchy Jul 29, 2025
e6d0685
First set of refactoring changes.
lukedegruchy Jul 29, 2025
60d86cc
More cleanup.
lukedegruchy Jul 29, 2025
334e4f1
Start merging old and new code bases. Clean up more cruft.
lukedegruchy Jul 29, 2025
75831ee
More refactoring to merge old and new code branches. More cleanup.
lukedegruchy Jul 29, 2025
bbc2c40
Small tweaks and integrate with latest CQL branch changes.
lukedegruchy Jul 30, 2025
c213ead
Add temporary logging for db queries.
lukedegruchy Jul 30, 2025
f2a0658
Set up complex $evaluate test, which doesn't yet exercise multiple li…
lukedegruchy Jul 31, 2025
127efb6
Adapt to new CQL result handling code, notably for error and excepti…
lukedegruchy Jul 31, 2025
1a62956
Adapt to new CQL exception handling code. Comment out RepositoryLogg…
lukedegruchy Jul 31, 2025
68ce13e
Adapt to new CQL library resolution/compilation code. Add a test tha…
lukedegruchy Aug 8, 2025
8216866
Merge remote-tracking branch 'origin/master' into ld-20250627-evaluat…
lukedegruchy Aug 11, 2025
2496ed7
Fix test input directory structures to conform to new standard. Add …
lukedegruchy Aug 11, 2025
d91e540
Fix some TODOs including tests, logging, and javadoc.
lukedegruchy Aug 11, 2025
335855f
Address rest of TODOs and add another test.
lukedegruchy Aug 11, 2025
f45835b
Sync up with pom versions from master. Clean up more cruft.
lukedegruchy Aug 11, 2025
4dbebe7
Change CQL version to 3.28.0.
lukedegruchy Aug 11, 2025
fd799f4
Fix tests for new evaluatedMeasures() logic.
lukedegruchy Aug 11, 2025
c054620
Merge branch 'master' into ld-20250627-evaluate-measure-population-fl…
JPercival Aug 11, 2025
dd4c01b
Fix capitalization
JPercival Aug 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) {
}

private R4MeasureService buildMeasureService() {
return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator, measureServiceUtils);
return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator);
}

public When when() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -25,6 +26,7 @@
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.opencds.cqf.cql.engine.execution.CqlEngine;
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib;
import org.opencds.cqf.fhir.cql.engine.parameters.CqlFhirParametersConverter;
import org.opencds.cqf.fhir.cql.engine.parameters.CqlParameterDefinition;
import org.opencds.cqf.fhir.utility.CqfExpression;
Expand Down Expand Up @@ -325,8 +327,8 @@ public List<IBase> resolveExpression(
return result;
}

public EvaluationResult getEvaluationResult(
VersionedIdentifier id,
public EvaluationResultsForMultiLib getEvaluationResult(
List<VersionedIdentifier> ids,
String patientId,
IBaseParameters parameters,
Map<String, Object> rawParameters,
Expand All @@ -336,25 +338,53 @@ public EvaluationResult getEvaluationResult(
@Nullable ZonedDateTime zonedDateTime,
CqlEngine engine) {

if (cqlFhirParametersConverter == null) {
cqlFhirParametersConverter = Engines.getCqlFhirParametersConverter(repository.fhirContext());
}
var cqlFhirParametersConverterToUse = Objects.requireNonNullElseGet(
cqlFhirParametersConverter, () -> Engines.getCqlFhirParametersConverter(repository.fhirContext()));

// engine context built externally of LibraryEngine?
if (engine == null) {
engine = Engines.forRepository(repository, settings, additionalData);
}
var engineToUse = Objects.requireNonNullElseGet(
engine, () -> Engines.forRepository(repository, settings, additionalData));

var evaluationParameters = cqlFhirParametersConverter.toCqlParameters(parameters);
var evaluationParameters = cqlFhirParametersConverterToUse.toCqlParameters(parameters);
if (rawParameters != null && !rawParameters.isEmpty()) {
evaluationParameters.putAll(rawParameters);
}

return engine.evaluate(
new VersionedIdentifier().withId(id.getId()),
var versionlessIdentifiers = ids.stream()
.map(id -> new VersionedIdentifier().withId(id.getId()))
.toList();

return engineToUse.evaluate(
versionlessIdentifiers,
expressions,
buildContextParameter(patientId),
evaluationParameters,
null,
zonedDateTime);
}

public EvaluationResult getEvaluationResult(
VersionedIdentifier id,
String patientId,
IBaseParameters parameters,
Map<String, Object> rawParameters,
IBaseBundle additionalData,
Set<String> expressions,
CqlFhirParametersConverter cqlFhirParametersConverter,
@Nullable ZonedDateTime zonedDateTime,
CqlEngine engine) {

var evaluationResultsForMultiLib = getEvaluationResult(
List.of(id),
patientId,
parameters,
rawParameters,
additionalData,
expressions,
cqlFhirParametersConverter,
zonedDateTime,
engine);

return evaluationResultsForMultiLib.getOnlyResultOrThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.BundleBuilder;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.StreamSupport;
import javax.annotation.Nonnull;
import org.cqframework.cql.cql2elm.StringLibrarySourceProvider;
import org.cqframework.fhir.npm.NpmPackageManager;
import org.cqframework.fhir.npm.NpmProcessor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@
import org.opencds.cqf.fhir.cr.cli.argument.MeasureCommandArgument;
import org.opencds.cqf.fhir.cr.cli.command.CqlCommand.SubjectAndResult;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.SubjectProviderOptions;
import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor;
import org.opencds.cqf.fhir.cr.measure.r4.R4RepositorySubjectProvider;
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;

Expand Down Expand Up @@ -139,11 +137,7 @@ private static R4MeasureProcessor getR4MeasureProcessor(
evaluationOptions.setApplyScoringSetMembership(false);
evaluationOptions.setEvaluationSettings(evaluationSettings);

return new R4MeasureProcessor(
repository,
evaluationOptions,
new R4RepositorySubjectProvider(new SubjectProviderOptions()),
new R4MeasureServiceUtils(repository));
return new R4MeasureProcessor(repository, evaluationOptions, new MeasureProcessorUtils());
}

private void writeJsonToFile(String json, String patientId, Path path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ void optionsFailure() {
Main.run(args);

String errOutput = errContent.toString();
assertTrue(errOutput.contains("library FluentFunctions loaded, but had errors"));
assertTrue(errOutput.contains("Library FluentFunctions loaded, but had errors"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,8 @@ public class CrR4Config {
R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory(
IRepositoryFactory repositoryFactory,
MeasureEvaluationOptions evaluationOptions,
MeasurePeriodValidator measurePeriodValidator,
R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) {
return rd -> new R4MeasureService(
repositoryFactory.create(rd),
evaluationOptions,
measurePeriodValidator,
r4MeasureServiceUtilsFactory.create(rd));
MeasurePeriodValidator measurePeriodValidator) {
return rd -> new R4MeasureService(repositoryFactory.create(rd), evaluationOptions, measurePeriodValidator);
}

@Bean
Expand Down Expand Up @@ -90,8 +85,7 @@ ICollectDataServiceFactory collectDataServiceFactory(
IRepositoryFactory repositoryFactory,
MeasureEvaluationOptions measureEvaluationOptions,
R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) {
return rd -> new R4CollectDataService(
repositoryFactory.create(rd), measureEvaluationOptions, r4MeasureServiceUtilsFactory.create(rd));
return rd -> new R4CollectDataService(repositoryFactory.create(rd), measureEvaluationOptions);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import ca.uhn.fhir.repository.IRepository;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils;
import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider;
import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureProcessor;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor;
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
Expand All @@ -26,8 +26,7 @@ Dstu3MeasureProcessor dstu3MeasureProcessor(
R4MeasureProcessor r4MeasureProcessor(
IRepository repository,
MeasureEvaluationOptions measureEvaluationOptions,
SubjectProvider subjectProvider,
R4MeasureServiceUtils measureServiceUtils) {
return new R4MeasureProcessor(repository, measureEvaluationOptions, subjectProvider, measureServiceUtils);
MeasureProcessorUtils measureProcessorUtils) {
return new R4MeasureProcessor(repository, measureEvaluationOptions, measureProcessorUtils);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.opencds.cqf.fhir.cr.measure.common;

import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.cql.engine.execution.EvaluationResult;

/**
* Container meant to hold the results or an early and cached CQL measure evaluation that holds
* two data points:
* <ol>
* <li>To hold the results of a measure evaluation, grouped by measure ID, with each measure evaluation grouped by Subject ID</li>
* <li>To hold any errors that occurred during the evaluation, grouped by measure ID</li>
* </ol>
* These data points are not mutually exclusive, meaning a measure may have both successful results and errors.
* <p/>
* This class also allows the caller to mutate a {@link MeasureDef} with the errors that occurred during the evaluation
*/
public class CompositeEvaluationResultsPerMeasure {
// The same measure may have successful results AND errors, so account for both
private final Map<IIdType, Map<String, EvaluationResult>> resultsPerMeasure;
// We may get several errors for a given measure
private final Map<IIdType, List<String>> errorsPerMeasure;

private CompositeEvaluationResultsPerMeasure(Builder builder) {

var resultsBuilder = ImmutableMap.<IIdType, Map<String, EvaluationResult>>builder();
builder.resultsPerMeasure.forEach((key, value) -> resultsBuilder.put(key, ImmutableMap.copyOf(value)));
resultsPerMeasure = resultsBuilder.build();

var errorsBuilder = ImmutableMap.<IIdType, List<String>>builder();
builder.errorsPerMeasure.forEach((key, value) -> errorsBuilder.put(key, List.copyOf(value)));
errorsPerMeasure = errorsBuilder.build();
}

/**
* Retrieves results and populates errors for a given measure.
* This method uses direct map lookups for efficient data retrieval.
* measureDef will occasionally be prepended with the version, which means we need to parse it into an IIdType which
* is too much work, so pass in the measureId directly
*
* @param measureId the ID of the measure to process
* @param measureDef the MeasureDef to populate with errors
*
* @return a map of evaluation results per subject, or an empty map if none exist
*/
public Map<String, EvaluationResult> processMeasureForSuccessOrFailure(IIdType measureId, MeasureDef measureDef) {
var unqualifiedMeasureId = measureId.toUnqualifiedVersionless();

errorsPerMeasure.getOrDefault(unqualifiedMeasureId, List.of()).forEach(measureDef::addError);

// We are explicitly maintaining the logic of accepting the lack of any sort of results,
// either errors or successes, and returning an empty map.
return resultsPerMeasure.getOrDefault(unqualifiedMeasureId, Map.of());
}

public static Builder builder() {
return new Builder();
}

public static class Builder {
private final Map<IIdType, Map<String, EvaluationResult>> resultsPerMeasure = new HashMap<>();
private final Map<IIdType, List<String>> errorsPerMeasure = new HashMap<>();

public CompositeEvaluationResultsPerMeasure build() {
return new CompositeEvaluationResultsPerMeasure(this);
}

public void addResults(List<IIdType> measureIds, String subjectId, EvaluationResult evaluationResult) {
for (IIdType measureId : measureIds) {
addResult(measureId, subjectId, evaluationResult);
}
}

public void addResult(IIdType measureId, String subjectId, EvaluationResult evaluationResult) {
// if we have no results, we don't need to add anything
if (evaluationResult == null || evaluationResult.expressionResults.isEmpty()) {
return;
}

var resultPerMeasure =
resultsPerMeasure.computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new HashMap<>());

resultPerMeasure.put(subjectId, evaluationResult);
}

public void addErrors(List<? extends IIdType> measureIds, String error) {
if (error == null || error.isEmpty()) {
return;
}

for (IIdType measureId : measureIds) {
addError(measureId, error);
}
}

public void addError(IIdType measureId, String error) {
if (error == null || error.isBlank()) {
return;
}

errorsPerMeasure
.computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new ArrayList<>())
.add(error);
}
}
}
Loading
Loading