From 42406479555e7db3ee5becd10c7da28233033e19 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Oct 2025 17:40:57 -0400 Subject: [PATCH 01/48] Introduce changes to continuous variable observations from another branch. Currently, everything compiles but several tests fail because changes from R4MeasureDefBuilder have not yet been merged, which will be difficult to merge. --- .../CompositeEvaluationResultsPerMeasure.java | 64 +- ...ousVariableObservationAggregateMethod.java | 39 + .../ContinuousVariableObservationHandler.java | 343 ++++++++ .../cqf/fhir/cr/measure/common/GroupDef.java | 29 + .../fhir/cr/measure/common/MeasureDef.java | 40 +- .../cr/measure/common/MeasureEvaluator.java | 174 +++- .../common/MeasureObservationResult.java | 8 + .../measure/common/MeasureProcessorUtils.java | 181 ++--- .../MultiLibraryIdMeasureEngineDetails.java | 21 +- .../fhir/cr/measure/common/PopulationDef.java | 19 + .../cr/measure/constant/MeasureConstants.java | 4 + .../measure/dstu3/Dstu3MeasureProcessor.java | 10 +- .../dstu3/Dstu3MeasureReportBuilder.java | 2 - .../cr/measure/r4/R4MeasureDefBuilder.java | 1 + .../cr/measure/r4/R4MeasureProcessor.java | 31 +- .../cr/measure/r4/R4MeasureReportScorer.java | 259 +++++- .../r4/R4PopulationBasisValidator.java | 29 + ...positeEvaluationResultsPerMeasureTest.java | 36 +- .../measure/dstu3/MeasureValidationUtils.java | 2 +- ...ariableResourceMeasureObservationTest.java | 769 ++++++++++++++++++ .../cqf/fhir/cr/measure/r4/Measure.java | 59 +- .../MeasureEvaluationApplyScoringTests.java | 46 +- ...sureScoringTypeContinuousVariableTest.java | 42 +- .../cr/measure/r4/R4MeasureProcessorTest.java | 12 +- .../r4/R4MeasureReportBuilderTest.java | 4 +- .../r4/R4PopulationBasisValidatorTest.java | 13 +- ...tinuousVariableObservationBooleanBasis.cql | 44 + ...inuousVariableObservationBooleanBasis.json | 17 + ...urceMeasureObservationBooleanBasisAvg.json | 112 +++ ...ceMeasureObservationBooleanBasisCount.json | 112 +++ ...urceMeasureObservationBooleanBasisMax.json | 112 +++ ...eMeasureObservationBooleanBasisMedian.json | 112 +++ ...urceMeasureObservationBooleanBasisMin.json | 112 +++ ...urceMeasureObservationBooleanBasisSum.json | 112 +++ .../patient-1940-female-encounter-1.json | 12 + .../patient-1940-male-encounter-1.json | 12 + .../patient-1940-other-encounter-1.json | 12 + .../patient-1945-male-encounter-1.json | 12 + .../patient-1945-other-encounter-1.json | 12 + .../patient-1950-female-encounter-1.json | 12 + .../patient-1955-other-encounter-1.json | 12 + .../patient-1960-unknown-encounter-1.json | 12 + .../patient-1965-female-encounter-1.json | 12 + .../patient-1970-female-encounter-1.json | 12 + .../tests/patient/patient-1940-female.json | 6 + .../tests/patient/patient-1940-male.json | 6 + .../tests/patient/patient-1940-other.json | 6 + .../tests/patient/patient-1945-male.json | 6 + .../tests/patient/patient-1945-other.json | 6 + .../tests/patient/patient-1950-female.json | 6 + .../tests/patient/patient-1955-other.json | 6 + .../tests/patient/patient-1960-unknown.json | 6 + .../tests/patient/patient-1965-female.json | 6 + .../tests/patient/patient-1970-female.json | 6 + ...nuousVariableObservationEncounterBasis.cql | 43 + ...uousVariableObservationEncounterBasis.json | 17 + ...ceMeasureObservationEncounterBasisAvg.json | 112 +++ ...MeasureObservationEncounterBasisCount.json | 112 +++ ...ceMeasureObservationEncounterBasisMax.json | 112 +++ ...easureObservationEncounterBasisMedian.json | 112 +++ ...ceMeasureObservationEncounterBasisMin.json | 112 +++ ...ceMeasureObservationEncounterBasisSum.json | 112 +++ .../input/cql/LibrarySimple.cql | 168 ++++ .../resources/library/LibrarySimple.json | 17 + ...ntinuousVariableBooleanAllPopulations.json | 76 ++ ...VariableBooleanExtraInvalidPopulation.json | 92 +++ ...tinuousVariableBooleanGroupScoringDef.json | 82 ++ ...sVariableBooleanMissingReqdPopulation.json | 60 ++ ...sVariableBooleanProhibitedPopulations.json | 105 +++ ...tinuousVariableResourceAllPopulations.json | 76 ++ ...imalProportionResourceBasisSingleGroup.cql | 5 +- ...portionResourceBasisSingleGroupINVALID.cql | 55 ++ ...ortionResourceBasisSingleGroupINVALID.json | 17 + ...riableResourceBasisSingleGroupINVALID.json | 90 ++ ...ortionResourceBasisSingleGroupINVALID.json | 127 +++ 75 files changed, 4561 insertions(+), 291 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationAggregateMethod.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java create mode 100644 cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/cql/ContinuousVariableObservationBooleanBasis.cql create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/library/ContinuousVariableObservationBooleanBasis.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisAvg.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisCount.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMax.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMedian.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMin.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisSum.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-female-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-other-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-male-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-other-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1950-female-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1955-other-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1960-unknown-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1965-female-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1970-female-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-female.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-other.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-male.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-other.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1950-female.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1955-other.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1960-unknown.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1965-female.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1970-female.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/cql/ContinuousVariableObservationEncounterBasis.cql create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/library/ContinuousVariableObservationEncounterBasis.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisAvg.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisCount.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMax.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMedian.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMin.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisSum.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/cql/LibrarySimple.cql create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/library/LibrarySimple.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanAllPopulations.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanExtraInvalidPopulation.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanGroupScoringDef.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanMissingReqdPopulation.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanProhibitedPopulations.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableResourceAllPopulations.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroupINVALID.cql create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/library/MinimalProportionResourceBasisSingleGroupINVALID.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalContinuousVariableResourceBasisSingleGroupINVALID.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalProportionResourceBasisSingleGroupINVALID.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java index f876539b47..df3dcd76de 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java @@ -5,7 +5,6 @@ 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; /** @@ -21,17 +20,17 @@ */ public class CompositeEvaluationResultsPerMeasure { // The same measure may have successful results AND errors, so account for both - private final Map> resultsPerMeasure; + private final Map> resultsPerMeasure; // We may get several errors for a given measure - private final Map> errorsPerMeasure; + private final Map> errorsPerMeasure; private CompositeEvaluationResultsPerMeasure(Builder builder) { - var resultsBuilder = ImmutableMap.>builder(); + var resultsBuilder = ImmutableMap.>builder(); builder.resultsPerMeasure.forEach((key, value) -> resultsBuilder.put(key, ImmutableMap.copyOf(value))); resultsPerMeasure = resultsBuilder.build(); - var errorsBuilder = ImmutableMap.>builder(); + var errorsBuilder = ImmutableMap.>builder(); builder.errorsPerMeasure.forEach((key, value) -> errorsBuilder.put(key, List.copyOf(value))); errorsPerMeasure = errorsBuilder.build(); } @@ -39,22 +38,17 @@ private CompositeEvaluationResultsPerMeasure(Builder builder) { /** * 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 processMeasureForSuccessOrFailure(IIdType measureId, MeasureDef measureDef) { - var unqualifiedMeasureId = measureId.toUnqualifiedVersionless(); - - errorsPerMeasure.getOrDefault(unqualifiedMeasureId, List.of()).forEach(measureDef::addError); + public Map processMeasureForSuccessOrFailure(MeasureDef measureDef) { + errorsPerMeasure.getOrDefault(measureDef, 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()); + return resultsPerMeasure.getOrDefault(measureDef, Map.of()); } /** @@ -63,7 +57,7 @@ public Map processMeasureForSuccessOrFailure(IIdType m * and associated EvaluationResult produced from CQL expression evaluation * @return {@code Map>} */ - public Map> getResultsPerMeasure() { + public Map> getResultsPerMeasure() { return this.resultsPerMeasure; } @@ -72,7 +66,7 @@ public Map> getResultsPerMeasure() { * When an error is produced while evaluating, we capture the errors generated in this object, which can be rendered per Measure evaluated. * @return {@code Map>} */ - public Map> getErrorsPerMeasure() { + public Map> getErrorsPerMeasure() { return this.errorsPerMeasure; } @@ -81,49 +75,59 @@ public static Builder builder() { } public static class Builder { - private final Map> resultsPerMeasure = new HashMap<>(); - private final Map> errorsPerMeasure = new HashMap<>(); + private final Map> resultsPerMeasure = new HashMap<>(); + private final Map> errorsPerMeasure = new HashMap<>(); public CompositeEvaluationResultsPerMeasure build() { return new CompositeEvaluationResultsPerMeasure(this); } - public void addResults(List measureIds, String subjectId, EvaluationResult evaluationResult) { - for (IIdType measureId : measureIds) { - addResult(measureId, subjectId, evaluationResult); + public void addResults( + List measureDefs, + String subjectId, + EvaluationResult evaluationResult, + List measureObservationResults) { + for (MeasureDef measureDef : measureDefs) { + addResult(measureDef, subjectId, evaluationResult, measureObservationResults); } } - public void addResult(IIdType measureId, String subjectId, EvaluationResult evaluationResult) { + public void addResult( + MeasureDef measureDef, + String subjectId, + EvaluationResult evaluationResult, + List measureObservationResults) { + // 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<>()); + // Mutate the evaluationResults to include continuous variable evaluation results + measureObservationResults.forEach(measureObservationResult -> evaluationResult.expressionResults.put( + measureObservationResult.expressionName(), measureObservationResult.expressionResult())); + + var resultPerMeasure = resultsPerMeasure.computeIfAbsent(measureDef, k -> new HashMap<>()); resultPerMeasure.put(subjectId, evaluationResult); } - public void addErrors(List measureIds, String error) { + public void addErrors(List measureDefs, String error) { if (error == null || error.isEmpty()) { return; } - for (IIdType measureId : measureIds) { - addError(measureId, error); + for (MeasureDef measureDef : measureDefs) { + addError(measureDef, error); } } - public void addError(IIdType measureId, String error) { + public void addError(MeasureDef measureDef, String error) { if (error == null || error.isBlank()) { return; } - errorsPerMeasure - .computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new ArrayList<>()) - .add(error); + errorsPerMeasure.computeIfAbsent(measureDef, k -> new ArrayList<>()).add(error); } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationAggregateMethod.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationAggregateMethod.java new file mode 100644 index 0000000000..439c6d339c --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationAggregateMethod.java @@ -0,0 +1,39 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import jakarta.annotation.Nullable; + +/** + * All continuous variable scoring aggregation methods. + */ +public enum ContinuousVariableObservationAggregateMethod { + AVG("avg"), + COUNT("count"), + MAX("max"), + MEDIAN("median"), + MIN("min"), + SUM("sum"), + N_A(null); + + @Nullable + private final String text; + + ContinuousVariableObservationAggregateMethod(@Nullable String text) { + this.text = text; + } + + @Nullable + public String getText() { + return text; + } + + @Nullable + public static ContinuousVariableObservationAggregateMethod fromString(@Nullable String text) { + for (ContinuousVariableObservationAggregateMethod value : values()) { + if (text.equals(value.getText())) { + return value; + } + } + + return null; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java new file mode 100644 index 0000000000..81797c76ad --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -0,0 +1,343 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.cqframework.cql.cql2elm.model.CompiledLibrary; +import org.hl7.elm.r1.FunctionDef; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Quantity; +import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.cql.engine.execution.ExpressionResult; +import org.opencds.cqf.cql.engine.execution.Libraries; +import org.opencds.cqf.cql.engine.execution.Variable; + +/** + * Capture all logic for measure evaluation for continuous variable scoring. + */ +public class ContinuousVariableObservationHandler { + + private ContinuousVariableObservationHandler() { + // static class with private constructor + } + + // LUKETODO: refactor this a lot + static List continuousVariableEvaluation( + CqlEngine context, + List measureDefs, + List libraryIdentifiers, + EvaluationResult evaluationResult, + String subjectTypePart) { + + final List finalResults = new ArrayList<>(); + + final List measureDefsWithMeasureObservations = measureDefs.stream() + // if measure contains measure-observation, otherwise short circuit + .filter(MeasureProcessorUtils::hasMeasureObservation) + .toList(); + + if (measureDefsWithMeasureObservations.isEmpty()) { + // Don't need to do anything if there are no measure observations to process + return finalResults; + } + + // measure Observation Path, have to re-initialize everything again + // TODO: extend library evaluated context so library initialization isn't having to be built for + // both, and takes advantage of caching + // LUKETODO: figure out how to reuse this pattern: + var compiledLibraries = MeasureProcessorUtils.getCompiledLibraries(libraryIdentifiers, context); + + var libraries = + compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); + + // Add back the libraries to the stack, since we popped them off during CQL + context.getState().init(libraries); + + // Measurement Period: operation parameter defined measurement period + // this necessary? + // Interval measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); + + // setMeasurementPeriod( + // measurementPeriodParams, + // context, + // Optional.ofNullable(measureDef.).map(List::of).orElse(List.of("Unknown + // Measure URL"))); + // one Library may be linked to multiple Measures + for (MeasureDef measureDefWithMeasureObservations : measureDefsWithMeasureObservations) { + + // get function for measure-observation from populationDef + for (GroupDef groupDef : measureDefWithMeasureObservations.groups()) { + + final List measureObservationPopulations = groupDef.populations().stream() + .filter(populationDef -> MeasurePopulationType.MEASUREOBSERVATION.equals(populationDef.type())) + .toList(); + for (PopulationDef populationDef : measureObservationPopulations) { + // each measureObservation is evaluated + var results = processMeasureObservation( + context, evaluationResult, subjectTypePart, groupDef, populationDef); + + finalResults.addAll(results); + } + } + } + + return finalResults; + } + + // LUKETODO: javadoc + private static List processMeasureObservation( + CqlEngine context, + EvaluationResult evaluationResult, + String subjectTypePart, + GroupDef groupDef, + PopulationDef populationDef) { + // get criteria input for results to get (measure-population, numerator, + // denominator) + var criteriaPopulationId = populationDef.getCriteriaReference(); + // function that will be evaluated + var observationExpression = populationDef.expression(); + // get expression from criteriaPopulation reference + String criteriaExpressionInput = groupDef.populations().stream() + .filter(populationDefInner -> populationDefInner.id().equals(criteriaPopulationId)) + .map(PopulationDef::expression) + .findFirst() + .orElse(null); + Optional optExpressionResult = + tryGetExpressionResult(criteriaExpressionInput, evaluationResult); + + if (optExpressionResult.isEmpty()) { + return List.of(); + } + + final ExpressionResult expressionResult = optExpressionResult.get(); + // makes expression results iterable + var resultsIter = getResultIterable(evaluationResult, expressionResult, subjectTypePart); + // make new expression name for uniquely extracting results + // this will be used in MeasureEvaluator + var expressionName = criteriaPopulationId + "-" + observationExpression; + // loop through measure-population results + int i = 0; + Map functionResults = new HashMap<>(); + Set evaluatedResources = new HashSet<>(); + final List results = new ArrayList<>(); + for (Object result : resultsIter) { + Object observationResult = evaluateObservationCriteria( + result, observationExpression, evaluatedResources, groupDef.isBooleanBasis(), context); + + if (!(observationResult instanceof String + || observationResult instanceof Integer + || observationResult instanceof Double)) { + throw new IllegalArgumentException( + "continuous variable observation CQL \"MeasureObservation\" function result must be of type String, Integer or Double but was: " + + result.getClass().getSimpleName()); + } + + var observationId = expressionName + "-" + i; + // wrap result in Observation resource to avoid duplicate results data loss + // in set object + Observation observation = wrapResultAsObservation(observationId, observationId, observationResult); + // add function results to existing EvaluationResult under new expression + // name + // need a way to capture input parameter here too, otherwise we have no way + // to connect input objects related to output object + // key= input parameter to function + // value= the output Observation resource containing calculated value + functionResults.put(result, observation); + + results.add(new MeasureObservationResult( + expressionName, new ExpressionResult(functionResults, evaluatedResources))); + } + + return results; + } + + /** + * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. + * This method is a downstream calculation given it requires calculated results before it can be called. + * Results are then added to associated MeasureDef + * @param measureDef measure defined objects that are populated from criteria expression results + * @param context cql engine context used to evaluate results + */ + public static void continuousVariableObservation(MeasureDef measureDef, CqlEngine context) { + // Continuous Variable? + for (GroupDef groupDef : measureDef.groups()) { + // Measure Observation defined? + if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) + && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { + + PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); + PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); + + // Inject MeasurePopulation results into Measure Observation Function + Map> subjectResources = measurePopulation.getSubjectResources(); + + for (Map.Entry> entry : subjectResources.entrySet()) { + String subjectId = entry.getKey(); + Set resourcesForSubject = entry.getValue(); + + for (Object resource : resourcesForSubject) { + Object observationResult = evaluateObservationCriteria( + resource, + measureObservation.expression(), + measureObservation.getEvaluatedResources(), + groupDef.isBooleanBasis(), + context); + measureObservation.addResource(observationResult); + measureObservation.addResource(subjectId, observationResult); + } + } + } + } + } + + /** + * method used to evaluate cql expression defined for 'continuous variable' scoring type measures that have 'measure observation' to calculate + * This method is called as a second round of processing given it uses 'measure population' results as input data for function + * @param resource object that stores results of cql + * @param criteriaExpression expression name to call + * @param outEvaluatedResources set to store evaluated resources touched + * @param isBooleanBasis the type of result created from expression + * @param context cql engine context used to evaluate expression + * @return cql results for subject requested + */ + @SuppressWarnings({"deprecation", "removal"}) + private static Object evaluateObservationCriteria( + Object resource, + String criteriaExpression, + Set outEvaluatedResources, + boolean isBooleanBasis, + CqlEngine context) { + + var ed = Libraries.resolveExpressionRef( + criteriaExpression, context.getState().getCurrentLibrary()); + + if (!(ed instanceof FunctionDef functionDef)) { + throw new InvalidRequestException( + "Measure observation %s does not reference a function definition".formatted(criteriaExpression)); + } + + Object result; + context.getState().pushActivationFrame(functionDef, functionDef.getContext()); + try { + if (!isBooleanBasis) { + // subject based observations don't have a parameter to pass in + context.getState() + .push(new Variable(functionDef.getOperand().get(0).getName()).withValue(resource)); + } + result = context.getEvaluationVisitor().visitExpression(ed.getExpression(), context.getState()); + + } finally { + context.getState().popActivationFrame(); + } + + captureEvaluatedResources(outEvaluatedResources, context); + + return result; + } + + private static Optional tryGetExpressionResult( + String expressionName, EvaluationResult evaluationResult) { + // LUKETODO: add more context to this exception + if (expressionName == null) { + throw new InvalidRequestException("expressionName is null"); + } + + if (evaluationResult == null) { + return Optional.empty(); + } + + final Map expressionResults = evaluationResult.expressionResults; + + if (!expressionResults.containsKey(expressionName)) { + throw new InvalidRequestException("Could not find expression result for expression: " + expressionName); + } + + return Optional.of(evaluationResult.expressionResults.get(expressionName)); + } + + private static Iterable getResultIterable( + EvaluationResult evaluationResult, ExpressionResult expressionResult, String subjectTypePart) { + if (expressionResult.value() instanceof Boolean) { + if ((Boolean.TRUE.equals(expressionResult.value()))) { + // if Boolean, returns context by SubjectType + Object booleanResult = + evaluationResult.forExpression(subjectTypePart).value(); + // remove evaluated resources + return Collections.singletonList(booleanResult); + } else { + // false result shows nothing + return Collections.emptyList(); + } + } + + Object value = expressionResult.value(); + if (value instanceof Iterable iterable) { + return iterable; + } else { + return Collections.singletonList(value); + } + } + + /** + * method used to extract evaluated resources touched by CQL criteria expressions + * @param outEvaluatedResources set object used to capture resources touched + * @param context cql engine context + */ + private static void captureEvaluatedResources(Set outEvaluatedResources, CqlEngine context) { + if (outEvaluatedResources != null && context.getState().getEvaluatedResources() != null) { + outEvaluatedResources.addAll(context.getState().getEvaluatedResources()); + } + clearEvaluatedResources(context); + } + + // reset evaluated resources followed by a context evaluation + private static void clearEvaluatedResources(CqlEngine context) { + context.getState().clearEvaluatedResources(); + } + + private static Observation wrapResultAsObservation(String id, String observationName, Object result) { + + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + obs.setId(id); + CodeableConcept cc = new CodeableConcept(); + cc.setText(observationName); + obs.setValue(convertToQuantity(result)); + obs.setCode(cc); + + return obs; + } + + private static Quantity convertToQuantity(Object obj) { + if (obj == null) return null; + + Quantity q = new Quantity(); + + if (obj instanceof Quantity existing) { + return existing; + } else if (obj instanceof Number number) { + q.setValue(number.doubleValue()); + } else if (obj instanceof String s) { + try { + q.setValue(Double.parseDouble(s)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("String is not a valid number: " + s, e); + } + } else { + throw new IllegalArgumentException("Cannot convert object of type " + obj.getClass() + " to Quantity"); + } + + return q; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java index b8a5f09a30..68d9c24cdb 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.common; +import jakarta.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; @@ -16,6 +17,7 @@ public class GroupDef { private final CodeDef populationBasis; private final CodeDef improvementNotation; private final Map> populationIndex; + private final ContinuousVariableObservationAggregateMethod aggregateMethod; public GroupDef( String id, @@ -26,6 +28,28 @@ public GroupDef( boolean isGroupImprovementNotation, CodeDef improvementNotation, CodeDef populationBasis) { + this( + id, + code, + stratifiers, + populations, + measureScoring, + isGroupImprovementNotation, + improvementNotation, + populationBasis, + null); + } + + public GroupDef( + String id, + ConceptDef code, + List stratifiers, + List populations, + MeasureScoring measureScoring, + boolean isGroupImprovementNotation, + CodeDef improvementNotation, + CodeDef populationBasis, + @Nullable ContinuousVariableObservationAggregateMethod aggregateMethod) { // this.id = id; this.code = code; @@ -36,6 +60,7 @@ public GroupDef( this.isGroupImpNotation = isGroupImprovementNotation; this.improvementNotation = improvementNotation; this.populationBasis = populationBasis; + this.aggregateMethod = aggregateMethod; } public String id() { @@ -103,4 +128,8 @@ public CodeDef getPopulationBasis() { public CodeDef getImprovementNotation() { return this.improvementNotation; } + + public ContinuousVariableObservationAggregateMethod getAggregateMethod() { + return this.aggregateMethod; + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureDef.java index 1f8a22b40a..3b8536783c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureDef.java @@ -2,18 +2,22 @@ import java.util.ArrayList; import java.util.List; -import org.opencds.cqf.cql.engine.runtime.Interval; +import java.util.Objects; +import java.util.StringJoiner; public class MeasureDef { private final String id; private final String url; private final String version; - private Interval defaultMeasurementPeriod; private final List groups; private final List sdes; private final List errors; + public static MeasureDef fromIdAndUrl(String id, String url) { + return new MeasureDef(id, url, null, List.of(), List.of()); + } + public MeasureDef(String id, String url, String version, List groups, List sdes) { this.id = id; this.url = url; @@ -36,10 +40,6 @@ public String version() { return this.version; } - public Interval getDefaultMeasurementPeriod() { - return defaultMeasurementPeriod; - } - public List sdes() { return this.sdes; } @@ -55,4 +55,32 @@ public List errors() { public void addError(String error) { this.errors.add(error); } + + // We need to limit the contract of equality to id, url, and version only + @Override + public boolean equals(Object other) { + if (other == null || getClass() != other.getClass()) { + return false; + } + MeasureDef that = (MeasureDef) other; + return Objects.equals(id, that.id) && Objects.equals(url, that.url) && Objects.equals(version, that.version); + } + + // We need to limit the contract of equality to id, url, and version only + @Override + public int hashCode() { + return Objects.hash(id, url, version); + } + + @Override + public String toString() { + return new StringJoiner(", ", MeasureDef.class.getSimpleName() + "[", "]") + .add("id='" + id + "'") + .add("url='" + url + "'") + .add("version='" + version + "'") + .add("groups=" + groups.size()) + .add("sdes=" + sdes.size()) + .add("errors=" + errors) + .toString(); + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index dfb2b5a9e0..e804c499c3 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -5,6 +5,7 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.DENOMINATOREXCEPTION; import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.DENOMINATOREXCLUSION; import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.INITIALPOPULATION; +import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREOBSERVATION; import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATIONEXCLUSION; import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.NUMERATOR; @@ -12,9 +13,10 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; @@ -144,8 +146,25 @@ protected Iterable evaluatePopulationCriteria( protected PopulationDef evaluatePopulationMembership( String subjectType, String subjectId, PopulationDef inclusionDef, EvaluationResult evaluationResult) { - // find matching expression - var matchingResult = evaluationResult.forExpression(inclusionDef.expression()); + return evaluatePopulationMembership(subjectType, subjectId, inclusionDef, evaluationResult, null); + } + + protected PopulationDef evaluatePopulationMembership( + String subjectType, + String subjectId, + PopulationDef inclusionDef, + EvaluationResult evaluationResult, + String expression) { + // use expressionName passed in instead of criteria expression defined on populationDef + // this is mainly for measureObservation functions + + ExpressionResult matchingResult; + if (expression == null || expression.isEmpty()) { + // find matching expression + matchingResult = evaluationResult.forExpression(inclusionDef.expression()); + } else { + matchingResult = evaluationResult.forExpression(expression); + } // Add Resources from SubjectId int i = 0; @@ -283,23 +302,111 @@ protected void evaluateContinuousVariable( PopulationDef initialPopulation = groupDef.getSingle(INITIALPOPULATION); PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); PopulationDef measurePopulationExclusion = groupDef.getSingle(MEASUREPOPULATIONEXCLUSION); + PopulationDef measurePopulationObservation = groupDef.getSingle(MEASUREOBSERVATION); // Validate Required Populations are Present R4MeasureScoringTypePopulations.validateScoringTypePopulations( groupDef.populations().stream().map(PopulationDef::type).toList(), MeasureScoring.CONTINUOUSVARIABLE); initialPopulation = evaluatePopulationMembership(subjectType, subjectId, initialPopulation, evaluationResult); - if (initialPopulation.getSubjects().contains(subjectId)) { - // Evaluate Population Expressions - measurePopulation = - evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); - - if (measurePopulationExclusion != null) { - evaluatePopulationMembership( - subjectType, subjectId, groupDef.getSingle(MEASUREPOPULATIONEXCLUSION), evaluationResult); - if (applyScoring) { - // verify exclusions are in measure-population - measurePopulationExclusion.getResources().retainAll(measurePopulation.getResources()); - measurePopulationExclusion.getSubjects().retainAll(measurePopulation.getSubjects()); + measurePopulation = evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); + // Evaluate Population Expressions + measurePopulation = evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); + if (measurePopulation != null && initialPopulation != null) { + if (applyScoring) { + // verify initial-population are in measure-population + measurePopulation.getResources().retainAll(initialPopulation.getResources()); + measurePopulation.getSubjects().retainAll(initialPopulation.getSubjects()); + } + } + + if (measurePopulationExclusion != null) { + evaluatePopulationMembership( + subjectType, subjectId, groupDef.getSingle(MEASUREPOPULATIONEXCLUSION), evaluationResult); + if (applyScoring) { + // verify exclusions are in measure-population + measurePopulationExclusion.getResources().retainAll(measurePopulation.getResources()); + measurePopulationExclusion.getSubjects().retainAll(measurePopulation.getSubjects()); + } + } + if (measurePopulationObservation != null) { + // only Measure Population resources need to be removed + var expressionName = measurePopulationObservation.getCriteriaReference() + "-" + + measurePopulationObservation.expression(); + // assumes only one population + evaluatePopulationMembership( + subjectType, subjectId, groupDef.getSingle(MEASUREOBSERVATION), evaluationResult, expressionName); + if (applyScoring) { + // only measureObservations that intersect with finalized measure-population results should be retained + pruneObservationResources( + measurePopulationObservation.getResources(), measurePopulation, measurePopulationObservation); + // what about subjects? + pruneObservationSubjectResources( + measurePopulation.subjectResources, measurePopulationObservation.getSubjectResources()); + } + } + // measure Observation + // source expression result population.id-function-name? + // retainAll MeasureObservations found in MeasurePopulation + + } + /** + * Removes observation entries from measureObservation if their keys + * are not found in the corresponding measurePopulation set. + */ + @SuppressWarnings("unchecked") + public void pruneObservationSubjectResources( + Map> measurePopulation, Map> measureObservation) { + + if (measurePopulation == null || measureObservation == null) { + return; + } + + for (Iterator>> it = + measureObservation.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry> entry = it.next(); + String subjectId = entry.getKey(); + + // Cast subject's observation set to the expected type + Set> obsSet = (Set>) (Set) entry.getValue(); + + // get valid population values for this subject + Set validPopulation = measurePopulation.get(subjectId); + + if (validPopulation == null || validPopulation.isEmpty()) { + // no population for this subject -> drop the whole subject + it.remove(); + continue; + } + + // remove observations not matching population values + obsSet.removeIf(obsMap -> { + for (Object key : obsMap.keySet()) { + if (!validPopulation.contains(key)) { + return true; // remove this observation map + } + } + return false; + }); + + // if no observations remain for this subject, remove it entirely + if (obsSet.isEmpty()) { + it.remove(); + } + } + } + + protected void pruneObservationResources( + Set resources, PopulationDef measurePopulation, PopulationDef measurePopulationObservation) { + for (Object resource : resources) { + if (resource instanceof Map map) { + for (var entry : map.entrySet()) { + var measurePopResult = entry.getKey(); + if (measurePopulation != null + && !measurePopulation.getResources().contains(measurePopResult)) { + // remove observation results not found in measure population + measurePopulationObservation.getResources().remove(resource); + } } } } @@ -364,13 +471,31 @@ protected void evaluateSdes(String subjectId, List sdes, EvaluationResul } } + protected Object addStratifierResult(Object result, String subjectId) { + if (result instanceof Iterable iterable) { + var resultIter = iterable.iterator(); + if (!resultIter.hasNext()) { + result = null; + } else { + result = resultIter.next(); + } + + if (resultIter.hasNext()) { + throw new InvalidRequestException( + "stratifiers may not return multiple values for subjectId: " + subjectId); + } + } + return result; + } + protected void addStratifierComponentResult( List components, EvaluationResult evaluationResult, String subjectId) { for (StratifierComponentDef component : components) { var expressionResult = evaluationResult.forExpression(component.expression()); - Optional.ofNullable(expressionResult.value()) - .ifPresent(nonNullValue -> - component.putResult(subjectId, nonNullValue, expressionResult.evaluatedResources())); + Object result = addStratifierResult(expressionResult.value(), subjectId); + if (result != null) { + component.putResult(subjectId, result, expressionResult.evaluatedResources()); + } } } @@ -383,12 +508,13 @@ protected void evaluateStratifiers( } else { var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); - Optional.ofNullable(expressionResult) - .map(ExpressionResult::value) - .ifPresent(nonNullValue -> stratifierDef.putResult( - subjectId, // context of CQL expression ex: Patient based - nonNullValue, - expressionResult.evaluatedResources())); + Object result = addStratifierResult(expressionResult.value(), subjectId); + if (result != null) { + stratifierDef.putResult( + subjectId, // context of CQL expression ex: Patient based + result, + expressionResult.evaluatedResources()); + } } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java new file mode 100644 index 0000000000..258373b752 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import org.opencds.cqf.cql.engine.execution.ExpressionResult; + +/** + * Capture the results of continuous variable evaluation to be added to {@link org.opencds.cqf.cql.engine.execution.EvaluationResult}s + */ +public record MeasureObservationResult(String expressionName, ExpressionResult expressionResult) {} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index d2c3a75f1a..4d19e850b1 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -1,7 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.common; -import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; - import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import jakarta.annotation.Nonnull; @@ -13,9 +11,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import org.apache.commons.lang3.tuple.Pair; -import org.hl7.elm.r1.FunctionDef; +import org.cqframework.cql.cql2elm.CqlCompilerException; +import org.cqframework.cql.cql2elm.CqlIncludeException; +import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.IntervalTypeSpecifier; import org.hl7.elm.r1.NamedTypeSpecifier; import org.hl7.elm.r1.ParameterDef; @@ -23,8 +22,6 @@ 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.cql.engine.execution.Libraries; -import org.opencds.cqf.cql.engine.execution.Variable; import org.opencds.cqf.cql.engine.runtime.Date; import org.opencds.cqf.cql.engine.runtime.DateTime; import org.opencds.cqf.cql.engine.runtime.Interval; @@ -76,37 +73,6 @@ public void processResults( } } - /** - * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. - * This method is a downstream calculation given it requires calculated results before it can be called. - * Results are then added to associated MeasureDef - * @param measureDef measure defined objects that are populated from criteria expression results - * @param context cql engine context used to evaluate results - */ - public void continuousVariableObservation(MeasureDef measureDef, CqlEngine context) { - // Continuous Variable? - for (GroupDef groupDef : measureDef.groups()) { - // Measure Observation defined? - if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) - && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { - - PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); - PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); - - // Inject MeasurePopulation results into Measure Observation Function - for (Object resource : measurePopulation.getResources()) { - Object observationResult = evaluateObservationCriteria( - resource, - measureObservation.expression(), - measureObservation.getEvaluatedResources(), - groupDef.isBooleanBasis(), - context); - measureObservation.addResource(observationResult); - } - } - } - } - /** * method used to convert measurement period Interval object into ZonedDateTime * @param interval measurementPeriod interval @@ -278,67 +244,6 @@ public Date truncateDateTime(DateTime dateTime) { return new Date(odt.getYear(), odt.getMonthValue(), odt.getDayOfMonth()); } - /** - * method used to extract evaluated resources touched by CQL criteria expressions - * @param outEvaluatedResources set object used to capture resources touched - * @param context cql engine context - */ - public void captureEvaluatedResources(Set outEvaluatedResources, CqlEngine context) { - if (outEvaluatedResources != null && context.getState().getEvaluatedResources() != null) { - outEvaluatedResources.addAll(context.getState().getEvaluatedResources()); - } - clearEvaluatedResources(context); - } - - // reset evaluated resources followed by a context evaluation - private void clearEvaluatedResources(CqlEngine context) { - context.getState().clearEvaluatedResources(); - } - - /** - * method used to evaluate cql expression defined for 'continuous variable' scoring type measures that have 'measure observation' to calculate - * This method is called as a second round of processing given it uses 'measure population' results as input data for function - * @param resource object that stores results of cql - * @param criteriaExpression expression name to call - * @param outEvaluatedResources set to store evaluated resources touched - * @param isBooleanBasis the type of result created from expression - * @param context cql engine context used to evaluate expression - * @return cql results for subject requested - */ - @SuppressWarnings({"deprecation", "removal"}) - public Object evaluateObservationCriteria( - Object resource, - String criteriaExpression, - Set outEvaluatedResources, - boolean isBooleanBasis, - CqlEngine context) { - - var ed = Libraries.resolveExpressionRef( - criteriaExpression, context.getState().getCurrentLibrary()); - - if (!(ed instanceof FunctionDef functionDef)) { - throw new InvalidRequestException( - "Measure observation %s does not reference a function definition".formatted(criteriaExpression)); - } - - Object result; - context.getState().pushActivationFrame(functionDef, functionDef.getContext()); - try { - if (!isBooleanBasis) { - // subject based observations don't have a parameter to pass in - context.getState() - .push(new Variable(functionDef.getOperand().get(0).getName()).withValue(resource)); - } - result = context.getEvaluationVisitor().visitExpression(ed.getExpression(), context.getState()); - } finally { - context.getState().popActivationFrame(); - } - - captureEvaluatedResources(outEvaluatedResources, context); - - return result; - } - /** * method used to execute generate CQL results via Library $evaluate * @@ -388,19 +293,23 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( for (var libraryVersionedIdentifier : libraryIdentifiers) { validateEvaluationResultExistsForIdentifier( libraryVersionedIdentifier, evaluationResultsForMultiLib); - + // standard CQL expression results: if there are var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); - var measureIds = - multiLibraryIdMeasureEngineDetails.getMeasureIdsForLibrary(libraryVersionedIdentifier); + var measureDefs = + multiLibraryIdMeasureEngineDetails.getMeasureDefsForLibrary(libraryVersionedIdentifier); - resultsBuilder.addResults(measureIds, subjectId, evaluationResult); + final List measureObservationResults = + ContinuousVariableObservationHandler.continuousVariableEvaluation( + context, measureDefs, libraryIdentifiers, evaluationResult, subjectTypePart); + + resultsBuilder.addResults(measureDefs, subjectId, evaluationResult, measureObservationResults); Optional.ofNullable(evaluationResultsForMultiLib.getExceptionFor(libraryVersionedIdentifier)) .ifPresent(exception -> { var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted( subjectId, exception.getMessage()); - resultsBuilder.addErrors(measureIds, error); + resultsBuilder.addErrors(measureDefs, error); logger.error(error, exception); }); } @@ -408,9 +317,9 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( } catch (Exception e) { // If there's any error we didn't anticipate, catch it here: var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); - var measureIds = multiLibraryIdMeasureEngineDetails.getAllMeasureIds(); + var measureDefs = multiLibraryIdMeasureEngineDetails.getAllMeasureDefs(); - resultsBuilder.addErrors(measureIds, error); + resultsBuilder.addErrors(measureDefs, error); logger.error(error, e); } } @@ -418,10 +327,72 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( return resultsBuilder.build(); } + public static List getCompiledLibraries(List ids, CqlEngine context) { + try { + var resolvedLibraryResults = + context.getEnvironment().getLibraryManager().resolveLibraries(ids); + + var allErrors = resolvedLibraryResults.allErrors(); + if (resolvedLibraryResults.hasErrors() || ids.size() > allErrors.size()) { + return resolvedLibraryResults.allCompiledLibraries(); + } + + if (ids.size() == 1) { + final List cqlCompilerExceptions = + resolvedLibraryResults.getErrorsFor(ids.get(0)); + + if (cqlCompilerExceptions.size() == 1) { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted(ids.get(0).getId()), + cqlCompilerExceptions.get(0)); + } else { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted( + ids.get(0).getId(), + cqlCompilerExceptions.stream() + .map(CqlCompilerException::getMessage) + .reduce((s1, s2) -> s1 + "; " + s2) + .orElse("No error messages found."))); + } + } + + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted(ids, allErrors)); + + } catch (CqlIncludeException exception) { + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted( + ids.stream().map(VersionedIdentifier::getId).toList()), + exception); + } + } + /** + * Checks if a MeasureDef has at least one PopulationDef of type MEASUREOBSERVATION + * across all of its groups. + * + * @param measureDef the MeasureDef to check + * @return true if any PopulationDef in any GroupDef is MEASUREOBSERVATION + */ + public static boolean hasMeasureObservation(MeasureDef measureDef) { + if (measureDef == null || measureDef.groups() == null) { + return false; + } + + return measureDef.groups().stream() + .filter(group -> group.populations() != null) + .flatMap(group -> group.populations().stream()) + .anyMatch(pop -> pop.type() == MeasurePopulationType.MEASUREOBSERVATION); + } + private void validateEvaluationResultExistsForIdentifier( VersionedIdentifier versionedIdentifierFromQuery, EvaluationResultsForMultiLib evaluationResultsForMultiLib) { + // LUKETODO: this should hopefully be fixed with the next version of CQL var containsResults = evaluationResultsForMultiLib.containsResultsFor(versionedIdentifierFromQuery); var containsExceptions = evaluationResultsForMultiLib.containsExceptionsFor(versionedIdentifierFromQuery); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java index 10ed0f0ae8..ae2882dee9 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java @@ -4,7 +4,6 @@ import com.google.common.collect.ListMultimap; import java.util.List; import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.IIdType; import org.opencds.cqf.fhir.cql.LibraryEngine; /** @@ -13,11 +12,11 @@ */ public class MultiLibraryIdMeasureEngineDetails { private final LibraryEngine libraryEngine; - private final ListMultimap libraryIdToMeasureIds; + private final ListMultimap libraryIdToMeasureDef; private MultiLibraryIdMeasureEngineDetails(Builder builder) { this.libraryEngine = builder.libraryEngine; - this.libraryIdToMeasureIds = builder.libraryIdToMeasureIdsBuilder.build(); + this.libraryIdToMeasureDef = builder.libraryIdToMeasureDefBuilder.build(); } public LibraryEngine getLibraryEngine() { @@ -26,32 +25,32 @@ public LibraryEngine getLibraryEngine() { public List getLibraryIdentifiers() { // Assuming we want the first library identifier - return List.copyOf(libraryIdToMeasureIds.keySet()); + return List.copyOf(libraryIdToMeasureDef.keySet()); } - public List getMeasureIdsForLibrary(VersionedIdentifier libraryId) { - return libraryIdToMeasureIds.get(libraryId); + public List getMeasureDefsForLibrary(VersionedIdentifier libraryId) { + return libraryIdToMeasureDef.get(libraryId); } public static Builder builder(LibraryEngine engine) { return new Builder(engine); } - public List getAllMeasureIds() { - return List.copyOf(libraryIdToMeasureIds.values()); + public List getAllMeasureDefs() { + return List.copyOf(libraryIdToMeasureDef.values()); } public static class Builder { private final LibraryEngine libraryEngine; - private final ImmutableListMultimap.Builder libraryIdToMeasureIdsBuilder = + private final ImmutableListMultimap.Builder libraryIdToMeasureDefBuilder = ImmutableListMultimap.builder(); public Builder(LibraryEngine libraryEngine) { this.libraryEngine = libraryEngine; } - public Builder addLibraryIdToMeasureId(VersionedIdentifier libraryId, IIdType measureId) { - libraryIdToMeasureIdsBuilder.put(libraryId, measureId); + public Builder addLibraryIdToMeasureId(VersionedIdentifier libraryId, MeasureDef measureDef) { + libraryIdToMeasureDefBuilder.put(libraryId, measureDef); return this; } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java index 451eed2859..1c1253f65a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.common; +import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -11,16 +12,29 @@ public class PopulationDef { private final ConceptDef code; private final MeasurePopulationType measurePopulationType; + @Nullable + private final String criteriaReference; + protected Set evaluatedResources; protected Set resources; protected Set subjects; protected Map> subjectResources = new HashMap<>(); public PopulationDef(String id, ConceptDef code, MeasurePopulationType measurePopulationType, String expression) { + this(id, code, measurePopulationType, expression, null); + } + + public PopulationDef( + String id, + ConceptDef code, + MeasurePopulationType measurePopulationType, + String expression, + @Nullable String criteriaReference) { this.id = id; this.code = code; this.measurePopulationType = measurePopulationType; this.expression = expression; + this.criteriaReference = criteriaReference; } public MeasurePopulationType type() { @@ -75,6 +89,11 @@ public Set getResources() { return this.resources; } + @Nullable + public String getCriteriaReference() { + return this.criteriaReference; + } + public String expression() { return this.expression; } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java index 0959dcb86d..1f79bfb7d6 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java @@ -21,6 +21,10 @@ private MeasureConstants() {} public static final String URL_CODESYSTEM_MEASURE_POPULATION = "http://teminology.hl7.org/CodeSystem/measure-population"; // http://hl7.org/fhir/us/davinci-deqm/2023Jan/StructureDefinition-extension-populationReference.html + public static final String EXT_CQFM_AGGREGATE_METHOD_URL = + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod"; + public static final String EXT_CQFM_CRITERIA_REFERENCE = + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference"; public static final String EXT_DAVINCI_POPULATION_REFERENCE = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-populationReference"; // http://build.fhir.org/ig/HL7/davinci-deqm/StructureDefinition-extension-supplementalData.html diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index 3b569c79a5..f2e0e7898b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -26,6 +26,7 @@ import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationHandler; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; @@ -118,14 +119,14 @@ protected MeasureReport evaluateMeasure( // Process Criteria Expression Results measureProcessorUtils.processResults( - results.processMeasureForSuccessOrFailure(measure.getIdElement(), measureDef), + results.processMeasureForSuccessOrFailure(measureDef), measureDef, evalType, measureEvaluationOptions.getApplyScoringSetMembership(), new Dstu3PopulationBasisValidator()); } // Populate populationDefs that require MeasureDef results - measureProcessorUtils.continuousVariableObservation(measureDef, context); + ContinuousVariableObservationHandler.continuousVariableObservation(measureDef, context); // Build Measure Report with Results return new Dstu3MeasureReportBuilder() @@ -140,9 +141,10 @@ private MultiLibraryIdMeasureEngineDetails buildLibraryIdEngineDetails( final LibraryEngine libraryEngine = getLibraryEngine(parameters, libraryVersionIdentifier, context); + var measureDef = new Dstu3MeasureDefBuilder().build(measure); + return MultiLibraryIdMeasureEngineDetails.builder(libraryEngine) - .addLibraryIdToMeasureId( - new VersionedIdentifier().withId(libraryVersionIdentifier.getId()), measure.getIdElement()) + .addLibraryIdToMeasureId(new VersionedIdentifier().withId(libraryVersionIdentifier.getId()), measureDef) .build(); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilder.java index 548680dcf5..3205b0eb07 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilder.java @@ -505,8 +505,6 @@ protected MeasureReport createMeasureReport( if (measurementPeriod != null) { report.setPeriod(getPeriod(measurementPeriod)); - } else if (measureDef.getDefaultMeasurementPeriod() != null) { - report.setPeriod(getPeriod(measureDef.getDefaultMeasurementPeriod())); } report.setMeasure(new Reference(measure.getId())); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index c5bc3500bb..c5bad07d69 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -84,6 +84,7 @@ public MeasureDef build(Measure measure) { MeasurePopulationType populationType = MeasurePopulationType.fromCode( pop.getCode().getCodingFirstRep().getCode()); + // LUKETODO: start merging in changes to this class from the continuous variable branch populations.add(new PopulationDef( pop.getId(), conceptToConceptDef(pop.getCode()), diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 3056735564..3fa01399a8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -27,7 +27,6 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver; @@ -128,7 +127,8 @@ public MeasureReport evaluateMeasureResults( // Populate populationDefs that require MeasureDef results // blocking certain continuous-variable Measures due to need of CQL context - continuousVariableObservationCheck(measureDef, measure); + // LUKETODO: what do we do with this? + // continuousVariableObservationCheck(measureDef, measure); // Build Measure Report with Results return new R4MeasureReportBuilder() @@ -169,7 +169,7 @@ public MeasureReport evaluateMeasure( final IIdType measureId = measure.getIdElement().toUnqualifiedVersionless(); // populate results from Library $evaluate final Map resultForThisMeasure = - compositeEvaluationResultsPerMeasure.processMeasureForSuccessOrFailure(measureId, measureDef); + compositeEvaluationResultsPerMeasure.processMeasureForSuccessOrFailure(measureDef); measureProcessorUtils.processResults( resultForThisMeasure, @@ -178,6 +178,7 @@ public MeasureReport evaluateMeasure( this.measureEvaluationOptions.getApplyScoringSetMembership(), new R4PopulationBasisValidator()); + // LUKETODO: figure out if we still need this: var measurementPeriod = postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( measure, measureDef, periodStart, periodEnd, context); @@ -221,15 +222,11 @@ private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObser // Measurement Period: operation parameter defined measurement period Interval measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); - measureProcessorUtils.setMeasurementPeriod( - measurementPeriodParams, - context, - Optional.ofNullable(measure.getUrl()).map(List::of).orElse(List.of("Unknown Measure URL"))); - // DON'T pop the library off the stack yet, because we need it for continuousVariableObservation() // Populate populationDefs that require MeasureDef results - measureProcessorUtils.continuousVariableObservation(measureDef, context); + // LUKETODO: can we get rid of this? + // measureProcessorUtils.continuousVariableObservation(measureDef, context); // Now that we've done continuousVariableObservation(), we're safe to pop the libraries off // the stack @@ -387,8 +384,8 @@ private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails var libraryIdentifiersToMeasureIds = measures.stream() .collect(ImmutableListMultimap.toImmutableListMultimap( - this::getLibraryVersionIdentifier, // Key function - Resource::getIdElement // Value function + this::getLibraryVersionIdentifier, // key function + measure -> new R4MeasureDefBuilder().build(measure) // value function )); var libraryEngine = new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); @@ -444,7 +441,17 @@ protected MeasureReportType r4EvalTypeToReportType(MeasureEvalType measureEvalTy * @param measure resource that has desired Library * @return version identifier of Library */ - protected VersionedIdentifier getLibraryVersionIdentifier(Measure measure) { + private VersionedIdentifier getLibraryVersionIdentifier(Measure measure) { + + if (measure == null) { + throw new InvalidRequestException("Measure provided is null"); + } + + if (!measure.hasLibrary() || measure.getLibrary().isEmpty()) { + throw new InvalidRequestException( + "Measure %s does not have a primary library specified".formatted(measure.getUrl())); + } + var url = measure.getLibrary().get(0).asStringValue(); Bundle b = this.repository.search(Bundle.class, Library.class, Searches.byCanonical(url), null); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 7a3011b84e..6d40581334 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -1,18 +1,35 @@ package org.opencds.cqf.fhir.cr.measure.r4; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; +import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer; +import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; +import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; +import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Evaluation of Measure Report Data showing raw CQL criteria results compared to resulting Measure Report. @@ -67,6 +84,8 @@ */ public class R4MeasureReportScorer extends BaseMeasureReportScorer { + private static final Logger logger = LoggerFactory.getLogger(R4MeasureReportScorer.class); + private static final String NUMERATOR = "numerator"; private static final String DENOMINATOR = "denominator"; private static final String DENOMINATOR_EXCLUSION = "denominator-exclusion"; @@ -87,9 +106,11 @@ public void score(String measureUrl, MeasureDef measureDef, MeasureReport measur for (MeasureReportGroupComponent mrgc : measureReport.getGroup()) { scoreGroup( + measureUrl, getGroupMeasureScoring(mrgc, measureDef), mrgc, - getGroupDef(measureDef, mrgc).isIncreaseImprovementNotation()); + getGroupDef(measureDef, mrgc).isIncreaseImprovementNotation(), + getGroupDef(measureDef, mrgc)); } } @@ -145,11 +166,14 @@ protected MeasureScoring getGroupMeasureScoring(MeasureReportGroupComponent mrgc } protected void scoreGroup( - MeasureScoring measureScoring, MeasureReportGroupComponent mrgc, boolean isIncreaseImprovementNotation) { + String measureUrl, + MeasureScoring measureScoring, + MeasureReportGroupComponent mrgc, + boolean isIncreaseImprovementNotation, + GroupDef groupDef) { switch (measureScoring) { - case PROPORTION: - case RATIO: + case PROPORTION, RATIO: var score = calcProportionScore( getCountFromGroupPopulation(mrgc.getPopulation(), NUMERATOR) - getCountFromGroupPopulation(mrgc.getPopulation(), NUMERATOR_EXCLUSION), @@ -166,36 +190,239 @@ protected void scoreGroup( } } break; + + case CONTINUOUSVARIABLE: + scoreContinuousVariable(measureUrl, mrgc, groupDef); + break; default: break; } for (MeasureReportGroupStratifierComponent stratifierComponent : mrgc.getStratifier()) { - scoreStratifier(measureScoring, stratifierComponent); + scoreStratifier(measureUrl, groupDef, measureScoring, stratifierComponent); + } + } + + protected void scoreContinuousVariable(String measureUrl, MeasureReportGroupComponent mrgc, GroupDef groupDef) { + logger.info("1234: scoreContinuousVariable"); + final Quantity aggregateQuantity = + calculateContinuousVariableAggregateQuantity(measureUrl, groupDef, PopulationDef::getResources); + + mrgc.setMeasureScore(aggregateQuantity); + } + + @Nullable + private static Quantity calculateContinuousVariableAggregateQuantity( + String measureUrl, GroupDef groupDef, Function> popDefToResources) { + + var popDef = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); + if (popDef == null) { + // In the case where we're missing a measure population definition, we don't want to + // throw an Exception, but we want the existing error handling to include this + // error in the MeasureReport output. + logger.warn("Measure population group has no measure population defined for measure: {}", measureUrl); + return null; } + + return calculateContinuousVariableAggregateQuantity( + groupDef.getAggregateMethod(), popDefToResources.apply(popDef)); + } + + @Nullable + private static Quantity calculateContinuousVariableAggregateQuantity( + ContinuousVariableObservationAggregateMethod aggregateMethod, Set qualifyingResources) { + var observationQuantity = collectQuantities(qualifyingResources); + return aggregate(observationQuantity, aggregateMethod); } - protected void scoreStratum(MeasureScoring measureScoring, StratifierGroupComponent stratum) { + private static Quantity aggregate(List quantities, ContinuousVariableObservationAggregateMethod method) { + if (quantities == null || quantities.isEmpty()) { + return null; + } + + if (ContinuousVariableObservationAggregateMethod.N_A == method) { + throw new InvalidRequestException( + "Aggregate method must be provided for continuous variable scoring, but is NO-OP."); + } + + // assume all quantities share the same unit/system/code + Quantity base = quantities.get(0); + String unit = base.getUnit(); + String system = base.getSystem(); + String code = base.getCode(); + + double result; + + switch (method) { + case SUM: + result = quantities.stream() + .mapToDouble(q -> q.getValue().doubleValue()) + .sum(); + break; + case MAX: + result = quantities.stream() + .mapToDouble(q -> q.getValue().doubleValue()) + .max() + .orElse(Double.NaN); + break; + case MIN: + result = quantities.stream() + .mapToDouble(q -> q.getValue().doubleValue()) + .min() + .orElse(Double.NaN); + break; + case AVG: + result = quantities.stream() + .mapToDouble(q -> q.getValue().doubleValue()) + .average() + .orElse(Double.NaN); + break; + case COUNT: + result = quantities.size(); + break; + case MEDIAN: + List sorted = quantities.stream() + .map(q -> q.getValue().doubleValue()) + .sorted() + .toList(); + int n = sorted.size(); + if (n % 2 == 1) { + result = sorted.get(n / 2); + } else { + result = (sorted.get(n / 2 - 1) + sorted.get(n / 2)) / 2.0; + } + break; + default: + throw new IllegalArgumentException("Unsupported aggregation method: " + method); + } + + return new Quantity().setValue(result).setUnit(unit).setSystem(system).setCode(code); + } + + private static List collectQuantities(Set resources) { + List quantities = new ArrayList<>(); + + for (Object resource : resources) { + if (resource instanceof Map map) { + for (Object value : map.values()) { + if (value instanceof Observation obs && obs.hasValueQuantity()) { + quantities.add(obs.getValueQuantity()); + } + } + } + } + + return quantities; + } + + protected void scoreStratifier( + String measureUrl, + GroupDef groupDef, + MeasureScoring measureScoring, + MeasureReportGroupStratifierComponent stratifierComponent) { + for (StratifierGroupComponent sgc : stratifierComponent.getStratum()) { + scoreStratum(measureUrl, groupDef, measureScoring, sgc); + } + } + + protected void scoreStratum( + String measureUrl, GroupDef groupDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { + final Quantity quantity = getStratumScoreOrNull(measureUrl, groupDef, measureScoring, stratum); + + if (quantity != null) { + stratum.setMeasureScore(quantity); + } + } + + @Nullable + private Quantity getStratumScoreOrNull( + String measureUrl, GroupDef groupDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { + switch (measureScoring) { - case PROPORTION: - case RATIO: + case PROPORTION, RATIO -> { var score = calcProportionScore( getCountFromStratifierPopulation(stratum.getPopulation(), NUMERATOR), getCountFromStratifierPopulation(stratum.getPopulation(), DENOMINATOR)); + if (score != null) { - stratum.setMeasureScore(new Quantity(score)); + return new Quantity(score); } - break; - default: - break; + return null; + } + case CONTINUOUSVARIABLE -> { + final List stratifiers = groupDef.stratifiers(); + + if (CollectionUtils.isEmpty(stratifiers)) { + return null; + } + + // LUKETODO: what if we have more than one stratifier?????? + final StratifierDef stratifierDef = stratifiers.get(0); + + return calculateContinuousVariableAggregateQuantity( + measureUrl, + groupDef, + populationDef -> getResultsForStratum(populationDef, stratifierDef, stratum)); + } + default -> { + return null; + } } } - protected void scoreStratifier( - MeasureScoring measureScoring, MeasureReportGroupStratifierComponent stratifierComponent) { - for (StratifierGroupComponent sgc : stratifierComponent.getStratum()) { - scoreStratum(measureScoring, sgc); + /* + the existing algo takes the measure-observation population from the group definition and goes through all resources to get the quantities + MeasurePopulationType.MEASUREOBSERVATION + + but we don't want that: we want to filter only resources that belong to the patients captured by each stratum + so we want to do some sort of wizardry that involves getting the stratum values, and using those to retrieve the associated resources + */ + private Set getResultsForStratum( + PopulationDef measureObservationPopulationDef, + StratifierDef stratifierDef, + StratifierGroupComponent stratum) { + final String stratumValue = stratum.getValue().getText(); + + final Set subjectsWithStratumValue = stratifierDef.getResults().entrySet().stream() + .filter(entry -> doesStratumMatch(stratumValue, entry.getValue().rawValue())) + .map(Entry::getKey) + .collect(Collectors.toUnmodifiableSet()); + + logger.info("1234: stratum value: {}, qualifying subjects: {}", stratumValue, subjectsWithStratumValue); + + final Set qualifyingStratumResources = + measureObservationPopulationDef.getSubjectResources().entrySet().stream() + .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) + .map(Entry::getValue) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableSet()); + + logger.info( + "1234: stratum value: {}, qualifying subjects: {}, qualifyingStratumResources: {}", + stratumValue, + subjectsWithStratumValue, + qualifyingStratumResources); + + return qualifyingStratumResources; + } + + // LUKETODO: do we need to address more use cases? + private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { + if (rawValueFromStratifier == null || stratumValueAsString == null) { + return false; } + + if (rawValueFromStratifier instanceof Integer rawValueFromStratifierAsInt) { + final int stratumValueAsInt = Integer.parseInt(stratumValueAsString); + + return stratumValueAsInt == rawValueFromStratifierAsInt; + } + + if (rawValueFromStratifier instanceof Enumeration rawValueFromStratifierAsEnumeration) { + return stratumValueAsString.equals(rawValueFromStratifierAsEnumeration.asStringValue()); + } + + return false; } private int getCountFromGroupPopulation( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index 399280a4ae..2a427d0936 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -3,10 +3,14 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Enumeration; @@ -187,6 +191,31 @@ private Optional> extractResourceType(String groupPopulationBasisCode) return Optional.empty(); } + private List> extractClassesFromSingleOrListResult(Object result) { + if (result == null) { + return Collections.emptyList(); + } + + if (!(result instanceof Iterable iterable)) { + return Collections.singletonList(result.getClass()); + } + + // Need to this to return List> and get rid of Sonar warnings. + final Stream> classStream = + getStream(iterable).filter(Objects::nonNull).map(Object::getClass); + + return classStream.toList(); + } + + private Stream getStream(Iterable iterable) { + if (iterable instanceof List list) { + return list.stream(); + } + + // It's entirely possible CQL returns an Iterable that is not a List, so we need to handle that case + return StreamSupport.stream(iterable.spliterator(), false); + } + private List prettyClassNames(List> classes) { return classes.stream().map(Class::getSimpleName).toList(); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java index b6e7c4aab6..3ce3e944bf 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java @@ -8,8 +8,6 @@ import java.util.List; import java.util.Map; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Test; import org.opencds.cqf.cql.engine.execution.EvaluationResult; @@ -18,39 +16,39 @@ class CompositeEvaluationResultsPerMeasureTest { @Test void gettersContainExpectedData() { // Arrange - IIdType m1 = new IdType("Measure/one"); - IIdType m2 = new IdType("Measure/two"); + var measureDef1 = MeasureDef.fromIdAndUrl("Measure/one", "http://example.com/Measure/one"); + var measureDef2 = MeasureDef.fromIdAndUrl("Measure/two", "http://example.com/Measure/two"); // Create a non-empty EvaluationResult without depending on ExpressionResult constructors EvaluationResult er = new EvaluationResult(); er.expressionResults.put("subject-123", null); // non-empty map is all the Builder checks CompositeEvaluationResultsPerMeasure.Builder builder = CompositeEvaluationResultsPerMeasure.builder(); - builder.addResult(m1, "subject-123", er); - builder.addError(m1, "oops-1"); - builder.addError(m2, "oops-2"); + builder.addResult(measureDef1, "subject-123", er, List.of()); + builder.addError(measureDef1, "oops-1"); + builder.addError(measureDef2, "oops-2"); CompositeEvaluationResultsPerMeasure composite = builder.build(); // Act - Map> resultsPerMeasure = composite.getResultsPerMeasure(); - Map> errorsPerMeasure = composite.getErrorsPerMeasure(); + Map> resultsPerMeasure = composite.getResultsPerMeasure(); + Map> errorsPerMeasure = composite.getErrorsPerMeasure(); // Assert: results present for m1, none for m2 - assertTrue(resultsPerMeasure.containsKey(m1.toUnqualifiedVersionless())); - assertFalse(resultsPerMeasure.containsKey(m2.toUnqualifiedVersionless())); - Map m1Results = resultsPerMeasure.get(m1.toUnqualifiedVersionless()); + assertTrue(resultsPerMeasure.containsKey(measureDef1)); + assertFalse(resultsPerMeasure.containsKey(measureDef2)); + Map m1Results = resultsPerMeasure.get(measureDef1); assertNotNull(m1Results); assertTrue(m1Results.containsKey("subject-123")); // Assert: errors present for both measures - assertEquals(List.of("oops-1"), errorsPerMeasure.get(m1.toUnqualifiedVersionless())); - assertEquals(List.of("oops-2"), errorsPerMeasure.get(m2.toUnqualifiedVersionless())); + assertEquals(List.of("oops-1"), errorsPerMeasure.get(measureDef1)); + assertEquals(List.of("oops-2"), errorsPerMeasure.get(measureDef2)); } @Test void gettersReturnImmutableViews() { - IIdType m = new IdType("Measure/immutable"); + var measureDef1 = MeasureDef.fromIdAndUrl("Measure/immutable", "http://example.com/Measure/immutable"); EvaluationResult er = new EvaluationResult(); er.expressionResults.put("s", null); @@ -59,11 +57,11 @@ void gettersReturnImmutableViews() { CompositeEvaluationResultsPerMeasure.builder().build(); // empty instance to test top-level immutability // Top-level maps should be unmodifiable - Map> resultsPerMeasure = composite.getResultsPerMeasure(); - Map> errorsPerMeasure = composite.getErrorsPerMeasure(); + Map> resultsPerMeasure = composite.getResultsPerMeasure(); + Map> errorsPerMeasure = composite.getErrorsPerMeasure(); - assertThrows(UnsupportedOperationException.class, () -> resultsPerMeasure.put(m, Map.of("s", er))); + assertThrows(UnsupportedOperationException.class, () -> resultsPerMeasure.put(measureDef1, Map.of("s", er))); - assertThrows(UnsupportedOperationException.class, () -> errorsPerMeasure.put(m, List.of("err"))); + assertThrows(UnsupportedOperationException.class, () -> errorsPerMeasure.put(measureDef1, List.of("err"))); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/MeasureValidationUtils.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/MeasureValidationUtils.java index 87fe6ac6f4..604873f36a 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/MeasureValidationUtils.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/MeasureValidationUtils.java @@ -37,8 +37,8 @@ protected static void validateGroup( protected static void validatePopulation( MeasureReport.MeasureReportGroupPopulationComponent population, int count) { assertEquals( - population.getCount(), count, + population.getCount(), "expected count for population \"%s\" did not match".formatted(population.getId())); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java new file mode 100644 index 0000000000..b6b8e4d3cb --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java @@ -0,0 +1,769 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import java.time.LocalDate; +import java.time.Month; +import java.time.Period; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; + +public class ContinuousVariableResourceMeasureObservationTest { + + private static final Given GIVEN_BOOLEAN_BASIS = + Measure.given().repositoryFor("ContinuousVariableObservationBooleanBasis"); + private static final Given GIVEN_ENCOUNTER_BASIS = + Measure.given().repositoryFor("ContinuousVariableObservationEncounterBasis"); + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisAvg() { + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisAvg") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("73.0") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("81.5") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("64.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("77.33333333333333") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("67.75") + .up() + .up() + .up() + .report(); + } + + // LUKETODO: refactor for code reuse + @Test + void continuousVariableResourceMeasureObservationEncounterBasisAvg() { + + /* + # total sum: 2536.0 + + # first stratum: 84: + + patient-0-encounter-1: 00:00 to 02:00 (120 minutes) + patient-1-encounter-1: 00:00 to 02:00 (120 minutes) + + sum: 240.0 + + # second stratum: 74: + + patient-2-encounter-1: 03:00 to 12:00 (540 minutes) + patient-3-encounter-1: 07:00 to 14:00 (420 minutes) + patient-4-encounter-1: 05:00 to 19:00 (840 minutes) + patient-5-encounter-1: 02:00 to 04:00 (120 minutes) + + sum: 1920.0 + + # third stratum: 64: + + patient-6-encounter-1: 03:00 to 03:30 (30 minutes) + patient-7-encounter-1: 01:00 to 02:30 (90 minutes) + patient-8-encounter-1: 00:00 to 00:15 (15 minutes) + patient-9-encounter-1: 01:01 to 03:02 (121 minutes) + patient-9-encounter-2: 01:00 to 03:00 (120 minutes) + + sum: 376.0 + */ + + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisAvg") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("230.54545454545453") + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("120.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("480.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("75.2") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisCount() { + /* + female: + + patient-1940-1 + patient-1950 + patient-1965 + patient-1970 + + male: + + patient-1940-2 + patient-1945-2 + + other: + + patient-1940-3 + patient-1945-1 + patient-1955 + + unknown: + patient-1960 + */ + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisCount") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("10.0") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("2.0") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("1.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("3.0") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("4.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationEncounterBasisCount() { + + /* + # total sum: 2536.0 + + # first stratum: 84: + + patient-0-encounter-1: 00:00 to 02:00 (120 minutes) + patient-1-encounter-1: 00:00 to 02:00 (120 minutes) + + sum: 240.0 + + # second stratum: 74: + + patient-2-encounter-1: 03:00 to 12:00 (540 minutes) + patient-3-encounter-1: 07:00 to 14:00 (420 minutes) + patient-4-encounter-1: 05:00 to 19:00 (840 minutes) + patient-5-encounter-1: 02:00 to 04:00 (120 minutes) + + sum: 1920.0 + + # third stratum: 64: + + patient-6-encounter-1: 03:00 to 03:30 (30 minutes) + patient-7-encounter-1: 01:00 to 02:30 (90 minutes) + patient-8-encounter-1: 00:00 to 00:15 (15 minutes) + patient-9-encounter-1: 01:01 to 03:02 (121 minutes) + patient-9-encounter-2: 01:00 to 03:00 (120 minutes) + + sum: 376.0 + */ + + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisCount") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("11.0") // I assume this is the straight-up count of encounters? + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("2.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("4.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("5.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisMedian() { + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisMedian") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("76.5") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("81.5") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("64.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("79.0") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("66.5") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationEncounterBasisMedian() { + /* + # total sum: 2536.0 + + # first stratum: 84: + + patient-0-encounter-1: 00:00 to 02:00 (120 minutes) + patient-1-encounter-1: 00:00 to 02:00 (120 minutes) + + sum: 240.0 + + # second stratum: 74: + + patient-2-encounter-1: 03:00 to 12:00 (540 minutes) + patient-3-encounter-1: 07:00 to 14:00 (420 minutes) + patient-4-encounter-1: 05:00 to 19:00 (840 minutes) + patient-5-encounter-1: 02:00 to 04:00 (120 minutes) + + sum: 1920.0 + + # third stratum: 64: + + patient-6-encounter-1: 03:00 to 03:30 (30 minutes) + patient-7-encounter-1: 01:00 to 02:30 (90 minutes) + patient-8-encounter-1: 00:00 to 00:15 (15 minutes) + patient-9-encounter-1: 01:01 to 03:02 (121 minutes) + patient-9-encounter-2: 01:00 to 03:00 (120 minutes) + + sum: 376.0 + */ + + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisMedian") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("120.0") + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("120.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("480.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("90.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisMin() { + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisMin") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("54.0") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("79.0") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("64.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("69.0") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("54.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationEncounterBasisMin() { + + /* + # total sum: 2536.0 + + # first stratum: 84: + + patient-0-encounter-1: 00:00 to 02:00 (120 minutes) + patient-1-encounter-1: 00:00 to 02:00 (120 minutes) + + sum: 240.0 + + # second stratum: 74: + + patient-2-encounter-1: 03:00 to 12:00 (540 minutes) + patient-3-encounter-1: 07:00 to 14:00 (420 minutes) + patient-4-encounter-1: 05:00 to 19:00 (840 minutes) + patient-5-encounter-1: 02:00 to 04:00 (120 minutes) + + sum: 1920.0 + + # third stratum: 64: + + patient-6-encounter-1: 03:00 to 03:30 (30 minutes) + patient-7-encounter-1: 01:00 to 02:30 (90 minutes) + patient-8-encounter-1: 00:00 to 00:15 (15 minutes) + patient-9-encounter-1: 01:01 to 03:02 (121 minutes) + patient-9-encounter-2: 01:00 to 03:00 (120 minutes) + + sum: 376.0 + */ + + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisMin") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("15.0") + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("120.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("120.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("15.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisMax() { + /* + patient-1940-female-encounter-1.json (84) + patient-1950-female-encounter-1.json (74) + patient-1965-female-encounter-1.json (59) + patient-1970-female-encounter-1.json (54) + + patient-1940-male-encounter-1.json (84) + patient-1945-male-encounter-1.json (79) + + patient-1940-other-encounter-1.json (84) + patient-1945-other-encounter-1.json (79) + patient-1955-other-encounter-1.json (69) + + patient-1960-unknown-encounter-1.json (64) + */ + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisMax") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("84.0") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("84.0") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("64.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("84.0") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("84.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationEncounterBasisMax() { + + /* + # total sum: 2536.0 + + # first stratum: 84: + + patient-0-encounter-1: 00:00 to 02:00 (120 minutes) + patient-1-encounter-1: 00:00 to 02:00 (120 minutes) + + sum: 240.0 + + # second stratum: 74: + + patient-2-encounter-1: 03:00 to 12:00 (540 minutes) + patient-3-encounter-1: 07:00 to 14:00 (420 minutes) + patient-4-encounter-1: 05:00 to 19:00 (840 minutes) + patient-5-encounter-1: 02:00 to 04:00 (120 minutes) + + sum: 1920.0 + + # third stratum: 64: + + patient-6-encounter-1: 03:00 to 03:30 (30 minutes) + patient-7-encounter-1: 01:00 to 02:30 (90 minutes) + patient-8-encounter-1: 00:00 to 00:15 (15 minutes) + patient-9-encounter-1: 01:01 to 03:02 (121 minutes) + patient-9-encounter-2: 01:00 to 03:00 (120 minutes) + + sum: 376.0 + */ + + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisMax") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("840.0") + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("120.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("840.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("121.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationBooleanBasisSum() { + + GIVEN_BOOLEAN_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationBooleanBasisSum") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + // 10 encounters in all + .hasCount(10) + .up() + .population("measure-population") + .hasCount(10) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + // There are 10 patients in all + .hasCount(10) + .up() + .hasScore("730.0") + .stratifierById("stratifier-gender") + .stratumCount(4) + .firstStratum() + .hasValue("male") + .hasScore("163.0") + .up() + .stratumByPosition(2) + .hasValue("unknown") + .hasScore("64.0") + .up() + .stratumByPosition(3) + .hasValue("other") + .hasScore("232.0") + .up() + .stratumByPosition(4) + .hasValue("female") + .hasScore("271.0") + .up() + .up() + .up() + .report(); + } + + @Test + void continuousVariableResourceMeasureObservationEncounterBasisSum() { + final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); + + final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); + final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); + final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); + + GIVEN_ENCOUNTER_BASIS + .when() + .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisSum") + .evaluate() + .then() + .firstGroup() + .population("initial-population") + .hasCount(11) + .up() + .population("measure-population") + .hasCount(11) + .up() + .population("measure-population-exclusion") + .hasCount(0) + .up() + .population("measure-observation") + .hasCount(11) + .up() + .hasScore("2536.0") + .stratifierById("stratifier-age") + .stratumCount(3) + .firstStratum() + .hasValue(Integer.toString(expectedAgeStratum1)) + .hasScore("240.0") + .up() + .stratumByPosition(2) + .hasValue(Integer.toString(expectedAgeStratum2)) + .hasScore("1920.0") + .up() + .stratumByPosition(3) + .hasValue(Integer.toString(expectedAgeStratum3)) + .hasScore("376.0") + .up() + .up() + .up() + .report(); + } + + int computeAge(LocalDate measurementPeriod, LocalDate birthDate) { + return Period.between(birthDate, measurementPeriod).getYears(); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index 3c9fd50b61..85aef0b4e0 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -307,7 +308,7 @@ public SelectedGroup group(String id) { return this.group(x -> x.getGroup().stream() .filter(g -> g.getId().equals(id)) .findFirst() - .get()); + .orElse(null)); } public SelectedGroup group(Selector groupSelector) { @@ -324,7 +325,7 @@ public SelectedReference evaluatedResource(String name) { return this.reference(x -> x.getEvaluatedResource().stream() .filter(y -> y.getReference().equals(name)) .findFirst() - .get()); + .orElse(null)); } public SelectedReport hasEvaluatedResourceCount(int count) { @@ -588,10 +589,10 @@ public SelectedReport subjectResultsHaveResourceType(String resourceType) { /** * This method is a top level validation that all subjectResult lists accurately represent population counts - * + *

* This gets all contained Lists and checks for a matching reference on a report population * It then checks that each population.count matches the size of the List (ex population.count=10, subjectResult list has 10 items) - * @return + * @return report containing more chained methods */ public SelectedReport subjectResultsValidation() { List contained = getContainedIdsPerResourceType(ResourceType.List); @@ -752,7 +753,7 @@ private static String formatDate(Date javaUtilDate) { } } - static class SelectedExtension extends Selected { + public static class SelectedExtension extends Selected { public SelectedExtension(Extension value, SelectedReport parent) { super(value, parent); @@ -774,7 +775,7 @@ public SelectedExtension extensionHasSDEId(String id) { } } - static class SelectedContained extends Selected { + public static class SelectedContained extends Selected { public SelectedContained(Resource value, SelectedReport parent) { super(value, parent); @@ -859,12 +860,12 @@ public SelectedGroup hasDateOfCompliance() { .get(0) .getValue() .isEmpty()); - assertTrue( + assertInstanceOf( + Period.class, this.value() - .getExtensionsByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) - .get(0) - .getValue() - instanceof Period); + .getExtensionsByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) + .get(0) + .getValue()); return this; } @@ -897,10 +898,14 @@ public SelectedStratifier firstStratifier() { } public SelectedStratifier stratifierById(String stratId) { - return this.stratifier(g -> g.getStratifier().stream() + final SelectedStratifier stratifier = this.stratifier(g -> g.getStratifier().stream() .filter(t -> t.getId().equals(stratId)) .findFirst() - .get()); + .orElse(null)); + + assertNotNull(stratifier); + + return stratifier; } public SelectedStratifier stratifier( @@ -909,7 +914,7 @@ public SelectedStratifier stratifier( return new SelectedStratifier(s, this); } - static class SelectedReference extends Selected { + public static class SelectedReference extends Selected { public SelectedReference(Reference value, SelectedReport parent) { super(value, parent); @@ -1015,6 +1020,18 @@ public SelectedStratifier hasStratumCount(int stratumCount) { return this; } + // Position is the numerical position starting at 1 for the first + public SelectedStratum stratumByPosition(int position) { + assertTrue(value().getStratum().size() >= position && position > 0); + + return new SelectedStratum(value().getStratum().get(position - 1), this); + } + + public SelectedStratifier stratumCount(int stratumCount) { + assertEquals(stratumCount, value().getStratum().size()); + return this; + } + public SelectedStratifier hasStratum(String textValue) { final SelectedStratum stratum = stratum(textValue); assertNotNull(stratum.value()); @@ -1025,7 +1042,7 @@ public SelectedStratum stratum(CodeableConcept value) { return stratum(s -> s.getStratum().stream() .filter(x -> x.hasValue() && x.getValue().equalsDeep(value)) .findFirst() - .get()); + .orElse(null)); } public SelectedStratum stratum(String textValue) { @@ -1041,7 +1058,7 @@ public SelectedStratum stratumByComponentValueText(String textValue) { .filter(x -> x.getComponent().stream() .anyMatch(t -> t.getValue().getText().equals(textValue))) .findFirst() - .get()); + .orElse(null)); } public SelectedStratum stratumByComponentCodeText(String textValue) { @@ -1049,7 +1066,7 @@ public SelectedStratum stratumByComponentCodeText(String textValue) { .filter(x -> x.getComponent().stream() .anyMatch(t -> t.getCode().getText().equals(textValue))) .findFirst() - .get()); + .orElse(null)); } public SelectedStratum stratum( @@ -1103,13 +1120,19 @@ public SelectedStratumPopulation firstPopulation() { return population(MeasureReport.StratifierGroupComponent::getPopulationFirstRep); } + public SelectedStratum hasValue(String textValue) { + assertTrue(value().hasValue() && value().getValue().hasText()); + assertEquals(textValue, value().getValue().getText()); + return this; + } + public SelectedStratumPopulation population(String name) { return population(s -> s.getPopulation().stream() .filter(x -> x.hasCode() && x.getCode().hasCoding() && x.getCode().getCoding().get(0).getCode().equals(name)) .findFirst() - .get()); + .orElse(null)); } public SelectedStratumPopulation population( diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java index 4ddf2eb998..bdc0b51d3b 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java @@ -39,10 +39,10 @@ class MeasureEvaluationApplyScoringTests { private static final String IG_NAME_SINGLE_MEASURE = "MeasureTest"; private static final String IG_NAME_MULTI_MEASURE = "MinimalMeasureEvaluation"; - private static final Measure.Given GIVEN_1 = getGivenWithMockedRepositorySingleMeasure(); - private static final TestDataGenerator testDataGenerator = new TestDataGenerator(GIVEN_1.getRepository()); + private static final Measure.Given GIVEN_SINGLE = getGivenWithMockedRepositorySingleMeasure(); + private static final TestDataGenerator testDataGenerator = new TestDataGenerator(GIVEN_SINGLE.getRepository()); - private static final MultiMeasure.Given GIVEN_2 = getGivenWithMockedRepositoryMultiMeasure(); + private static final MultiMeasure.Given GIVEN_MULTI = getGivenWithMockedRepositoryMultiMeasure(); @BeforeAll static void init() { @@ -55,7 +55,8 @@ static void init() { @Test void proportionResourceWithReportTypeParameterPatientGroup() { // Patients in Group - GIVEN_1.when() + GIVEN_SINGLE + .when() .measureId("ProportionResourceAllPopulations") .reportType("population") .subject("Group/group-patients-1") @@ -94,7 +95,8 @@ void proportionResourceWithReportTypeParameterPatientGroup() { @Test void proportionResourceWithReportTypeParameterPractitionerGroup() { // Patients with generalPractitioner.reference matching member of group - GIVEN_1.when() + GIVEN_SINGLE + .when() .measureId("ProportionResourceAllPopulations") .reportType("population") .subject("Group/group-practitioners-1") @@ -133,7 +135,8 @@ void proportionResourceWithReportTypeParameterPractitionerGroup() { @Test void proportionResourceWithReportTypeParameterPractitioner() { // Patients with generalPractitioner.reference matching member of group - GIVEN_1.when() + GIVEN_SINGLE + .when() .measureId("ProportionResourceAllPopulations") .reportType("population") .subject("Practitioner/practitioner-1") @@ -172,7 +175,8 @@ void proportionResourceWithReportTypeParameterPractitioner() { @Test void proportionResourceWithNoReportType() { // this should default to 'Summary' for empty subject - GIVEN_1.when() + GIVEN_SINGLE + .when() .measureId("ProportionResourceAllPopulations") .evaluate() .then() @@ -209,7 +213,8 @@ void proportionResourceWithNoReportType() { @Test void proportionResourceWithReportTypeParameterEmptySubject() { // All subjects - GIVEN_1.when() + GIVEN_SINGLE + .when() .measureId("ProportionResourceAllPopulations") .reportType("population") .evaluate() @@ -246,7 +251,8 @@ void proportionResourceWithReportTypeParameterEmptySubject() { @Test void MultiMeasure_EightMeasures_AllSubjects_MeasureUrl() { - var when = GIVEN_2.when() + var when = GIVEN_MULTI + .when() .measureUrl("http://example.com/Measure/MinimalProportionNoBasisSingleGroup") .measureUrl("http://example.com/Measure/MinimalProportionBooleanBasisSingleGroup") .measureUrl("http://example.com/Measure/MinimalRatioBooleanBasisSingleGroup") @@ -382,9 +388,29 @@ void MultiMeasure_EightMeasures_AllSubjects_MeasureUrl() { .hasCount(10); } + // This test is for a Measure that references CQL with an invalid "MeasureObservation" function that returns an + // Encounter instead of String, Integer or Double + @Test + void ContinuousVariableResourceMeasureObservationFunctionReturnsEncounterINVALID() { + GIVEN_MULTI + .when() + .measureId("MinimalContinuousVariableResourceBasisSingleGroupINVALID") + .periodStart("2024-01-01") + .periodEnd("2024-12-31") + .reportType("population") + .evaluate() + .then() + .hasMeasureReportCount(1) + .getFirstMeasureReport() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg( + "continuous variable observation CQL \"MeasureObservation\" function result must be of type String, Integer or Double but was: Encounter"); + } + @Test void MultiMeasure_EightMeasures_AllSubjects_MeasureId() { - var when = GIVEN_2.when() + var when = GIVEN_MULTI + .when() .measureId("MinimalProportionNoBasisSingleGroup") .measureId("MinimalProportionBooleanBasisSingleGroup") .measureId("MinimalRatioBooleanBasisSingleGroup") diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariableTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariableTest.java index 79012030f1..2051ed2663 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariableTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariableTest.java @@ -1,10 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.r4; -import static org.opencds.cqf.fhir.test.Resources.getResourcePath; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.repository.IRepository; -import java.nio.file.Path; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus; import org.hl7.fhir.r4.model.Period; @@ -12,7 +7,6 @@ import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; import org.opencds.cqf.fhir.cr.measure.r4.utils.TestDataGenerator; -import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; /** * the purpose of this test is to validate the output and required fields for evaluating MeasureScoring type Continuous-Variable @@ -26,13 +20,8 @@ class MeasureScoringTypeContinuousVariableTest { // resource based // boolean based // group scoring def - private static final String CLASS_PATH = "org/opencds/cqf/fhir/cr/measure/r4"; - private static final IRepository repository = new IgRepository( - FhirContext.forR4Cached(), - Path.of(getResourcePath(MeasureScoringTypeContinuousVariableTest.class) + "/" + CLASS_PATH + "/" - + "MeasureTest")); - private final Given given = Measure.given().repository(repository); - private static final TestDataGenerator testDataGenerator = new TestDataGenerator(repository); + private static final Given GIVEN = Measure.given().repositoryFor("MeasureScoringTypeContinuousVariable"); + private static final TestDataGenerator testDataGenerator = new TestDataGenerator(GIVEN.getRepository()); @BeforeAll static void init() { @@ -45,7 +34,7 @@ static void init() { @Test void continuousVariableBooleanPopulation() { - given.when() + GIVEN.when() .measureId("ContinuousVariableBooleanAllPopulations") .evaluate() .then() @@ -66,7 +55,7 @@ void continuousVariableBooleanPopulation() { @Test void continuousVariableBooleanIndividual() { - given.when() + GIVEN.when() .measureId("ContinuousVariableBooleanAllPopulations") .subject("Patient/patient-9") .evaluate() @@ -88,7 +77,7 @@ void continuousVariableBooleanIndividual() { @Test void continuousVariableResourcePopulation() { - given.when() + GIVEN.when() .measureId("ContinuousVariableResourceAllPopulations") .evaluate() .then() @@ -108,7 +97,7 @@ void continuousVariableResourcePopulation() { @Test void continuousVariableBooleanMissingRequiredPopulation() { - given.when() + GIVEN.when() .measureId("ContinuousVariableBooleanMissingReqdPopulation") .evaluate() .then() @@ -119,10 +108,23 @@ void continuousVariableBooleanMissingRequiredPopulation() { .report(); } + @Test + void continuousVariableBooleanProhibitedPopulations() { + GIVEN.when() + .measureId("ContinuousVariableBooleanProhibitedPopulations") + .evaluate() + .then() + .hasStatus(MeasureReportStatus.ERROR) + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg( + "MeasurePopulationType: denominator, is not a member of allowed 'continuous-variable' populations.") + .report(); + } + @Test void continuousVariableResourceIndividual() { - given.when() + GIVEN.when() .measureId("ContinuousVariableResourceAllPopulations") .subject("Patient/patient-9") .evaluate() @@ -143,7 +145,7 @@ void continuousVariableResourceIndividual() { @Test void continuousVariableBooleanExtraInvalidPopulation() { - given.when() + GIVEN.when() .measureId("ContinuousVariableBooleanExtraInvalidPopulation") .evaluate() .then() @@ -157,7 +159,7 @@ void continuousVariableBooleanExtraInvalidPopulation() { @Test void continuousVariableBooleanGroupScoringDef() { - given.when() + GIVEN.when() .measureId("ContinuousVariableBooleanGroupScoringDef") .evaluate() .then() diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java index 6fb6d298e4..d9ad7adfef 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java @@ -6,7 +6,6 @@ import java.util.List; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.ResourceType; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; @@ -16,8 +15,9 @@ class R4MeasureProcessorTest { private static final Given GIVEN_REPO = MultiMeasure.given().repositoryFor("MinimalMeasureEvaluation"); - private static final IdType MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP = - new IdType(ResourceType.Measure.name(), "MinimalCohortBooleanBasisSingleGroup"); + private static final MeasureDef MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP = MeasureDef.fromIdAndUrl( + "Measure/MinimalCohortBooleanBasisSingleGroup", + "http://example.com/Measure/MinimalCohortBooleanBasisSingleGroup"); private static final String SUBJECT_ID = "Patient/female-1914"; // This test could probably be improved with better data and more assertions, but it's to @@ -32,16 +32,14 @@ void evaluateMultiMeasureIdsWithCqlEngine() { var results = r4MeasureProcessor.evaluateMultiMeasureIdsWithCqlEngine( List.of(SUBJECT_ID), - List.of(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP), + List.of(new IdType(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP.id())), null, null, new Parameters(), cqlEngine); assertNotNull(results); - var measureDef = new MeasureDef("", "", "", List.of(), List.of()); - var evaluationResults = - results.processMeasureForSuccessOrFailure(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP, measureDef); + var evaluationResults = results.processMeasureForSuccessOrFailure(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP); assertNotNull(evaluationResults); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java index d4d9957c6b..a81c68d34f 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java @@ -252,7 +252,8 @@ private static GroupDef buildGroupDef(String id, Collection resources) { MeasureScoring.PROPORTION, false, null, - new CodeDef(MeasureConstants.POPULATION_BASIS_URL, "boolean")); + new CodeDef(MeasureConstants.POPULATION_BASIS_URL, "boolean"), + null); } private static PopulationDef buildPopulationRef(Collection resources) { @@ -260,6 +261,7 @@ private static PopulationDef buildPopulationRef(Collection resources) { null, new ConceptDef(List.of(new CodeDef("system", MeasurePopulationType.DATEOFCOMPLIANCE.toCode())), null), MeasurePopulationType.DATEOFCOMPLIANCE, + null, null); if (resources != null) { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java index da3e753b77..5466fefeb8 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java @@ -415,7 +415,15 @@ void validateStratifierBasisTypeErrorPath( private static GroupDef buildGroupDef( Basis basis, List populationDefs, List stratifierDefs) { return new GroupDef( - null, null, stratifierDefs, populationDefs, MeasureScoring.PROPORTION, false, null, basis.codeDef); + null, + null, + stratifierDefs, + populationDefs, + MeasureScoring.PROPORTION, + false, + null, + basis.codeDef, + null); } @Nonnull @@ -431,7 +439,8 @@ private static PopulationDef buildPopulationDef(MeasurePopulationType measurePop measurePopulationType.toCode(), null, measurePopulationType, - resolveExpressionFor(measurePopulationType)); + resolveExpressionFor(measurePopulationType), + null); } private static String resolveExpressionFor(MeasurePopulationType theMeasurePopulationType) { diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/cql/ContinuousVariableObservationBooleanBasis.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/cql/ContinuousVariableObservationBooleanBasis.cql new file mode 100644 index 0000000000..f8562847a7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/cql/ContinuousVariableObservationBooleanBasis.cql @@ -0,0 +1,44 @@ +library ContinuousVariableObservationBooleanBasis + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +parameter "Measurement Period" Interval default Interval[@2024-01-01T00:00:00, @2024-12-31T23:59:59] +// cql parameter that can pass in fhir id of an encounter +parameter practitionerParam String + +context Patient + +// boolean population results +// has matching encounter + +define "Initial Population Boolean": + exists "All Encounters" + +define "Measure Population Exclusions Boolean": + exists "Encounter Cancelled" + +define "Measure Population Boolean": + "Initial Population Boolean" + +// Continuous Variable +// age of patient in years from Measurement Period start + +define function "MeasureObservation"(): + CalendarAgeInYearsAt(Patient.birthDate, "Measurement Period".low) + +define function "CalendarAgeInYearsAt"(BirthDate Date, AsOf DateTime): + years between BirthDate and ToDate(AsOf) + +// main criteria logic + +define "All Encounters": + [Encounter] E + +define "Encounter Cancelled": + [Encounter] E + where E.status = 'cancelled' + +define "Gender": + Patient.gender diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/library/ContinuousVariableObservationBooleanBasis.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/library/ContinuousVariableObservationBooleanBasis.json new file mode 100644 index 0000000000..6ac5ba5610 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/library/ContinuousVariableObservationBooleanBasis.json @@ -0,0 +1,17 @@ +{ + "id": "ContinuousVariableObservationBooleanBasis", + "resourceType": "Library", + "url": "http://example.com/Library/ContinuousVariableObservationBooleanBasis", + "name": "ContinuousVariableObservationBooleanBasis", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/ContinuousVariableObservationBooleanBasis.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisAvg.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisAvg.json new file mode 100644 index 0000000000..26da0fccfd --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisAvg.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisAvg", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisAvg", + "name": "ContinuousVariableResourceMeasureObservationBooleanBasisAvg", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "avg" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisCount.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisCount.json new file mode 100644 index 0000000000..38ec256dcd --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisCount.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisCount", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisCount", + "name": "ContinuousVariableResourceMeasureObservationBooleanBasisCount", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "count" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMax.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMax.json new file mode 100644 index 0000000000..74f90e9d31 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMax.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisMax", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisMax", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasis", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "max" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMedian.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMedian.json new file mode 100644 index 0000000000..eb15b5ae73 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMedian.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisMedian", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisMedian", + "name": "ContinuousVariableResourceMeasureObservationBooleanBasisMedian", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "median" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMin.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMin.json new file mode 100644 index 0000000000..b9c62b5d5f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisMin.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisMin", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisMin", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisBooleanBasisMin", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "min" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisSum.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisSum.json new file mode 100644 index 0000000000..d6714e45a0 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationBooleanBasisSum.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationBooleanBasisSum", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationBooleanBasisSum", + "name": "ContinuousVariableResourceMeasureObservationBooleanBasisSum", + "library": [ + "http://example.com/Library/ContinuousVariableObservationBooleanBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "sum" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-gender", + "criteria": { + "language": "text/cql.identifier", + "expression": "Gender" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-female-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-female-encounter-1.json new file mode 100644 index 0000000000..7e18f257b5 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-female-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1940-female-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1940-female" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T02:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json new file mode 100644 index 0000000000..073378f93a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1940-male-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1940-male" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T02:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-other-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-other-encounter-1.json new file mode 100644 index 0000000000..44c1b5b78e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-other-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1940-other-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1940-other" + }, + "period": { + "start": "2024-01-01T03:00:00Z", + "end": "2024-01-01T12:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-male-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-male-encounter-1.json new file mode 100644 index 0000000000..9176842cb7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-male-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1945-male-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1945-male" + }, + "period": { + "start": "2024-01-01T07:00:00Z", + "end": "2024-01-01T14:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-other-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-other-encounter-1.json new file mode 100644 index 0000000000..0865a74dbd --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1945-other-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1945-other-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1945-other" + }, + "period": { + "start": "2024-01-01T05:00:00Z", + "end": "2024-01-01T19:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1950-female-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1950-female-encounter-1.json new file mode 100644 index 0000000000..d1c0a63cbe --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1950-female-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1950-female-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1950-female" + }, + "period": { + "start": "2024-01-01T03:00:00Z", + "end": "2024-01-01T03:30:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1955-other-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1955-other-encounter-1.json new file mode 100644 index 0000000000..e764f44367 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1955-other-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1955-other-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1955-other" + }, + "period": { + "start": "2024-01-01T01:00:00Z", + "end": "2024-01-01T02:30:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1960-unknown-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1960-unknown-encounter-1.json new file mode 100644 index 0000000000..a49727f015 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1960-unknown-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1960-unknown-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1960-unknown" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T00:15:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1965-female-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1965-female-encounter-1.json new file mode 100644 index 0000000000..52605ab3b3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1965-female-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1965-female-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1965-female" + }, + "period": { + "start": "2024-01-01T01:01:00Z", + "end": "2024-01-01T03:02:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1970-female-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1970-female-encounter-1.json new file mode 100644 index 0000000000..b0cf0109a7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1970-female-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1970-female-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1970-female" + }, + "period": { + "start": "2024-01-01T01:00:00Z", + "end": "2024-01-01T03:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-female.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-female.json new file mode 100644 index 0000000000..d51cb8e348 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-female.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1940-female", + "gender": "female", + "birthDate": "1940-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json new file mode 100644 index 0000000000..5445ccc313 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1940-male", + "gender": "male", + "birthDate": "1940-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-other.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-other.json new file mode 100644 index 0000000000..f16ff1ab52 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-other.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1940-other", + "gender": "other", + "birthDate": "1940-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-male.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-male.json new file mode 100644 index 0000000000..980e194feb --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-male.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1945-male", + "gender": "male", + "birthDate": "1945-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-other.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-other.json new file mode 100644 index 0000000000..c33ced9618 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1945-other.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1945-other", + "gender": "other", + "birthDate": "1945-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1950-female.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1950-female.json new file mode 100644 index 0000000000..679962bbb7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1950-female.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1950-female", + "gender": "female", + "birthDate": "1950-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1955-other.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1955-other.json new file mode 100644 index 0000000000..2bc9126423 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1955-other.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1955-other", + "gender": "other", + "birthDate": "1955-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1960-unknown.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1960-unknown.json new file mode 100644 index 0000000000..2da7ff8c91 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1960-unknown.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1960-unknown", + "gender": "unknown", + "birthDate": "1960-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1965-female.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1965-female.json new file mode 100644 index 0000000000..b34f40f52e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1965-female.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1965-female", + "gender": "female", + "birthDate": "1965-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1970-female.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1970-female.json new file mode 100644 index 0000000000..fad81a4e20 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1970-female.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1970-female", + "gender": "female", + "birthDate": "1970-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/cql/ContinuousVariableObservationEncounterBasis.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/cql/ContinuousVariableObservationEncounterBasis.cql new file mode 100644 index 0000000000..d5e93297ee --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/cql/ContinuousVariableObservationEncounterBasis.cql @@ -0,0 +1,43 @@ +library ContinuousVariableObservationEncounterBasis + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +parameter "Measurement Period" Interval default Interval[@2024-01-01T00:00:00, @2024-12-31T23:59:59] +// cql parameter that can pass in fhir id of an encounter +parameter practitionerParam String + +context Patient + +// resource population results +// qty of matching encounters + +define "Initial Population Resource": + "All Encounters" + +define "Measure Population Exclusions Resource": + "Encounter Cancelled" + +define "Measure Population Resource": + "Initial Population Resource" + +// Continuous Variable +// number of hours for encounter + +define function "MeasureObservation"(encounter Encounter): + duration in minutes of encounter.period + +// main criteria logic + +define "All Encounters": + [Encounter] E + +define "Encounter Cancelled": + [Encounter] E + where E.status = 'cancelled' + +// For stratifier + +define "Age": + AgeInYearsAt(start of "Measurement Period") diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/library/ContinuousVariableObservationEncounterBasis.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/library/ContinuousVariableObservationEncounterBasis.json new file mode 100644 index 0000000000..ab35589506 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/library/ContinuousVariableObservationEncounterBasis.json @@ -0,0 +1,17 @@ +{ + "id": "ContinuousVariableObservationEncounterBasis", + "resourceType": "Library", + "url": "http://example.com/Library/ContinuousVariableObservationEncounterBasis", + "name": "ContinuousVariableObservationEncounterBasis", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/ContinuousVariableObservationEncounterBasis.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisAvg.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisAvg.json new file mode 100644 index 0000000000..eb8e15a67b --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisAvg.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisAvg", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisAvg", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasis", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "avg" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisCount.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisCount.json new file mode 100644 index 0000000000..3dccdc60ec --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisCount.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisCount", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisCount", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisCount", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "count" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMax.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMax.json new file mode 100644 index 0000000000..683e2ff627 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMax.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisMax", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisMax", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisMax", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "max" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMedian.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMedian.json new file mode 100644 index 0000000000..7cdeff4c8f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMedian.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisMedian", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisMedian", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisMedian", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "median" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMin.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMin.json new file mode 100644 index 0000000000..9e4611b785 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisMin.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisMin", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisMin", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisMin", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "min" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisSum.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisSum.json new file mode 100644 index 0000000000..61c6f8223d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/resources/measure/ContinuousVariableResourceMeasureObservationEncounterBasisSum.json @@ -0,0 +1,112 @@ +{ + "id": "ContinuousVariableResourceMeasureObservationEncounterBasisSum", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceMeasureObservationEncounterBasisSum", + "name": "ContinuousVariableResourceMeasureObservationEncounterBasisSum", + "library": [ + "http://example.com/Library/ContinuousVariableObservationEncounterBasis" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + }, + { + "id": "measure-observation-1", + "extension" : [ + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString" : "measure-population" + }, + { + "url" : "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode" : "sum" + } + ], + "code" : { + "coding" : [ + { + "system" : "http://terminology.hl7.org/CodeSystem/measure-population", + "code" : "measure-observation", + "display" : "Measure Observation" + } + ] + }, + "criteria" : { + "language" : "text/cql-identifier", + "expression" : "MeasureObservation" + } + } + ], + "stratifier": [ + { + "id": "stratifier-age", + "criteria": { + "language": "text/cql.identifier", + "expression": "Age" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/cql/LibrarySimple.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/cql/LibrarySimple.cql new file mode 100644 index 0000000000..5011b09a48 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/cql/LibrarySimple.cql @@ -0,0 +1,168 @@ +library LibrarySimple + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +parameter "Measurement Period" Interval default Interval[@2024-01-01T00:00:00, @2024-12-31T23:59:59] +// cql parameter that can pass in fhir id of an encounter +parameter practitionerParam String + +context Patient + +// boolean population results +// has matching encounter + +define "Initial Population Boolean": + exists "All Encounters" + +define "Denominator Boolean": + "Initial Population Boolean" + +define "Denominator Exclusion Boolean": + exists "Encounter Cancelled" + +define "Denominator Exception Boolean": + exists "Encounter InProgress" + +define "Numerator Exclusion Boolean": + exists "Encounter Arrived" + +define "Numerator Boolean": + exists "Encounters in Period" + +define "Measure Population Exclusions Boolean": + "Denominator Exclusion Boolean" + +define "Measure Population Boolean": + "Denominator Boolean" + +// resource population results +// qty of matching encounters + +define "Initial Population Resource": + "All Encounters" + +define "Denominator Resource": + "Initial Population Resource" + +define "Denominator Exclusion Resource": + "Encounter Cancelled" + +define "Denominator Exception Resource": + "Encounter InProgress" + +define "Numerator Exclusion Resource": + "Encounter Arrived" + +define "Numerator Resource": + "Encounters in Period" + +define "Measure Population Exclusions Resource": + "Denominator Exclusion Resource" + +define "Measure Population Resource": + "Denominator Resource" + +// for prospective gap calculations +define "date of compliance": + "Measurement Period" + +// cql to force results +define "always false": + false + +define "always true": + true + +// sde single value + +define "SDE Sex": + case + when Patient.gender = 'male' then Code { code: 'M', system: 'http://hl7.org/fhir/v3/AdministrativeGender', display: 'Male' } + when Patient.gender = 'female' then Code { code: 'F', system: 'http://hl7.org/fhir/v3/AdministrativeGender', display: 'Female' } + else null + end + +// sde list of values + + define "SDE Encounters": + "All Encounters" + +// Continuous Variable +// number of hours for encounter + +define function "MeasureObservation"(Encounter Encounter): + Encounter e + where (difference in hours between start of e.period and end of e.period)>0 + +// component stratifier + +define "Gender Stratification": + "SDE Sex" + +// boolean criteria stratifier + +define "boolean strat not finished": + exists "Encounter Not Finished" + +// cql parameter boolean criteria stratifier + +define "boolean strat has practitioner": + exists "Matching General Practitioner" + +// resource criteria stratifier + +define "resource strat not finished": + "Encounter Not Finished" + +// main criteria logic + +define "All Encounters": + [Encounter] E + +define "Encounters in Period": + [Encounter] E + where E.period during "Measurement Period" and E.status='finished' + +define "Encounter Cancelled": + [Encounter] E + where E.status = 'cancelled' + +define "Encounter Status": + [Encounter] E + return E.status + +define "Encounter InProgress": + [Encounter] E + where E.status = 'in-progress' + +define "Encounter Arrived": + [Encounter] E + where E.status = 'arrived' + +define "Encounter Not Finished": + [Encounter] E + where E.status != 'finished' + +define "Matching General Practitioner": + [Patient] p + where Last(Split(First(p.generalPractitioner.reference),'/')) = Last(Split(practitionerParam,'/')) + +define "Age": + AgeInYearsAt(start of "Measurement Period") + +define "Date": + Interval[@2024-02-01T00:00:00, @2024-10-31T23:59:59] + +define "ip date": + "Date" + +define "den date": + "Date" + +define "num date": + "Date" + +define "exc date": + "Date" \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/library/LibrarySimple.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/library/LibrarySimple.json new file mode 100644 index 0000000000..e689c1b3cf --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/library/LibrarySimple.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "LibrarySimple", + "url": "http://example.com/Library/LibrarySimple", + "name": "LibrarySimple", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/LibrarySimple.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanAllPopulations.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanAllPopulations.json new file mode 100644 index 0000000000..1891e34d3a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanAllPopulations.json @@ -0,0 +1,76 @@ +{ + "id": "ContinuousVariableBooleanAllPopulations", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableBooleanAllPopulations", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanExtraInvalidPopulation.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanExtraInvalidPopulation.json new file mode 100644 index 0000000000..dd9d1c11b6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanExtraInvalidPopulation.json @@ -0,0 +1,92 @@ +{ + "id": "ContinuousVariableBooleanExtraInvalidPopulation", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableBooleanExtraInvalidPopulation", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Denominator Boolean" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanGroupScoringDef.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanGroupScoringDef.json new file mode 100644 index 0000000000..13618a6006 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanGroupScoringDef.json @@ -0,0 +1,82 @@ +{ + "id": "ContinuousVariableBooleanGroupScoringDef", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableBooleanGroupScoringDef", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "group": [ + { + "id": "group-1", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "continuous-variable", + "display": "Continuous Variable" + } + ] + } + } + ], + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanMissingReqdPopulation.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanMissingReqdPopulation.json new file mode 100644 index 0000000000..081569d063 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanMissingReqdPopulation.json @@ -0,0 +1,60 @@ +{ + "id": "ContinuousVariableBooleanMissingReqdPopulation", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableBooleanMissingReqdPopulation", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanProhibitedPopulations.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanProhibitedPopulations.json new file mode 100644 index 0000000000..98d6b45f46 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableBooleanProhibitedPopulations.json @@ -0,0 +1,105 @@ +{ + "id": "ContinuousVariableBooleanProhibitedPopulations", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableBooleanProhibitedPopulations", + "name": "ContinuousVariableBooleanProhibitedPopulations", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Denominator Boolean" + } + }, + { + "id": "numerator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Boolean" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Boolean" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableResourceAllPopulations.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableResourceAllPopulations.json new file mode 100644 index 0000000000..5b461df316 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeContinuousVariable/input/resources/measure/ContinuousVariableResourceAllPopulations.json @@ -0,0 +1,76 @@ +{ + "id": "ContinuousVariableResourceAllPopulations", + "resourceType": "Measure", + "url": "http://example.com/Measure/ContinuousVariableResourceAllPopulations", + "library": [ + "http://example.com/Library/LibrarySimple" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Resource" + } + }, + { + "id": "measure-population-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Measure Population Exclusion Resource" + } + } + ] + }] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroup.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroup.cql index c7c3ae6023..98786f43a5 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroup.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroup.cql @@ -34,9 +34,8 @@ define "Numerator": such that "CalendarAgeInYearsAt"(FHIRHelpers.ToDate(BirthDate.birthDate), start of InpatientEncounter.period) > 2 where "LengthInDays"(FHIRHelpers.ToInterval(InpatientEncounter.period)) < 10 -define function "MeasureObservation"(Encounter Encounter): - Encounter e - where (difference in days between start of e.period and end of e.period)>0 +define function "MeasureObservation"(encounter Encounter): + duration in minutes of encounter.period define "Measure Population Exclusions": "Denominator Exclusion" diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroupINVALID.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroupINVALID.cql new file mode 100644 index 0000000000..dc13933353 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/cql/MinimalProportionResourceBasisSingleGroupINVALID.cql @@ -0,0 +1,55 @@ +library MinimalProportionResourceBasisSingleGroupINVALID + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +parameter "Measurement Period" Interval default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0) + +context Patient + +define "Initial Population": + ["Encounter"] InpatientEncounter + +define "Denominator": + "Initial Population" + +define "Denominator Exclusion": + ["Encounter"] InpatientEncounter + with ["Patient"] BirthDate + such that "CalendarAgeInYearsAt"(FHIRHelpers.ToDate(BirthDate.birthDate), start of InpatientEncounter.period) between 75 and 90 + +define "Denominator Exception": + ["Encounter"] InpatientEncounter + where "LengthInDays"(FHIRHelpers.ToInterval(InpatientEncounter.period)) > 5 + +define "Numerator Exclusion": + ["Encounter"] InpatientEncounter + with ["Patient"] BirthDate + such that "CalendarAgeInYearsAt"(FHIRHelpers.ToDate(BirthDate.birthDate), start of InpatientEncounter.period)> 90 + +define "Numerator": + ["Encounter"] InpatientEncounter + with ["Patient"] BirthDate + such that "CalendarAgeInYearsAt"(FHIRHelpers.ToDate(BirthDate.birthDate), start of InpatientEncounter.period) > 2 + where "LengthInDays"(FHIRHelpers.ToInterval(InpatientEncounter.period)) < 10 + +// This function is DELIBERATELY incorrect to test error handling +define function "MeasureObservation"(Encounter Encounter): + Encounter e + where (difference in days between start of e.period and end of e.period)>0 + +define "Measure Population Exclusions": + "Denominator Exclusion" + +define "Measure Population": + "Denominator" + +define function "CalendarAgeInYearsAt"(BirthDateTime DateTime, AsOf DateTime): + years between ToDate(BirthDateTime)and ToDate(AsOf) + +define function "LengthInDays"(Value Interval): + difference in days between start of Value and end of Value + +define "date of compliance": + "Measurement Period" \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/library/MinimalProportionResourceBasisSingleGroupINVALID.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/library/MinimalProportionResourceBasisSingleGroupINVALID.json new file mode 100644 index 0000000000..64814f5352 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/library/MinimalProportionResourceBasisSingleGroupINVALID.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "MinimalProportionResourceBasisSingleGroupINVALID", + "url": "http://example.com/Library/MinimalProportionResourceBasisSingleGroupINVALID", + "name": "MinimalProportionResourceBasisSingleGroupINVALID", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/MinimalProportionResourceBasisSingleGroupINVALID.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalContinuousVariableResourceBasisSingleGroupINVALID.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalContinuousVariableResourceBasisSingleGroupINVALID.json new file mode 100644 index 0000000000..5e2bb0e7c4 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalContinuousVariableResourceBasisSingleGroupINVALID.json @@ -0,0 +1,90 @@ +{ + "id": "MinimalContinuousVariableResourceBasisSingleGroupINVALID", + "resourceType": "Measure", + "url": "http://example.com/Measure/MinimalContinuousVariableResourceBasisSingleGroupINVALID", + "library": [ + "http://example.com/Library/MinimalProportionResourceBasisSingleGroupINVALID" + ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "continuous-variable" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + }, + { + "id": "measure-population", + "code": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population", + "display": "Measure Population" + } ] + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Measure Population" + } + }, { + "id": "measure-population-exclusion", + "code": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population-exclusion", + "display": "Measure Population Exclusion" + } ] + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Measure Population Exclusions" + } + }, + { + "id": "observation", + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode": "sum" + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString": "measure-population" + } ], + "code": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-observation", + "display": "Measure Observation" + } ] + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "MeasureObservation" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalProportionResourceBasisSingleGroupINVALID.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalProportionResourceBasisSingleGroupINVALID.json new file mode 100644 index 0000000000..6f56c022d8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/input/resources/measure/MinimalProportionResourceBasisSingleGroupINVALID.json @@ -0,0 +1,127 @@ +{ + "id": "MinimalProportionResourceBasisSingleGroupINVALID", + "resourceType": "Measure", + "url": "http://example.com/Measure/MinimalProportionResourceBasisSingleGroupINVALID", + "library": [ + "http://example.com/Library/MinimalProportionResourceBasisSingleGroupINVALID" + ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + }, + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-care-gap-compatible", + "valueBoolean": "true" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "proportion" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Denominator" + } + }, + { + "id": "denominator-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator-exclusion", + "display": "Denominator-Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Denominator Exclusion" + } + }, + { + "id": "denominator-exception", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator-exception", + "display": "Denominator-Exception" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Denominator Exception" + } + }, + { + "id": "numerator-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator-exclusion", + "display": "Numerator-Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Numerator Exclusion" + } + }, + { + "id": "numerator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Numerator" + } + } + ] + } + ] +} \ No newline at end of file From b5c6e49e08b1d1468708324296e5721e83b4bd80 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 15 Oct 2025 17:49:10 -0400 Subject: [PATCH 02/48] Add TODOs about error handling. --- .../measure/common/ContinuousVariableObservationHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 81797c76ad..4d91b7bd3e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -108,6 +108,9 @@ private static List processMeasureObservation( // function that will be evaluated var observationExpression = populationDef.expression(); // get expression from criteriaPopulation reference + // LUKETODO: is this the best behaviour? rely on the populationDef having the + // criteriaReference populated or blow up in the next method? + // LUKETODO: figure out the best error handling? String criteriaExpressionInput = groupDef.populations().stream() .filter(populationDefInner -> populationDefInner.id().equals(criteriaPopulationId)) .map(PopulationDef::expression) From 3d9ec5e01666e89363ab8aadb8f1271f8b2283ac Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 09:20:43 -0400 Subject: [PATCH 03/48] Merge with changes in R4MeasureDefBuilder. Tweak a couple of tests. Still several test failures. --- .../cr/measure/r4/R4MeasureDefBuilder.java | 284 ++++++++++++------ .../MeasureEvaluationApplyScoringTests.java | 12 +- .../cr/measure/r4/MeasureStratifierTest.java | 2 +- 3 files changed, 202 insertions(+), 96 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index c5bad07d69..aaf3718fdd 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -3,6 +3,8 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.DATEOFCOMPLIANCE; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.CQFM_SCORING_EXT_URL; +import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_CQFM_AGGREGATE_METHOD_URL; +import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_CQFM_CRITERIA_REFERENCE; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.FHIR_ALL_TYPES_SYSTEM_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.IMPROVEMENT_NOTATION_SYSTEM_DECREASE; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.IMPROVEMENT_NOTATION_SYSTEM_INCREASE; @@ -11,11 +13,13 @@ import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.SDE_USAGE_CODE; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; +import java.util.Optional; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; @@ -33,6 +37,7 @@ import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; import org.opencds.cqf.fhir.cr.measure.common.CodeDef; import org.opencds.cqf.fhir.cr.measure.common.ConceptDef; +import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDefBuilder; @@ -50,15 +55,6 @@ public class R4MeasureDefBuilder implements MeasureDefBuilder { public MeasureDef build(Measure measure) { checkId(measure); - // SDES - List sdes = new ArrayList<>(); - for (MeasureSupplementalDataComponent s : measure.getSupplementalData()) { - checkId(s); - checkSDEUsage(measure, s); - var sdeDef = new SdeDef( - s.getId(), conceptToConceptDef(s.getCode()), s.getCriteria().getExpression()); - sdes.add(sdeDef); - } // scoring var measureScoring = getMeasureScoring(measure); // populationBasis @@ -69,94 +65,204 @@ public MeasureDef build(Measure measure) { // Groups List groups = new ArrayList<>(); for (MeasureGroupComponent group : measure.getGroup()) { - // group Measure Scoring - var groupScoring = getGroupMeasureScoring(measure, group); - // populationBasis - var groupBasis = getGroupPopulationBasis(group); - // improvement Notation - var groupImpNotation = getGroupImpNotation(measure, group); - var hasGroupImpNotation = groupImpNotation != null; - - // Populations - List populations = new ArrayList<>(); - for (MeasureGroupPopulationComponent pop : group.getPopulation()) { - checkId(pop); - MeasurePopulationType populationType = MeasurePopulationType.fromCode( - pop.getCode().getCodingFirstRep().getCode()); - - // LUKETODO: start merging in changes to this class from the continuous variable branch - populations.add(new PopulationDef( - pop.getId(), - conceptToConceptDef(pop.getCode()), - populationType, - pop.getCriteria().getExpression())); + var groupDef = buildGroupDef(measure, group, measureScoring, measureImpNotation, measureBasis); + + groups.add(groupDef); + } + + return new MeasureDef(measure.getId(), measure.getUrl(), measure.getVersion(), groups, getSdeDefs(measure)); + } + + // LUKETODO: break up this monster + private GroupDef buildGroupDef( + Measure measure, + MeasureGroupComponent group, + MeasureScoring measureScoring, + CodeDef measureImpNotation, + CodeDef measureBasis) { + + // group Measure Scoring + var groupScoring = getGroupMeasureScoring(measure, group); + // populationBasis + var groupBasis = getGroupPopulationBasis(group); + // improvement Notation + var groupImpNotation = getGroupImpNotation(measure, group); + var hasGroupImpNotation = groupImpNotation != null; + // Populations + List populations = new ArrayList<>(); + + final Optional optMeasureObservationPopulation = group.getPopulation().stream() + .filter(this::isMeasureObservation) + .findFirst(); + + // aggregateMethod is used to capture continuous-variable method of aggregating MeasureObservation + final ContinuousVariableObservationAggregateMethod aggregateMethod; + final String criteriaReference; + if (optMeasureObservationPopulation.isPresent()) { + final MeasureGroupPopulationComponent measureObservationPopulation = optMeasureObservationPopulation.get(); + + aggregateMethod = getAggregateMethod(measure.getUrl(), measureObservationPopulation); + + criteriaReference = getCriteriaReference(measure.getUrl(), group, measureObservationPopulation); + } else { + aggregateMethod = ContinuousVariableObservationAggregateMethod.N_A; + criteriaReference = null; + } + + for (MeasureGroupPopulationComponent pop : group.getPopulation()) { + checkId(pop); + MeasurePopulationType populationType = MeasurePopulationType.fromCode( + pop.getCode().getCodingFirstRep().getCode()); + + var populationDef = new PopulationDef( + pop.getId(), + conceptToConceptDef(pop.getCode()), + populationType, + pop.getCriteria().getExpression(), + criteriaReference); + + populations.add(populationDef); + } + + if (group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) != null + && checkPopulationForCode(populations, DATEOFCOMPLIANCE) == null) { + // add to definition + var expressionType = (Expression) group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) + .getValue(); + if (!expressionType.hasExpression()) { + throw new InvalidRequestException("no expression was listed for extension: %s for Measure: %s" + .formatted(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL, measure.getUrl())); } + var expression = expressionType.getExpression(); + var populateDefDateOfCompliance = new PopulationDef( + "dateOfCompliance", totalConceptDefCreator(DATEOFCOMPLIANCE), DATEOFCOMPLIANCE, expression); - if (group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) != null - && checkPopulationForCode(populations, DATEOFCOMPLIANCE) == null) { - // add to definition - var expressionType = (Expression) group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) - .getValue(); - if (!expressionType.hasExpression()) { - throw new InvalidRequestException("no expression was listed for extension: %s for Measure: %s" - .formatted(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL, measure.getUrl())); - } - var expression = expressionType.getExpression(); - populations.add(new PopulationDef( - "dateOfCompliance", totalConceptDefCreator(DATEOFCOMPLIANCE), DATEOFCOMPLIANCE, expression)); + populations.add(populateDefDateOfCompliance); + } + + // Stratifiers + List stratifiers = new ArrayList<>(); + for (MeasureGroupStratifierComponent mgsc : group.getStratifier()) { + var stratifierDef = buildStratifierDef(mgsc); + + stratifiers.add(stratifierDef); + } + + return new GroupDef( + group.getId(), + conceptToConceptDef(group.getCode()), + stratifiers, + populations, + getScoringDef(measure, measureScoring, groupScoring), + hasGroupImpNotation, + getImprovementNotation(measureImpNotation, groupImpNotation), + getPopulationBasisDef(measureBasis, groupBasis), + aggregateMethod); + } + + @Nullable + private String getCriteriaReference( + String measureUrl, + MeasureGroupComponent group, + MeasureGroupPopulationComponent measureObservationPopulation) { + + var populationCriteriaExt = measureObservationPopulation.getExtensionByUrl(EXT_CQFM_CRITERIA_REFERENCE); + if (populationCriteriaExt != null) { + // required for measure-observation populations + // the underlying expression is a cql function + // the criteria reference is what is used to populate parameters of the function + String critReference = populationCriteriaExt.getValue().toString(); + // check that the reference exists in the GroupDef.populationId + if (group.getPopulation().stream().map(Element::getId).noneMatch(id -> id.equals(critReference))) { + throw new InvalidRequestException( + "no matching criteria reference was found for extension: %s for Measure: %s" + .formatted(EXT_CQFM_CRITERIA_REFERENCE, measureUrl)); } + // assign validated reference + return critReference; + } + + return null; + } + + private boolean isMeasureObservation(MeasureGroupPopulationComponent pop) { + + checkId(pop); + + MeasurePopulationType populationType = + MeasurePopulationType.fromCode(pop.getCode().getCodingFirstRep().getCode()); + + return populationType != null && populationType.equals(MeasurePopulationType.MEASUREOBSERVATION); + } + + private ContinuousVariableObservationAggregateMethod getAggregateMethod( + String measureUrl, MeasureGroupPopulationComponent measureObservationPopulation) { + + var aggMethodExt = measureObservationPopulation.getExtensionByUrl(EXT_CQFM_AGGREGATE_METHOD_URL); + if (aggMethodExt != null) { + // this method is only required if scoringType = continuous-variable + var aggregateMethodString = aggMethodExt.getValue().toString(); + + var aggregateMethod = ContinuousVariableObservationAggregateMethod.fromString(aggregateMethodString); - // Stratifiers - List stratifiers = new ArrayList<>(); - for (MeasureGroupStratifierComponent mgsc : group.getStratifier()) { - checkId(mgsc); - - // Components - var components = new ArrayList(); - for (MeasureGroupStratifierComponentComponent scc : mgsc.getComponent()) { - checkId(scc); - var scd = new StratifierComponentDef( - scc.getId(), - conceptToConceptDef(scc.getCode()), - scc.hasCriteria() ? scc.getCriteria().getExpression() : null); - - components.add(scd); - } - - if (!components.isEmpty() && mgsc.getCriteria().getExpression() != null) { - throw new InvalidRequestException( - "Measure stratifier: %s, has both component and stratifier criteria expression defined. Only one should be specified" - .formatted(mgsc.getId())); - } - - var stratifierDef = new StratifierDef( - mgsc.getId(), - conceptToConceptDef(mgsc.getCode()), - mgsc.getCriteria().getExpression(), - getStratifierType(mgsc), - components); - - stratifiers.add(stratifierDef); + // check that method is accepted + if (aggregateMethod == null) { + throw new InvalidRequestException("Measure Observation method: %s is not a valid value for Measure: %s" + .formatted(aggregateMethodString, measureUrl)); } - var groupDef = new GroupDef( - group.getId(), - conceptToConceptDef(group.getCode()), - stratifiers, - populations, - getScoringDef(measure, measureScoring, groupScoring), - hasGroupImpNotation, - getImprovementNotation(measureImpNotation, groupImpNotation), - getPopulationBasisDef(measureBasis, groupBasis)); - groups.add(groupDef); + return aggregateMethod; } - return new MeasureDef(measure.getId(), measure.getUrl(), measure.getVersion(), groups, sdes); + return ContinuousVariableObservationAggregateMethod.N_A; + } + + @Nonnull + private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { + checkId(mgsc); + + // Components + var components = new ArrayList(); + for (MeasureGroupStratifierComponentComponent scc : mgsc.getComponent()) { + checkId(scc); + var scd = new StratifierComponentDef( + scc.getId(), + conceptToConceptDef(scc.getCode()), + scc.hasCriteria() ? scc.getCriteria().getExpression() : null); + + components.add(scd); + } + + if (!components.isEmpty() && mgsc.getCriteria().getExpression() != null) { + throw new InvalidRequestException( + "Measure stratifier: %s, has both component and stratifier criteria expression defined. Only one should be specified" + .formatted(mgsc.getId())); + } + + return new StratifierDef( + mgsc.getId(), + conceptToConceptDef(mgsc.getCode()), + mgsc.getCriteria().getExpression(), + getStratifierType(mgsc), + components); } public static void triggerFirstPassValidation(List measures) { measures.forEach(R4MeasureDefBuilder::triggerFirstPassValidation); } + @Nonnull + private List getSdeDefs(Measure measure) { + final List sdes = new ArrayList<>(); + for (MeasureSupplementalDataComponent s : measure.getSupplementalData()) { + checkId(s); + checkSDEUsage(measure, s); + var sdeDef = new SdeDef( + s.getId(), conceptToConceptDef(s.getCode()), s.getCriteria().getExpression()); + sdes.add(sdeDef); + } + return sdes; + } + private static MeasureStratifierType getStratifierType( MeasureGroupStratifierComponent measureGroupStratifierComponent) { if (measureGroupStratifierComponent == null) { @@ -202,8 +308,8 @@ private static void checkSDEUsage( Measure measure, MeasureSupplementalDataComponent measureSupplementalDataComponent) { var hasUsage = measureSupplementalDataComponent.getUsage().stream() .filter(t -> t.getCodingFirstRep().getCode().equals(SDE_USAGE_CODE)) - .collect(Collectors.toList()); - if (hasUsage == null || hasUsage.isEmpty()) { + .toList(); + if (CollectionUtils.isEmpty(hasUsage)) { throw new InvalidRequestException("SupplementalDataComponent usage is missing code: %s for Measure: %s" .formatted(SDE_USAGE_CODE, measure.getUrl())); } @@ -228,13 +334,13 @@ private CodeDef codeToCodeDef(Coding coding) { private static void checkId(Element e) { if (e.getId() == null || StringUtils.isBlank(e.getId())) { - throw new NullPointerException("id is required on all Elements of type: " + e.fhirType()); + throw new InvalidRequestException("id is required on all Elements of type: " + e.fhirType()); } } private static void checkId(Resource r) { if (r.getId() == null || StringUtils.isBlank(r.getId())) { - throw new NullPointerException("id is required on all Resources of type: " + r.fhirType()); + throw new InvalidRequestException("id is required on all Resources of type: " + r.fhirType()); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java index bdc0b51d3b..5b8585d2d3 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluationApplyScoringTests.java @@ -36,13 +36,13 @@ class MeasureEvaluationApplyScoringTests { // These are "IG"'s used in other tests: - private static final String IG_NAME_SINGLE_MEASURE = "MeasureTest"; - private static final String IG_NAME_MULTI_MEASURE = "MinimalMeasureEvaluation"; + private static final String IG_NAME_MEASURE_TEST = "MeasureTest"; + private static final String IG_NAME_MINIMAL_MEASURE_EVALUATION = "MinimalMeasureEvaluation"; private static final Measure.Given GIVEN_SINGLE = getGivenWithMockedRepositorySingleMeasure(); private static final TestDataGenerator testDataGenerator = new TestDataGenerator(GIVEN_SINGLE.getRepository()); - private static final MultiMeasure.Given GIVEN_MULTI = getGivenWithMockedRepositoryMultiMeasure(); + private static final MultiMeasure.Given GIVEN_MULTI = getGivenWithMockedRepositoryMinimalMeasure(); @BeforeAll static void init() { @@ -550,7 +550,7 @@ void MultiMeasure_EightMeasures_AllSubjects_MeasureId() { // Use this if you want database-like behaviour of retrieving different objects per query private static Measure.Given getGivenWithMockedRepositorySingleMeasure() { - var origGiven = Measure.given().repositoryFor(IG_NAME_SINGLE_MEASURE); + var origGiven = Measure.given().repositoryFor(IG_NAME_MEASURE_TEST); var spiedRepository = spy(origGiven.getRepository()); @@ -560,8 +560,8 @@ private static Measure.Given getGivenWithMockedRepositorySingleMeasure() { } // Use this if you want database-like behaviour of retrieving different objects per query - private static MultiMeasure.Given getGivenWithMockedRepositoryMultiMeasure() { - var origGiven = MultiMeasure.given().repositoryFor(IG_NAME_MULTI_MEASURE); + private static MultiMeasure.Given getGivenWithMockedRepositoryMinimalMeasure() { + var origGiven = MultiMeasure.given().repositoryFor(IG_NAME_MINIMAL_MEASURE_EVALUATION); var spiedRepository = spy(origGiven.getRepository()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java index cb0e8499ca..2184dd6337 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java @@ -183,7 +183,7 @@ void cohortBooleanValueStratNoIdStratInvalid() { try { evaluate.then(); fail("should throw a missing Id scenario"); - } catch (NullPointerException e) { + } catch (InvalidRequestException e) { assertTrue(e.getMessage().contains("id is required on all Elements of type: Measure.group.stratifier")); } } From 9dd224cdd6bc12d12c85f2a87c2ae65f8eceb8b7 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 09:33:31 -0400 Subject: [PATCH 04/48] Merge changes from R4MeasureReportBuilder. More tests are fixed, but not all. --- .../cr/measure/r4/R4MeasureReportBuilder.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 859178bcdb..4dba183a2f 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -345,7 +345,8 @@ protected void buildGroup( // add extension to group for totalDenominator and totalNumerator if (groupDef.measureScoring().equals(MeasureScoring.PROPORTION) - || groupDef.measureScoring().equals(MeasureScoring.RATIO)) { + || groupDef.measureScoring().equals(MeasureScoring.RATIO) + || groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE)) { // add extension to group for if (bc.measureReport.getType().equals(MeasureReport.MeasureReportType.INDIVIDUAL)) { @@ -424,7 +425,13 @@ private void buildPopulation( if (groupDef.isBooleanBasis()) { reportPopulation.setCount(populationDef.getSubjects().size()); } else { - reportPopulation.setCount(populationDef.getResources().size()); + if (populationDef.type().equals(MeasurePopulationType.MEASUREOBSERVATION)) { + // resources has nested maps containing correct qty of resources + reportPopulation.setCount(countObservations(populationDef)); + } else { + // standard behavior + reportPopulation.setCount(populationDef.getResources().size()); + } } if (measurePopulation.hasDescription()) { @@ -465,6 +472,18 @@ private void buildPopulation( } } + public int countObservations(PopulationDef populationDef) { + if (populationDef == null || populationDef.getResources() == null) { + return 0; + } + + return populationDef.getResources().stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .mapToInt(Map::size) + .sum(); + } + protected void buildMeasureObservations(BuilderContext bc, String observationName, Set resources) { for (int i = 0; i < resources.size(); i++) { // TODO: Do something with the resource... From 17d0dc6a788c25271ea961ff0f456b083bc5a8b7 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 09:41:38 -0400 Subject: [PATCH 05/48] Fix all tests in MeasureStratifierTest by fixing at least of bad merge in MeasureEvaluator. --- .../cr/measure/common/MeasureEvaluator.java | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index e804c499c3..a1e4f54145 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; @@ -471,31 +472,13 @@ protected void evaluateSdes(String subjectId, List sdes, EvaluationResul } } - protected Object addStratifierResult(Object result, String subjectId) { - if (result instanceof Iterable iterable) { - var resultIter = iterable.iterator(); - if (!resultIter.hasNext()) { - result = null; - } else { - result = resultIter.next(); - } - - if (resultIter.hasNext()) { - throw new InvalidRequestException( - "stratifiers may not return multiple values for subjectId: " + subjectId); - } - } - return result; - } - protected void addStratifierComponentResult( List components, EvaluationResult evaluationResult, String subjectId) { for (StratifierComponentDef component : components) { var expressionResult = evaluationResult.forExpression(component.expression()); - Object result = addStratifierResult(expressionResult.value(), subjectId); - if (result != null) { - component.putResult(subjectId, result, expressionResult.evaluatedResources()); - } + Optional.ofNullable(expressionResult.value()) + .ifPresent(nonNullValue -> + component.putResult(subjectId, nonNullValue, expressionResult.evaluatedResources())); } } @@ -508,13 +491,12 @@ protected void evaluateStratifiers( } else { var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); - Object result = addStratifierResult(expressionResult.value(), subjectId); - if (result != null) { - stratifierDef.putResult( - subjectId, // context of CQL expression ex: Patient based - result, - expressionResult.evaluatedResources()); - } + Optional.ofNullable(expressionResult) + .map(ExpressionResult::value) + .ifPresent(nonNullValue -> stratifierDef.putResult( + subjectId, // context of CQL expression ex: Patient based + nonNullValue, + expressionResult.evaluatedResources())); } } } From b634702f08d84153d657670e9f165b18cc15c15f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 10:00:03 -0400 Subject: [PATCH 06/48] Fix last set of test due to missing test files. Add more TODOs. --- .../cqf/fhir/cr/measure/common/MeasureEvaluator.java | 2 ++ .../input/tests/encounter/patient-0-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-1-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-2-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-3-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-4-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-5-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-6-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-7-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-8-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-9-encounter-1.json | 12 ++++++++++++ .../input/tests/encounter/patient-9-encounter-2.json | 12 ++++++++++++ .../input/tests/patient/patient-0.json | 6 ++++++ .../input/tests/patient/patient-1.json | 6 ++++++ .../input/tests/patient/patient-2.json | 6 ++++++ .../input/tests/patient/patient-3.json | 6 ++++++ .../input/tests/patient/patient-4.json | 6 ++++++ .../input/tests/patient/patient-5.json | 6 ++++++ .../input/tests/patient/patient-6.json | 6 ++++++ .../input/tests/patient/patient-7.json | 6 ++++++ .../input/tests/patient/patient-8.json | 6 ++++++ .../input/tests/patient/patient-9.json | 6 ++++++ 22 files changed, 194 insertions(+) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-0-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-1-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-2-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-3-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-4-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-5-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-6-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-7-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-8-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-2.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-0.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-2.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-3.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-4.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-5.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-6.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-7.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-8.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-9.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index a1e4f54145..12a1f4fa7c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -325,6 +325,7 @@ protected void evaluateContinuousVariable( subjectType, subjectId, groupDef.getSingle(MEASUREPOPULATIONEXCLUSION), evaluationResult); if (applyScoring) { // verify exclusions are in measure-population + // LUKETODO: null warnings measurePopulationExclusion.getResources().retainAll(measurePopulation.getResources()); measurePopulationExclusion.getSubjects().retainAll(measurePopulation.getSubjects()); } @@ -342,6 +343,7 @@ protected void evaluateContinuousVariable( measurePopulationObservation.getResources(), measurePopulation, measurePopulationObservation); // what about subjects? pruneObservationSubjectResources( + // LUKETODO: null warnings measurePopulation.subjectResources, measurePopulationObservation.getSubjectResources()); } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-0-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-0-encounter-1.json new file mode 100644 index 0000000000..355f95eab4 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-0-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-0-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-0" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T02:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-1-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-1-encounter-1.json new file mode 100644 index 0000000000..1a701d6e52 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-1-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T02:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-2-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-2-encounter-1.json new file mode 100644 index 0000000000..c212952514 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-2-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-2-encounter-1", + "status": "arrived", + "subject": { + "reference": "Patient/patient-2" + }, + "period": { + "start": "2024-01-01T03:00:00Z", + "end": "2024-01-01T12:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-3-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-3-encounter-1.json new file mode 100644 index 0000000000..6d31e29037 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-3-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-3-encounter-1", + "status": "arrived", + "subject": { + "reference": "Patient/patient-3" + }, + "period": { + "start": "2024-01-01T07:00:00Z", + "end": "2024-01-01T14:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-4-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-4-encounter-1.json new file mode 100644 index 0000000000..2b7ea0a95c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-4-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-4-encounter-1", + "status": "triaged", + "subject": { + "reference": "Patient/patient-4" + }, + "period": { + "start": "2024-01-01T05:00:00Z", + "end": "2024-01-01T19:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-5-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-5-encounter-1.json new file mode 100644 index 0000000000..8fab51f2f3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-5-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-5-encounter-1", + "status": "triaged", + "subject": { + "reference": "Patient/patient-5" + }, + "period": { + "start": "2024-01-01T02:00:00Z", + "end": "2024-01-01T04:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-6-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-6-encounter-1.json new file mode 100644 index 0000000000..b6d7a3c3c2 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-6-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-6-encounter-1", + "status": "cancelled", + "subject": { + "reference": "Patient/patient-6" + }, + "period": { + "start": "2024-01-01T03:00:00Z", + "end": "2024-01-01T03:30:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-7-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-7-encounter-1.json new file mode 100644 index 0000000000..3ab1781d9d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-7-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-7-encounter-1", + "status": "cancelled", + "subject": { + "reference": "Patient/patient-7" + }, + "period": { + "start": "2024-01-01T01:00:00Z", + "end": "2024-01-01T02:30:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-8-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-8-encounter-1.json new file mode 100644 index 0000000000..80e2046494 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-8-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-8-encounter-1", + "status": "finished", + "subject": { + "reference": "Patient/patient-8" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T00:15:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-1.json new file mode 100644 index 0000000000..f7326a2683 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-9-encounter-1", + "status": "finished", + "subject": { + "reference": "Patient/patient-9" + }, + "period": { + "start": "2024-01-01T01:01:00Z", + "end": "2024-01-01T03:02:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-2.json new file mode 100644 index 0000000000..7537303236 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/encounter/patient-9-encounter-2.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-9-encounter-2", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-9" + }, + "period": { + "start": "2024-01-01T01:00:00Z", + "end": "2024-01-01T03:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-0.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-0.json new file mode 100644 index 0000000000..96665fd3d1 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-0.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-0", + "gender": "female", + "birthDate": "1940-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-1.json new file mode 100644 index 0000000000..1bd3240073 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-1.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1", + "gender": "male", + "birthDate": "1940-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-2.json new file mode 100644 index 0000000000..67d36fd630 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-2.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-2", + "gender": "female", + "birthDate": "1950-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-3.json new file mode 100644 index 0000000000..193ae554e1 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-3.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-3", + "gender": "male", + "birthDate": "1950-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-4.json new file mode 100644 index 0000000000..ea1c8964e9 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-4.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-4", + "gender": "female", + "birthDate": "1950-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-5.json new file mode 100644 index 0000000000..377a39b5a6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-5.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-5", + "gender": "male", + "birthDate": "1950-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-6.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-6.json new file mode 100644 index 0000000000..8c83a5d5ce --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-6.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-6", + "gender": "female", + "birthDate": "1960-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-7.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-7.json new file mode 100644 index 0000000000..cb397dbba3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-7.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-7", + "gender": "male", + "birthDate": "1960-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-8.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-8.json new file mode 100644 index 0000000000..9615db7f5d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-8.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-8", + "gender": "female", + "birthDate": "1960-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-9.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-9.json new file mode 100644 index 0000000000..f763d1e2a6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationEncounterBasis/input/tests/patient/patient-9.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-9", + "gender": "male", + "birthDate": "1960-01-01" +} \ No newline at end of file From 753a49d451e27618a71f7af16d0d3133bb8ba220 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 10:08:08 -0400 Subject: [PATCH 07/48] Sonar. --- .../ContinuousVariableObservationHandler.java | 9 ------ .../cr/measure/common/MeasureEvaluator.java | 28 ++++++++----------- .../cr/measure/r4/R4MeasureProcessor.java | 6 +--- 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 4d91b7bd3e..45538d1bd6 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -64,15 +64,6 @@ static List continuousVariableEvaluation( // Add back the libraries to the stack, since we popped them off during CQL context.getState().init(libraries); - // Measurement Period: operation parameter defined measurement period - // this necessary? - // Interval measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); - - // setMeasurementPeriod( - // measurementPeriodParams, - // context, - // Optional.ofNullable(measureDef.).map(List::of).orElse(List.of("Unknown - // Measure URL"))); // one Library may be linked to multiple Measures for (MeasureDef measureDefWithMeasureObservations : measureDefsWithMeasureObservations) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 12a1f4fa7c..0d827708e4 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -312,20 +312,17 @@ protected void evaluateContinuousVariable( measurePopulation = evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); // Evaluate Population Expressions measurePopulation = evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); - if (measurePopulation != null && initialPopulation != null) { - if (applyScoring) { - // verify initial-population are in measure-population - measurePopulation.getResources().retainAll(initialPopulation.getResources()); - measurePopulation.getSubjects().retainAll(initialPopulation.getSubjects()); - } + if (measurePopulation != null && initialPopulation != null && applyScoring) { + // verify initial-population are in measure-population + measurePopulation.getResources().retainAll(initialPopulation.getResources()); + measurePopulation.getSubjects().retainAll(initialPopulation.getSubjects()); } if (measurePopulationExclusion != null) { evaluatePopulationMembership( subjectType, subjectId, groupDef.getSingle(MEASUREPOPULATIONEXCLUSION), evaluationResult); - if (applyScoring) { + if (applyScoring && measurePopulation != null) { // verify exclusions are in measure-population - // LUKETODO: null warnings measurePopulationExclusion.getResources().retainAll(measurePopulation.getResources()); measurePopulationExclusion.getSubjects().retainAll(measurePopulation.getSubjects()); } @@ -342,9 +339,10 @@ protected void evaluateContinuousVariable( pruneObservationResources( measurePopulationObservation.getResources(), measurePopulation, measurePopulationObservation); // what about subjects? - pruneObservationSubjectResources( - // LUKETODO: null warnings - measurePopulation.subjectResources, measurePopulationObservation.getSubjectResources()); + if (measurePopulation != null) { + pruneObservationSubjectResources( + measurePopulation.subjectResources, measurePopulationObservation.getSubjectResources()); + } } } // measure Observation @@ -416,11 +414,7 @@ protected void pruneObservationResources( } protected void evaluateCohort( - GroupDef groupDef, - String subjectType, - String subjectId, - EvaluationResult evaluationResult, - boolean applyScoring) { + GroupDef groupDef, String subjectType, String subjectId, EvaluationResult evaluationResult) { PopulationDef initialPopulation = groupDef.getSingle(INITIALPOPULATION); // Validate Required Populations are Present R4MeasureScoringTypePopulations.validateScoringTypePopulations( @@ -452,7 +446,7 @@ protected void evaluateGroup( evaluateContinuousVariable(groupDef, subjectType, subjectId, evaluationResult, applyScoring); break; case COHORT: - evaluateCohort(groupDef, subjectType, subjectId, evaluationResult, applyScoring); + evaluateCohort(groupDef, subjectType, subjectId, evaluationResult); break; } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 3fa01399a8..814bf36856 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -165,9 +165,6 @@ public MeasureReport evaluateMeasure( // setup MeasureDef var measureDef = new R4MeasureDefBuilder().build(measure); - // Process Criteria Expression Results - final IIdType measureId = measure.getIdElement().toUnqualifiedVersionless(); - // populate results from Library $evaluate final Map resultForThisMeasure = compositeEvaluationResultsPerMeasure.processMeasureForSuccessOrFailure(measureDef); @@ -180,7 +177,7 @@ public MeasureReport evaluateMeasure( // LUKETODO: figure out if we still need this: var measurementPeriod = postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( - measure, measureDef, periodStart, periodEnd, context); + measure, periodStart, periodEnd, context); // Build Measure Report with Results return new R4MeasureReportBuilder() @@ -203,7 +200,6 @@ public MeasureReport evaluateMeasure( */ private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( Measure measure, - MeasureDef measureDef, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, CqlEngine context) { From 3766a1ecd680f6ba574ac93586ef13fea8de1dea Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 10:50:54 -0400 Subject: [PATCH 08/48] Finish refactoring R4MeasureDefBuilder. --- .../cr/measure/r4/R4MeasureDefBuilder.java | 165 ++++++++++-------- 1 file changed, 93 insertions(+), 72 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index aaf3718fdd..1a7fce5504 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -13,6 +13,8 @@ import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.SDE_USAGE_CODE; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.ArrayList; @@ -73,7 +75,6 @@ public MeasureDef build(Measure measure) { return new MeasureDef(measure.getId(), measure.getUrl(), measure.getVersion(), groups, getSdeDefs(measure)); } - // LUKETODO: break up this monster private GroupDef buildGroupDef( Measure measure, MeasureGroupComponent group, @@ -88,71 +89,36 @@ private GroupDef buildGroupDef( // improvement Notation var groupImpNotation = getGroupImpNotation(measure, group); var hasGroupImpNotation = groupImpNotation != null; - // Populations - List populations = new ArrayList<>(); final Optional optMeasureObservationPopulation = group.getPopulation().stream() .filter(this::isMeasureObservation) .findFirst(); // aggregateMethod is used to capture continuous-variable method of aggregating MeasureObservation - final ContinuousVariableObservationAggregateMethod aggregateMethod; - final String criteriaReference; - if (optMeasureObservationPopulation.isPresent()) { - final MeasureGroupPopulationComponent measureObservationPopulation = optMeasureObservationPopulation.get(); - - aggregateMethod = getAggregateMethod(measure.getUrl(), measureObservationPopulation); - - criteriaReference = getCriteriaReference(measure.getUrl(), group, measureObservationPopulation); - } else { - aggregateMethod = ContinuousVariableObservationAggregateMethod.N_A; - criteriaReference = null; - } - - for (MeasureGroupPopulationComponent pop : group.getPopulation()) { - checkId(pop); - MeasurePopulationType populationType = MeasurePopulationType.fromCode( - pop.getCode().getCodingFirstRep().getCode()); + final ContinuousVariableObservationAggregateMethod aggregateMethod = + getAggregateMethod(measure.getUrl(), optMeasureObservationPopulation.orElse(null)); - var populationDef = new PopulationDef( - pop.getId(), - conceptToConceptDef(pop.getCode()), - populationType, - pop.getCriteria().getExpression(), - criteriaReference); + final String criteriaReference = + getCriteriaReference(measure.getUrl(), group, optMeasureObservationPopulation.orElse(null)); - populations.add(populationDef); - } + // Populations + var populationsWithCriteriaReference = group.getPopulation().stream() + .peek(R4MeasureDefBuilder::checkId) + .map(population -> buildPopulationDefWithCriteriaReference(population, criteriaReference)) + .toList(); - if (group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) != null - && checkPopulationForCode(populations, DATEOFCOMPLIANCE) == null) { - // add to definition - var expressionType = (Expression) group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) - .getValue(); - if (!expressionType.hasExpression()) { - throw new InvalidRequestException("no expression was listed for extension: %s for Measure: %s" - .formatted(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL, measure.getUrl())); - } - var expression = expressionType.getExpression(); - var populateDefDateOfCompliance = new PopulationDef( - "dateOfCompliance", totalConceptDefCreator(DATEOFCOMPLIANCE), DATEOFCOMPLIANCE, expression); - - populations.add(populateDefDateOfCompliance); - } + final Optional optPopulationDefDateOfCompliance = + buildPopulationDefForDateOfCompliance(measure.getUrl(), group, populationsWithCriteriaReference); // Stratifiers - List stratifiers = new ArrayList<>(); - for (MeasureGroupStratifierComponent mgsc : group.getStratifier()) { - var stratifierDef = buildStratifierDef(mgsc); - - stratifiers.add(stratifierDef); - } + var stratifiers = + group.getStratifier().stream().map(this::buildStratifierDef).toList(); return new GroupDef( group.getId(), conceptToConceptDef(group.getCode()), stratifiers, - populations, + mergePopulations(populationsWithCriteriaReference, optPopulationDefDateOfCompliance.orElse(null)), getScoringDef(measure, measureScoring, groupScoring), hasGroupImpNotation, getImprovementNotation(measureImpNotation, groupImpNotation), @@ -160,11 +126,87 @@ && checkPopulationForCode(populations, DATEOFCOMPLIANCE) == null) { aggregateMethod); } + private List mergePopulations( + List populationsWithCriteriaReference, @Nullable PopulationDef populationDef) { + + final Builder immutableListBuilder = ImmutableList.builder(); + + immutableListBuilder.addAll(populationsWithCriteriaReference); + + Optional.ofNullable(populationDef).ifPresent(immutableListBuilder::add); + + return immutableListBuilder.build(); + } + + @Nonnull + private PopulationDef buildPopulationDefWithCriteriaReference( + MeasureGroupPopulationComponent population, String criteriaReference) { + return new PopulationDef( + population.getId(), + conceptToConceptDef(population.getCode()), + MeasurePopulationType.fromCode( + population.getCode().getCodingFirstRep().getCode()), + population.getCriteria().getExpression(), + criteriaReference); + } + + private Optional buildPopulationDefForDateOfCompliance( + String measureUrl, MeasureGroupComponent group, List populationDefs) { + + if (group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) == null + || checkPopulationForCode(populationDefs, DATEOFCOMPLIANCE) == null) { + return Optional.empty(); + } + + // add to definition + var expressionType = (Expression) group.getExtensionByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) + .getValue(); + if (!expressionType.hasExpression()) { + throw new InvalidRequestException("no expression was listed for extension: %s for Measure: %s" + .formatted(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL, measureUrl)); + } + var expression = expressionType.getExpression(); + var populateDefDateOfCompliance = new PopulationDef( + "dateOfCompliance", totalConceptDefCreator(DATEOFCOMPLIANCE), DATEOFCOMPLIANCE, expression); + + return Optional.of(populateDefDateOfCompliance); + } + + private ContinuousVariableObservationAggregateMethod getAggregateMethod( + String measureUrl, @Nullable MeasureGroupPopulationComponent measureObservationPopulation) { + + if (measureObservationPopulation == null) { + return ContinuousVariableObservationAggregateMethod.N_A; + } + + var aggMethodExt = measureObservationPopulation.getExtensionByUrl(EXT_CQFM_AGGREGATE_METHOD_URL); + if (aggMethodExt != null) { + // this method is only required if scoringType = continuous-variable + var aggregateMethodString = aggMethodExt.getValue().toString(); + + var aggregateMethod = ContinuousVariableObservationAggregateMethod.fromString(aggregateMethodString); + + // check that method is accepted + if (aggregateMethod == null) { + throw new InvalidRequestException("Measure Observation method: %s is not a valid value for Measure: %s" + .formatted(aggregateMethodString, measureUrl)); + } + + return aggregateMethod; + } + + return ContinuousVariableObservationAggregateMethod.N_A; + } + @Nullable private String getCriteriaReference( String measureUrl, MeasureGroupComponent group, - MeasureGroupPopulationComponent measureObservationPopulation) { + @Nullable MeasureGroupPopulationComponent measureObservationPopulation) { + + if (measureObservationPopulation == null) { + return null; + } var populationCriteriaExt = measureObservationPopulation.getExtensionByUrl(EXT_CQFM_CRITERIA_REFERENCE); if (populationCriteriaExt != null) { @@ -195,27 +237,6 @@ private boolean isMeasureObservation(MeasureGroupPopulationComponent pop) { return populationType != null && populationType.equals(MeasurePopulationType.MEASUREOBSERVATION); } - private ContinuousVariableObservationAggregateMethod getAggregateMethod( - String measureUrl, MeasureGroupPopulationComponent measureObservationPopulation) { - - var aggMethodExt = measureObservationPopulation.getExtensionByUrl(EXT_CQFM_AGGREGATE_METHOD_URL); - if (aggMethodExt != null) { - // this method is only required if scoringType = continuous-variable - var aggregateMethodString = aggMethodExt.getValue().toString(); - - var aggregateMethod = ContinuousVariableObservationAggregateMethod.fromString(aggregateMethodString); - - // check that method is accepted - if (aggregateMethod == null) { - throw new InvalidRequestException("Measure Observation method: %s is not a valid value for Measure: %s" - .formatted(aggregateMethodString, measureUrl)); - } - - return aggregateMethod; - } - return ContinuousVariableObservationAggregateMethod.N_A; - } - @Nonnull private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { checkId(mgsc); From f39510b19c97b8799ec39f78855232ddc12ef02f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 11:32:21 -0400 Subject: [PATCH 09/48] More refactoring for ContinuousVariableObservationHandler, including extracting out a LibraryHandler to init the libs. --- .../ContinuousVariableObservationHandler.java | 169 ++++++++++-------- .../cr/measure/common/LibraryHandler.java | 70 ++++++++ .../measure/common/MeasureProcessorUtils.java | 65 ------- 3 files changed, 167 insertions(+), 137 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 45538d1bd6..af3d21b332 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -2,6 +2,7 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.ArrayList; import java.util.Collections; @@ -11,7 +12,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.FunctionDef; import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.fhir.r4.model.CodeableConcept; @@ -32,7 +32,6 @@ private ContinuousVariableObservationHandler() { // static class with private constructor } - // LUKETODO: refactor this a lot static List continuousVariableEvaluation( CqlEngine context, List measureDefs, @@ -40,29 +39,20 @@ static List continuousVariableEvaluation( EvaluationResult evaluationResult, String subjectTypePart) { - final List finalResults = new ArrayList<>(); - final List measureDefsWithMeasureObservations = measureDefs.stream() // if measure contains measure-observation, otherwise short circuit - .filter(MeasureProcessorUtils::hasMeasureObservation) + .filter(ContinuousVariableObservationHandler::hasMeasureObservation) .toList(); if (measureDefsWithMeasureObservations.isEmpty()) { // Don't need to do anything if there are no measure observations to process - return finalResults; + return List.of(); } // measure Observation Path, have to re-initialize everything again - // TODO: extend library evaluated context so library initialization isn't having to be built for - // both, and takes advantage of caching - // LUKETODO: figure out how to reuse this pattern: - var compiledLibraries = MeasureProcessorUtils.getCompiledLibraries(libraryIdentifiers, context); - - var libraries = - compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); + LibraryHandler.initLibraries(context, libraryIdentifiers); - // Add back the libraries to the stack, since we popped them off during CQL - context.getState().init(libraries); + final List finalResults = new ArrayList<>(); // one Library may be linked to multiple Measures for (MeasureDef measureDefWithMeasureObservations : measureDefsWithMeasureObservations) { @@ -93,20 +83,24 @@ private static List processMeasureObservation( String subjectTypePart, GroupDef groupDef, PopulationDef populationDef) { - // get criteria input for results to get (measure-population, numerator, - // denominator) + + if (populationDef.getCriteriaReference() == null) { + // We screwed up building the PopulationDef, somehow + throw new InternalErrorException( + "PopulationDef criteria reference is missing for continuous variable observation"); + } + + // get criteria input for results to get (measure-population, numerator, denominator) var criteriaPopulationId = populationDef.getCriteriaReference(); // function that will be evaluated var observationExpression = populationDef.expression(); // get expression from criteriaPopulation reference - // LUKETODO: is this the best behaviour? rely on the populationDef having the - // criteriaReference populated or blow up in the next method? - // LUKETODO: figure out the best error handling? - String criteriaExpressionInput = groupDef.populations().stream() + var criteriaExpressionInput = groupDef.populations().stream() .filter(populationDefInner -> populationDefInner.id().equals(criteriaPopulationId)) .map(PopulationDef::expression) .findFirst() .orElse(null); + Optional optExpressionResult = tryGetExpressionResult(criteriaExpressionInput, evaluationResult); @@ -121,7 +115,7 @@ private static List processMeasureObservation( // this will be used in MeasureEvaluator var expressionName = criteriaPopulationId + "-" + observationExpression; // loop through measure-population results - int i = 0; + int index = 0; Map functionResults = new HashMap<>(); Set evaluatedResources = new HashSet<>(); final List results = new ArrayList<>(); @@ -129,15 +123,9 @@ private static List processMeasureObservation( Object observationResult = evaluateObservationCriteria( result, observationExpression, evaluatedResources, groupDef.isBooleanBasis(), context); - if (!(observationResult instanceof String - || observationResult instanceof Integer - || observationResult instanceof Double)) { - throw new IllegalArgumentException( - "continuous variable observation CQL \"MeasureObservation\" function result must be of type String, Integer or Double but was: " - + result.getClass().getSimpleName()); - } + validateObservationResult(result, observationResult); - var observationId = expressionName + "-" + i; + var observationId = expressionName + "-" + index; // wrap result in Observation resource to avoid duplicate results data loss // in set object Observation observation = wrapResultAsObservation(observationId, observationId, observationResult); @@ -149,50 +137,18 @@ private static List processMeasureObservation( // value= the output Observation resource containing calculated value functionResults.put(result, observation); - results.add(new MeasureObservationResult( - expressionName, new ExpressionResult(functionResults, evaluatedResources))); - } + final ExpressionResult expressionResultFromFunction = + new ExpressionResult(functionResults, evaluatedResources); - return results; - } + final MeasureObservationResult measureObservationResult = + new MeasureObservationResult(expressionName, expressionResultFromFunction); - /** - * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. - * This method is a downstream calculation given it requires calculated results before it can be called. - * Results are then added to associated MeasureDef - * @param measureDef measure defined objects that are populated from criteria expression results - * @param context cql engine context used to evaluate results - */ - public static void continuousVariableObservation(MeasureDef measureDef, CqlEngine context) { - // Continuous Variable? - for (GroupDef groupDef : measureDef.groups()) { - // Measure Observation defined? - if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) - && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { - - PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); - PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); - - // Inject MeasurePopulation results into Measure Observation Function - Map> subjectResources = measurePopulation.getSubjectResources(); - - for (Map.Entry> entry : subjectResources.entrySet()) { - String subjectId = entry.getKey(); - Set resourcesForSubject = entry.getValue(); + results.add(measureObservationResult); - for (Object resource : resourcesForSubject) { - Object observationResult = evaluateObservationCriteria( - resource, - measureObservation.expression(), - measureObservation.getEvaluatedResources(), - groupDef.isBooleanBasis(), - context); - measureObservation.addResource(observationResult); - measureObservation.addResource(subjectId, observationResult); - } - } - } + index++; } + + return results; } /** @@ -226,6 +182,7 @@ private static Object evaluateObservationCriteria( try { if (!isBooleanBasis) { // subject based observations don't have a parameter to pass in + // LUKETODO: error handling context.getState() .push(new Variable(functionDef.getOperand().get(0).getName()).withValue(resource)); } @@ -242,9 +199,9 @@ private static Object evaluateObservationCriteria( private static Optional tryGetExpressionResult( String expressionName, EvaluationResult evaluationResult) { - // LUKETODO: add more context to this exception if (expressionName == null) { - throw new InvalidRequestException("expressionName is null"); + throw new InternalErrorException( + "PopulationDef criteria reference is missing for continuous variable observation"); } if (evaluationResult == null) { @@ -283,6 +240,34 @@ private static Iterable getResultIterable( } } + private static void validateObservationResult(Object result, Object observationResult) { + if (!(observationResult instanceof String + || observationResult instanceof Integer + || observationResult instanceof Double)) { + throw new IllegalArgumentException( + "continuous variable observation CQL \"MeasureObservation\" function result must be of type String, Integer or Double but was: " + + result.getClass().getSimpleName()); + } + } + + /** + * Checks if a MeasureDef has at least one PopulationDef of type MEASUREOBSERVATION + * across all of its groups. + * + * @param measureDef the MeasureDef to check + * @return true if any PopulationDef in any GroupDef is MEASUREOBSERVATION + */ + private static boolean hasMeasureObservation(MeasureDef measureDef) { + if (measureDef == null || measureDef.groups() == null) { + return false; + } + + return measureDef.groups().stream() + .filter(group -> group.populations() != null) + .flatMap(group -> group.populations().stream()) + .anyMatch(pop -> pop.type() == MeasurePopulationType.MEASUREOBSERVATION); + } + /** * method used to extract evaluated resources touched by CQL criteria expressions * @param outEvaluatedResources set object used to capture resources touched @@ -334,4 +319,44 @@ private static Quantity convertToQuantity(Object obj) { return q; } + + // LUKETODO: this is used by DSTU3 only: what to do with this? + /** + * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. + * This method is a downstream calculation given it requires calculated results before it can be called. + * Results are then added to associated MeasureDef + * @param measureDef measure defined objects that are populated from criteria expression results + * @param context cql engine context used to evaluate results + */ + public static void continuousVariableObservation(MeasureDef measureDef, CqlEngine context) { + // Continuous Variable? + for (GroupDef groupDef : measureDef.groups()) { + // Measure Observation defined? + if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) + && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { + + PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); + PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); + + // Inject MeasurePopulation results into Measure Observation Function + Map> subjectResources = measurePopulation.getSubjectResources(); + + for (Map.Entry> entry : subjectResources.entrySet()) { + String subjectId = entry.getKey(); + Set resourcesForSubject = entry.getValue(); + + for (Object resource : resourcesForSubject) { + Object observationResult = evaluateObservationCriteria( + resource, + measureObservation.expression(), + measureObservation.getEvaluatedResources(), + groupDef.isBooleanBasis(), + context); + measureObservation.addResource(observationResult); + measureObservation.addResource(subjectId, observationResult); + } + } + } + } + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java new file mode 100644 index 0000000000..44dc66b701 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java @@ -0,0 +1,70 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import java.util.List; +import org.cqframework.cql.cql2elm.CqlCompilerException; +import org.cqframework.cql.cql2elm.CqlIncludeException; +import org.cqframework.cql.cql2elm.model.CompiledLibrary; +import org.hl7.elm.r1.VersionedIdentifier; +import org.opencds.cqf.cql.engine.execution.CqlEngine; + +// LUKETODO: javadoc +public class LibraryHandler { + + // LUKETODO: extend library evaluated context so library initialization isn't having to be built for + // both, and takes advantage of caching + // LUKETODO: figure out how to reuse this pattern: + // LUKETODO: are we reinitializing the cache here? if so, should we try not to, somehow? + public static void initLibraries(CqlEngine context, List libraryIdentifiers) { + var compiledLibraries = getCompiledLibraries(libraryIdentifiers, context); + + var libraries = + compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); + + // Add back the libraries to the stack, since we popped them off during CQL + context.getState().init(libraries); + } + + private static List getCompiledLibraries(List ids, CqlEngine context) { + try { + var resolvedLibraryResults = + context.getEnvironment().getLibraryManager().resolveLibraries(ids); + + var allErrors = resolvedLibraryResults.allErrors(); + if (resolvedLibraryResults.hasErrors() || ids.size() > allErrors.size()) { + return resolvedLibraryResults.allCompiledLibraries(); + } + + if (ids.size() == 1) { + final List cqlCompilerExceptions = + resolvedLibraryResults.getErrorsFor(ids.get(0)); + + if (cqlCompilerExceptions.size() == 1) { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted(ids.get(0).getId()), + cqlCompilerExceptions.get(0)); + } else { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted( + ids.get(0).getId(), + cqlCompilerExceptions.stream() + .map(CqlCompilerException::getMessage) + .reduce((s1, s2) -> s1 + "; " + s2) + .orElse("No error messages found."))); + } + } + + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted(ids, allErrors)); + + } catch (CqlIncludeException exception) { + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted( + ids.stream().map(VersionedIdentifier::getId).toList()), + exception); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index 4d19e850b1..c1e6ce41bb 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -12,9 +12,6 @@ import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.tuple.Pair; -import org.cqframework.cql.cql2elm.CqlCompilerException; -import org.cqframework.cql.cql2elm.CqlIncludeException; -import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.IntervalTypeSpecifier; import org.hl7.elm.r1.NamedTypeSpecifier; import org.hl7.elm.r1.ParameterDef; @@ -327,72 +324,10 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( return resultsBuilder.build(); } - public static List getCompiledLibraries(List ids, CqlEngine context) { - try { - var resolvedLibraryResults = - context.getEnvironment().getLibraryManager().resolveLibraries(ids); - - var allErrors = resolvedLibraryResults.allErrors(); - if (resolvedLibraryResults.hasErrors() || ids.size() > allErrors.size()) { - return resolvedLibraryResults.allCompiledLibraries(); - } - - if (ids.size() == 1) { - final List cqlCompilerExceptions = - resolvedLibraryResults.getErrorsFor(ids.get(0)); - - if (cqlCompilerExceptions.size() == 1) { - throw new IllegalStateException( - "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." - .formatted(ids.get(0).getId()), - cqlCompilerExceptions.get(0)); - } else { - throw new IllegalStateException( - "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" - .formatted( - ids.get(0).getId(), - cqlCompilerExceptions.stream() - .map(CqlCompilerException::getMessage) - .reduce((s1, s2) -> s1 + "; " + s2) - .orElse("No error messages found."))); - } - } - - throw new IllegalStateException( - "Unable to load CQL/ELM for libraries: %s Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" - .formatted(ids, allErrors)); - - } catch (CqlIncludeException exception) { - throw new IllegalStateException( - "Unable to load CQL/ELM for libraries: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." - .formatted( - ids.stream().map(VersionedIdentifier::getId).toList()), - exception); - } - } - /** - * Checks if a MeasureDef has at least one PopulationDef of type MEASUREOBSERVATION - * across all of its groups. - * - * @param measureDef the MeasureDef to check - * @return true if any PopulationDef in any GroupDef is MEASUREOBSERVATION - */ - public static boolean hasMeasureObservation(MeasureDef measureDef) { - if (measureDef == null || measureDef.groups() == null) { - return false; - } - - return measureDef.groups().stream() - .filter(group -> group.populations() != null) - .flatMap(group -> group.populations().stream()) - .anyMatch(pop -> pop.type() == MeasurePopulationType.MEASUREOBSERVATION); - } - private void validateEvaluationResultExistsForIdentifier( VersionedIdentifier versionedIdentifierFromQuery, EvaluationResultsForMultiLib evaluationResultsForMultiLib) { - // LUKETODO: this should hopefully be fixed with the next version of CQL var containsResults = evaluationResultsForMultiLib.containsResultsFor(versionedIdentifierFromQuery); var containsExceptions = evaluationResultsForMultiLib.containsExceptionsFor(versionedIdentifierFromQuery); From d13a2ba6a7f914ef7cbaa4a8f0b8995f0c7b99bf Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 12:22:02 -0400 Subject: [PATCH 10/48] Refactor tests and clean up more cruft. --- .../measure/common/MeasureProcessorUtils.java | 20 +- .../fhir/cr/measure/helper/DateHelper.java | 14 + .../cr/measure/r4/R4MeasureProcessor.java | 47 +-- ...ariableResourceMeasureObservationTest.java | 273 ++---------------- 4 files changed, 65 insertions(+), 289 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index c1e6ce41bb..0826640e40 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -24,6 +24,7 @@ import org.opencds.cqf.cql.engine.runtime.Interval; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.helper.DateHelper; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4DateHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -163,13 +164,30 @@ public void setMeasurementPeriod(Interval measurementPeriod, CqlEngine context, context.getState().setParameter(null, MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME, convertedPeriod); } + public Interval getMeasurementPeriod( + @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, CqlEngine context) { + + return getDefaultMeasurementPeriod(buildMeasurementPeriod(periodStart, periodEnd), context); + } + + // LUKETODO: use DateHelper method instead + private Interval buildMeasurementPeriod(ZonedDateTime periodStart, ZonedDateTime periodEnd) { + Interval measurementPeriod = null; + if (periodStart != null && periodEnd != null) { + // Operation parameter defined measurementPeriod + var helper = new R4DateHelper(); + measurementPeriod = helper.buildMeasurementPeriodInterval(periodStart, periodEnd); + } + return measurementPeriod; + } + /** * Get Cql MeasurementPeriod if parameters are empty * @param measurementPeriod Interval from operation parameters * @param context cql context to extract default values * @return operation parameters if populated, otherwise default CQL interval */ - public Interval getDefaultMeasurementPeriod(Interval measurementPeriod, CqlEngine context) { + public static Interval getDefaultMeasurementPeriod(Interval measurementPeriod, CqlEngine context) { if (measurementPeriod == null) { return (Interval) context.getState().getParameters().get(MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelper.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelper.java index bec6400be6..df114fa2c6 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelper.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelper.java @@ -5,12 +5,15 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.TimeZone; import org.opencds.cqf.cql.engine.runtime.DateTime; +import org.opencds.cqf.cql.engine.runtime.Interval; +import org.opencds.cqf.cql.engine.runtime.Precision; /** * Helper class to resolve measurement period start and end dates. If a timezone @@ -105,4 +108,15 @@ private static DateTime resolveDate(boolean start, String dateString) { var offset = calendar.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime(); return new DateTime(offset); } + + public static Interval buildMeasurementPeriodInterval(ZonedDateTime periodStart, ZonedDateTime periodEnd) { + return new Interval(convertToDateTime(periodStart), true, convertToDateTime(periodEnd), true); + } + + private static DateTime convertToDateTime(ZonedDateTime zonedDateTime) { + final OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime(); + final DateTime convertedDateTime = new DateTime(offsetDateTime); + convertedDateTime.setPrecision(Precision.SECOND); + return convertedDateTime; + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 814bf36856..85fb88de61 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -175,9 +175,7 @@ public MeasureReport evaluateMeasure( this.measureEvaluationOptions.getApplyScoringSetMembership(), new R4PopulationBasisValidator()); - // LUKETODO: figure out if we still need this: - var measurementPeriod = postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( - measure, periodStart, periodEnd, context); + var measurementPeriod = measureProcessorUtils.getMeasurementPeriod(periodStart, periodEnd, context); // Build Measure Report with Results return new R4MeasureReportBuilder() @@ -189,49 +187,6 @@ public MeasureReport evaluateMeasure( subjectIds); } - /** - * Do post-processing after the libraries have been evaluated, such as: setting the measurement period, - * once again, with the view to running continuousVariableObservation() and computing the - * interval used in the MeasureReportBuilder. - *

- * Now that we've pushed and popped the current library stack, we're doing it again a 3rd time, - * since this is easier to reason about than leaving duplicate libraries on the stack that - * through good fortune before we didn't accidentally evaluate twice. - */ - private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( - Measure measure, - @Nullable ZonedDateTime periodStart, - @Nullable ZonedDateTime periodEnd, - CqlEngine context) { - - var libraryVersionedIdentifiers = - getMultiLibraryIdMeasureEngineDetails(List.of(measure)).getLibraryIdentifiers(); - - var compiledLibraries = getCompiledLibraries(libraryVersionedIdentifiers, context); - - var libraries = - compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); - - // Add back the libraries to the stack, since we popped them off during CQL - context.getState().init(libraries); - - // Measurement Period: operation parameter defined measurement period - Interval measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); - - // DON'T pop the library off the stack yet, because we need it for continuousVariableObservation() - - // Populate populationDefs that require MeasureDef results - // LUKETODO: can we get rid of this? - // measureProcessorUtils.continuousVariableObservation(measureDef, context); - - // Now that we've done continuousVariableObservation(), we're safe to pop the libraries off - // the stack - popAllLibrariesFromCqlEngine(context, libraries); - - // extract measurement Period from CQL to pass to report Builder - return measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context); - } - public CompositeEvaluationResultsPerMeasure evaluateMeasureWithCqlEngine( List subjects, Either3 measureEither, diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java index b6b8e4d3cb..70fd7d5bf9 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java @@ -8,6 +8,16 @@ public class ContinuousVariableResourceMeasureObservationTest { + public static final LocalDate ENCOUNTER_BASIS_MEASUREMENT_PERIOD_START = LocalDate.of(2024, 1, 1); + + private static final LocalDate _1940_JAN_01 = LocalDate.of(1940, Month.JANUARY, 1); + private static final LocalDate _1950_JAN_01 = LocalDate.of(1950, Month.JANUARY, 1); + private static final LocalDate _1960_JAN_01 = LocalDate.of(1960, Month.JANUARY, 1); + + private static final int EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1 = computeAge(_1940_JAN_01); + private static final int EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2 = computeAge(_1950_JAN_01); + private static final int EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3 = computeAge(_1960_JAN_01); + private static final Given GIVEN_BOOLEAN_BASIS = Measure.given().repositoryFor("ContinuousVariableObservationBooleanBasis"); private static final Given GIVEN_ENCOUNTER_BASIS = @@ -60,46 +70,9 @@ void continuousVariableResourceMeasureObservationBooleanBasisAvg() { .report(); } - // LUKETODO: refactor for code reuse @Test void continuousVariableResourceMeasureObservationEncounterBasisAvg() { - /* - # total sum: 2536.0 - - # first stratum: 84: - - patient-0-encounter-1: 00:00 to 02:00 (120 minutes) - patient-1-encounter-1: 00:00 to 02:00 (120 minutes) - - sum: 240.0 - - # second stratum: 74: - - patient-2-encounter-1: 03:00 to 12:00 (540 minutes) - patient-3-encounter-1: 07:00 to 14:00 (420 minutes) - patient-4-encounter-1: 05:00 to 19:00 (840 minutes) - patient-5-encounter-1: 02:00 to 04:00 (120 minutes) - - sum: 1920.0 - - # third stratum: 64: - - patient-6-encounter-1: 03:00 to 03:30 (30 minutes) - patient-7-encounter-1: 01:00 to 02:30 (90 minutes) - patient-8-encounter-1: 00:00 to 00:15 (15 minutes) - patient-9-encounter-1: 01:01 to 03:02 (121 minutes) - patient-9-encounter-2: 01:00 to 03:00 (120 minutes) - - sum: 376.0 - */ - - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); - GIVEN_ENCOUNTER_BASIS .when() .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisAvg") @@ -122,15 +95,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisAvg() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("480.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("75.2") .up() .up() @@ -140,28 +113,6 @@ void continuousVariableResourceMeasureObservationEncounterBasisAvg() { @Test void continuousVariableResourceMeasureObservationBooleanBasisCount() { - /* - female: - - patient-1940-1 - patient-1950 - patient-1965 - patient-1970 - - male: - - patient-1940-2 - patient-1945-2 - - other: - - patient-1940-3 - patient-1945-1 - patient-1955 - - unknown: - patient-1960 - */ GIVEN_BOOLEAN_BASIS .when() @@ -210,42 +161,6 @@ void continuousVariableResourceMeasureObservationBooleanBasisCount() { @Test void continuousVariableResourceMeasureObservationEncounterBasisCount() { - /* - # total sum: 2536.0 - - # first stratum: 84: - - patient-0-encounter-1: 00:00 to 02:00 (120 minutes) - patient-1-encounter-1: 00:00 to 02:00 (120 minutes) - - sum: 240.0 - - # second stratum: 74: - - patient-2-encounter-1: 03:00 to 12:00 (540 minutes) - patient-3-encounter-1: 07:00 to 14:00 (420 minutes) - patient-4-encounter-1: 05:00 to 19:00 (840 minutes) - patient-5-encounter-1: 02:00 to 04:00 (120 minutes) - - sum: 1920.0 - - # third stratum: 64: - - patient-6-encounter-1: 03:00 to 03:30 (30 minutes) - patient-7-encounter-1: 01:00 to 02:30 (90 minutes) - patient-8-encounter-1: 00:00 to 00:15 (15 minutes) - patient-9-encounter-1: 01:01 to 03:02 (121 minutes) - patient-9-encounter-2: 01:00 to 03:00 (120 minutes) - - sum: 376.0 - */ - - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); - GIVEN_ENCOUNTER_BASIS .when() .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisCount") @@ -268,15 +183,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisCount() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("2.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("4.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("5.0") .up() .up() @@ -333,41 +248,6 @@ void continuousVariableResourceMeasureObservationBooleanBasisMedian() { @Test void continuousVariableResourceMeasureObservationEncounterBasisMedian() { - /* - # total sum: 2536.0 - - # first stratum: 84: - - patient-0-encounter-1: 00:00 to 02:00 (120 minutes) - patient-1-encounter-1: 00:00 to 02:00 (120 minutes) - - sum: 240.0 - - # second stratum: 74: - - patient-2-encounter-1: 03:00 to 12:00 (540 minutes) - patient-3-encounter-1: 07:00 to 14:00 (420 minutes) - patient-4-encounter-1: 05:00 to 19:00 (840 minutes) - patient-5-encounter-1: 02:00 to 04:00 (120 minutes) - - sum: 1920.0 - - # third stratum: 64: - - patient-6-encounter-1: 03:00 to 03:30 (30 minutes) - patient-7-encounter-1: 01:00 to 02:30 (90 minutes) - patient-8-encounter-1: 00:00 to 00:15 (15 minutes) - patient-9-encounter-1: 01:01 to 03:02 (121 minutes) - patient-9-encounter-2: 01:00 to 03:00 (120 minutes) - - sum: 376.0 - */ - - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); GIVEN_ENCOUNTER_BASIS .when() @@ -391,15 +271,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisMedian() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("480.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("90.0") .up() .up() @@ -457,42 +337,6 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { @Test void continuousVariableResourceMeasureObservationEncounterBasisMin() { - /* - # total sum: 2536.0 - - # first stratum: 84: - - patient-0-encounter-1: 00:00 to 02:00 (120 minutes) - patient-1-encounter-1: 00:00 to 02:00 (120 minutes) - - sum: 240.0 - - # second stratum: 74: - - patient-2-encounter-1: 03:00 to 12:00 (540 minutes) - patient-3-encounter-1: 07:00 to 14:00 (420 minutes) - patient-4-encounter-1: 05:00 to 19:00 (840 minutes) - patient-5-encounter-1: 02:00 to 04:00 (120 minutes) - - sum: 1920.0 - - # third stratum: 64: - - patient-6-encounter-1: 03:00 to 03:30 (30 minutes) - patient-7-encounter-1: 01:00 to 02:30 (90 minutes) - patient-8-encounter-1: 00:00 to 00:15 (15 minutes) - patient-9-encounter-1: 01:01 to 03:02 (121 minutes) - patient-9-encounter-2: 01:00 to 03:00 (120 minutes) - - sum: 376.0 - */ - - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); - GIVEN_ENCOUNTER_BASIS .when() .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisMin") @@ -515,15 +359,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisMin() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("120.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("15.0") .up() .up() @@ -533,21 +377,6 @@ void continuousVariableResourceMeasureObservationEncounterBasisMin() { @Test void continuousVariableResourceMeasureObservationBooleanBasisMax() { - /* - patient-1940-female-encounter-1.json (84) - patient-1950-female-encounter-1.json (74) - patient-1965-female-encounter-1.json (59) - patient-1970-female-encounter-1.json (54) - - patient-1940-male-encounter-1.json (84) - patient-1945-male-encounter-1.json (79) - - patient-1940-other-encounter-1.json (84) - patient-1945-other-encounter-1.json (79) - patient-1955-other-encounter-1.json (69) - - patient-1960-unknown-encounter-1.json (64) - */ GIVEN_BOOLEAN_BASIS .when() @@ -596,42 +425,6 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { @Test void continuousVariableResourceMeasureObservationEncounterBasisMax() { - /* - # total sum: 2536.0 - - # first stratum: 84: - - patient-0-encounter-1: 00:00 to 02:00 (120 minutes) - patient-1-encounter-1: 00:00 to 02:00 (120 minutes) - - sum: 240.0 - - # second stratum: 74: - - patient-2-encounter-1: 03:00 to 12:00 (540 minutes) - patient-3-encounter-1: 07:00 to 14:00 (420 minutes) - patient-4-encounter-1: 05:00 to 19:00 (840 minutes) - patient-5-encounter-1: 02:00 to 04:00 (120 minutes) - - sum: 1920.0 - - # third stratum: 64: - - patient-6-encounter-1: 03:00 to 03:30 (30 minutes) - patient-7-encounter-1: 01:00 to 02:30 (90 minutes) - patient-8-encounter-1: 00:00 to 00:15 (15 minutes) - patient-9-encounter-1: 01:01 to 03:02 (121 minutes) - patient-9-encounter-2: 01:00 to 03:00 (120 minutes) - - sum: 376.0 - */ - - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); - GIVEN_ENCOUNTER_BASIS .when() .measureId("ContinuousVariableResourceMeasureObservationEncounterBasisMax") @@ -654,15 +447,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisMax() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("840.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("121.0") .up() .up() @@ -719,11 +512,6 @@ void continuousVariableResourceMeasureObservationBooleanBasisSum() { @Test void continuousVariableResourceMeasureObservationEncounterBasisSum() { - final LocalDate measurementPeriodStart = LocalDate.of(2024, 1, 1); - - final int expectedAgeStratum1 = computeAge(measurementPeriodStart, LocalDate.of(1940, Month.JANUARY, 1)); - final int expectedAgeStratum2 = computeAge(measurementPeriodStart, LocalDate.of(1950, Month.JANUARY, 1)); - final int expectedAgeStratum3 = computeAge(measurementPeriodStart, LocalDate.of(1960, Month.JANUARY, 1)); GIVEN_ENCOUNTER_BASIS .when() @@ -747,15 +535,15 @@ void continuousVariableResourceMeasureObservationEncounterBasisSum() { .stratifierById("stratifier-age") .stratumCount(3) .firstStratum() - .hasValue(Integer.toString(expectedAgeStratum1)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("240.0") .up() .stratumByPosition(2) - .hasValue(Integer.toString(expectedAgeStratum2)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_2)) .hasScore("1920.0") .up() .stratumByPosition(3) - .hasValue(Integer.toString(expectedAgeStratum3)) + .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_3)) .hasScore("376.0") .up() .up() @@ -763,7 +551,8 @@ void continuousVariableResourceMeasureObservationEncounterBasisSum() { .report(); } - int computeAge(LocalDate measurementPeriod, LocalDate birthDate) { - return Period.between(birthDate, measurementPeriod).getYears(); + private static int computeAge(LocalDate birthDate) { + return Period.between(birthDate, ENCOUNTER_BASIS_MEASUREMENT_PERIOD_START) + .getYears(); } } From 9019842062f470fe37fe8e6fe7116c2678b298f6 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 14:31:11 -0400 Subject: [PATCH 11/48] More refactoring: Make it easier to reason about the workflow in question and cut down on structure mutation as much as possible. --- .../CompositeEvaluationResultsPerMeasure.java | 10 +-- .../ContinuousVariableObservationHandler.java | 86 ++++++++----------- ...ryHandler.java => LibraryInitHandler.java} | 7 +- .../common/MeasureObservationResult.java | 34 ++++++-- .../common/MeasureObservationResults.java | 34 ++++++++ .../measure/common/MeasureProcessorUtils.java | 14 ++- .../measure/dstu3/Dstu3MeasureProcessor.java | 2 +- .../cr/measure/r4/R4MeasureProcessor.java | 29 ------- .../cr/measure/r4/R4MeasureReportScorer.java | 68 ++++++++------- ...positeEvaluationResultsPerMeasureTest.java | 2 +- 10 files changed, 153 insertions(+), 133 deletions(-) rename cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/{LibraryHandler.java => LibraryInitHandler.java} (97%) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java index df3dcd76de..935ce4efee 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java @@ -86,7 +86,7 @@ public void addResults( List measureDefs, String subjectId, EvaluationResult evaluationResult, - List measureObservationResults) { + MeasureObservationResults measureObservationResults) { for (MeasureDef measureDef : measureDefs) { addResult(measureDef, subjectId, evaluationResult, measureObservationResults); } @@ -96,20 +96,18 @@ public void addResult( MeasureDef measureDef, String subjectId, EvaluationResult evaluationResult, - List measureObservationResults) { + MeasureObservationResults measureObservationResults) { // if we have no results, we don't need to add anything if (evaluationResult == null || evaluationResult.expressionResults.isEmpty()) { return; } - // Mutate the evaluationResults to include continuous variable evaluation results - measureObservationResults.forEach(measureObservationResult -> evaluationResult.expressionResults.put( - measureObservationResult.expressionName(), measureObservationResult.expressionResult())); + var evaluationResultToUse = measureObservationResults.withNewEvaluationResult(evaluationResult); var resultPerMeasure = resultsPerMeasure.computeIfAbsent(measureDef, k -> new HashMap<>()); - resultPerMeasure.put(subjectId, evaluationResult); + resultPerMeasure.put(subjectId, evaluationResultToUse); } public void addErrors(List measureDefs, String error) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index af3d21b332..35509a65cd 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -32,7 +32,7 @@ private ContinuousVariableObservationHandler() { // static class with private constructor } - static List continuousVariableEvaluation( + static MeasureObservationResults continuousVariableEvaluation( CqlEngine context, List measureDefs, List libraryIdentifiers, @@ -46,11 +46,11 @@ static List continuousVariableEvaluation( if (measureDefsWithMeasureObservations.isEmpty()) { // Don't need to do anything if there are no measure observations to process - return List.of(); + return MeasureObservationResults.EMPTY; } // measure Observation Path, have to re-initialize everything again - LibraryHandler.initLibraries(context, libraryIdentifiers); + LibraryInitHandler.initLibraries(context, libraryIdentifiers); final List finalResults = new ArrayList<>(); @@ -65,19 +65,22 @@ static List continuousVariableEvaluation( .toList(); for (PopulationDef populationDef : measureObservationPopulations) { // each measureObservation is evaluated - var results = processMeasureObservation( + var result = processMeasureObservation( context, evaluationResult, subjectTypePart, groupDef, populationDef); - finalResults.addAll(results); + finalResults.add(result); } } } - return finalResults; + return new MeasureObservationResults(finalResults); } - // LUKETODO: javadoc - private static List processMeasureObservation( + /** + * For a given measure observation population, do an ad-hoc function evaluation and + * accumulate the results that will be subsequently added to the CQL evaluation result. + */ + private static MeasureObservationResult processMeasureObservation( CqlEngine context, EvaluationResult evaluationResult, String subjectTypePart, @@ -105,7 +108,7 @@ private static List processMeasureObservation( tryGetExpressionResult(criteriaExpressionInput, evaluationResult); if (optExpressionResult.isEmpty()) { - return List.of(); + return MeasureObservationResult.EMPTY; } final ExpressionResult expressionResult = optExpressionResult.get(); @@ -116,19 +119,18 @@ private static List processMeasureObservation( var expressionName = criteriaPopulationId + "-" + observationExpression; // loop through measure-population results int index = 0; - Map functionResults = new HashMap<>(); - Set evaluatedResources = new HashSet<>(); - final List results = new ArrayList<>(); + final Map functionResults = new HashMap<>(); + final Set evaluatedResources = new HashSet<>(); for (Object result : resultsIter) { - Object observationResult = evaluateObservationCriteria( - result, observationExpression, evaluatedResources, groupDef.isBooleanBasis(), context); + final ObservationEvaluationResult observationResult = + evaluateObservationCriteria(result, observationExpression, groupDef.isBooleanBasis(), context); - validateObservationResult(result, observationResult); + validateObservationResult(result, observationResult.result); var observationId = expressionName + "-" + index; // wrap result in Observation resource to avoid duplicate results data loss // in set object - Observation observation = wrapResultAsObservation(observationId, observationId, observationResult); + Observation observation = wrapResultAsObservation(observationId, observationId, observationResult.result); // add function results to existing EvaluationResult under new expression // name // need a way to capture input parameter here too, otherwise we have no way @@ -136,38 +138,28 @@ private static List processMeasureObservation( // key= input parameter to function // value= the output Observation resource containing calculated value functionResults.put(result, observation); - - final ExpressionResult expressionResultFromFunction = - new ExpressionResult(functionResults, evaluatedResources); - - final MeasureObservationResult measureObservationResult = - new MeasureObservationResult(expressionName, expressionResultFromFunction); - - results.add(measureObservationResult); + evaluatedResources.addAll(observationResult.evaluatedResources); index++; } - return results; + return new MeasureObservationResult(expressionName, evaluatedResources, functionResults); } + private record ObservationEvaluationResult(Object result, Set evaluatedResources) {} + /** * method used to evaluate cql expression defined for 'continuous variable' scoring type measures that have 'measure observation' to calculate * This method is called as a second round of processing given it uses 'measure population' results as input data for function * @param resource object that stores results of cql * @param criteriaExpression expression name to call - * @param outEvaluatedResources set to store evaluated resources touched * @param isBooleanBasis the type of result created from expression * @param context cql engine context used to evaluate expression * @return cql results for subject requested */ @SuppressWarnings({"deprecation", "removal"}) - private static Object evaluateObservationCriteria( - Object resource, - String criteriaExpression, - Set outEvaluatedResources, - boolean isBooleanBasis, - CqlEngine context) { + private static ObservationEvaluationResult evaluateObservationCriteria( + Object resource, String criteriaExpression, boolean isBooleanBasis, CqlEngine context) { var ed = Libraries.resolveExpressionRef( criteriaExpression, context.getState().getCurrentLibrary()); @@ -192,9 +184,7 @@ private static Object evaluateObservationCriteria( context.getState().popActivationFrame(); } - captureEvaluatedResources(outEvaluatedResources, context); - - return result; + return new ObservationEvaluationResult(result, captureEvaluatedResources(context)); } private static Optional tryGetExpressionResult( @@ -270,14 +260,18 @@ private static boolean hasMeasureObservation(MeasureDef measureDef) { /** * method used to extract evaluated resources touched by CQL criteria expressions - * @param outEvaluatedResources set object used to capture resources touched * @param context cql engine context */ - private static void captureEvaluatedResources(Set outEvaluatedResources, CqlEngine context) { - if (outEvaluatedResources != null && context.getState().getEvaluatedResources() != null) { - outEvaluatedResources.addAll(context.getState().getEvaluatedResources()); + private static Set captureEvaluatedResources(CqlEngine context) { + final Set evaluatedResources; + if (context.getState().getEvaluatedResources() != null) { + evaluatedResources = context.getState().getEvaluatedResources(); + } else { + evaluatedResources = new HashSet<>(); } clearEvaluatedResources(context); + + return evaluatedResources; } // reset evaluated resources followed by a context evaluation @@ -328,7 +322,7 @@ private static Quantity convertToQuantity(Object obj) { * @param measureDef measure defined objects that are populated from criteria expression results * @param context cql engine context used to evaluate results */ - public static void continuousVariableObservation(MeasureDef measureDef, CqlEngine context) { + public static void continuousVariableObservationLegacyLogic(MeasureDef measureDef, CqlEngine context) { // Continuous Variable? for (GroupDef groupDef : measureDef.groups()) { // Measure Observation defined? @@ -346,14 +340,10 @@ public static void continuousVariableObservation(MeasureDef measureDef, CqlEngin Set resourcesForSubject = entry.getValue(); for (Object resource : resourcesForSubject) { - Object observationResult = evaluateObservationCriteria( - resource, - measureObservation.expression(), - measureObservation.getEvaluatedResources(), - groupDef.isBooleanBasis(), - context); - measureObservation.addResource(observationResult); - measureObservation.addResource(subjectId, observationResult); + final ObservationEvaluationResult observationResult = evaluateObservationCriteria( + resource, measureObservation.expression(), groupDef.isBooleanBasis(), context); + measureObservation.addResource(observationResult.result); + measureObservation.addResource(subjectId, observationResult.evaluatedResources); } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java similarity index 97% rename from cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java rename to cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java index 44dc66b701..01fa7caa9c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java @@ -7,12 +7,13 @@ import org.hl7.elm.r1.VersionedIdentifier; import org.opencds.cqf.cql.engine.execution.CqlEngine; -// LUKETODO: javadoc -public class LibraryHandler { +/** + * Helper class to initialize CQL libraries for CQL evaluation. + */ +public class LibraryInitHandler { // LUKETODO: extend library evaluated context so library initialization isn't having to be built for // both, and takes advantage of caching - // LUKETODO: figure out how to reuse this pattern: // LUKETODO: are we reinitializing the cache here? if so, should we try not to, somehow? public static void initLibraries(CqlEngine context, List libraryIdentifiers) { var compiledLibraries = getCompiledLibraries(libraryIdentifiers, context); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java index 258373b752..68a15c5bef 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java @@ -1,8 +1,32 @@ package org.opencds.cqf.fhir.cr.measure.common; -import org.opencds.cqf.cql.engine.execution.ExpressionResult; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; -/** - * Capture the results of continuous variable evaluation to be added to {@link org.opencds.cqf.cql.engine.execution.EvaluationResult}s - */ -public record MeasureObservationResult(String expressionName, ExpressionResult expressionResult) {} +public class MeasureObservationResult { + private final String expressionName; + private final Set evaluatedResources; + private final Map functionResults; + + static final MeasureObservationResult EMPTY = new MeasureObservationResult(null, Set.of(), Map.of()); + + MeasureObservationResult( + String expressionName, Set evaluatedResources, Map functionResults) { + this.expressionName = expressionName; + this.evaluatedResources = evaluatedResources; + this.functionResults = functionResults; + } + + String getExpressionName() { + return expressionName; + } + + Map getFunctionResults() { + return new HashMap<>(functionResults); + } + + Set getEvaluatedResources() { + return new HashSetForFhirResources<>(evaluatedResources); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java new file mode 100644 index 0000000000..771516ea26 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java @@ -0,0 +1,34 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import java.util.HashMap; +import java.util.List; +import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.cql.engine.execution.ExpressionResult; + +public class MeasureObservationResults { + private final List measureObservationResults; + + static final MeasureObservationResults EMPTY = new MeasureObservationResults(List.of()); + + MeasureObservationResults(List measureObservationResults) { + this.measureObservationResults = measureObservationResults; + } + + EvaluationResult withNewEvaluationResult(EvaluationResult origEvaluationResult) { + final EvaluationResult evaluationResult = new EvaluationResult(); + + var copyOfExpressionResults = new HashMap<>(origEvaluationResult.expressionResults); + + measureObservationResults.forEach(measureObservationResult -> copyOfExpressionResults.put( + measureObservationResult.getExpressionName(), buildExpressionResult(measureObservationResult))); + + evaluationResult.expressionResults.putAll(copyOfExpressionResults); + + return evaluationResult; + } + + private ExpressionResult buildExpressionResult(MeasureObservationResult measureObservationResult) { + return new ExpressionResult( + measureObservationResult.getFunctionResults(), measureObservationResult.getEvaluatedResources()); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index 0826640e40..7dd9d74bdc 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -24,7 +24,6 @@ import org.opencds.cqf.cql.engine.runtime.Interval; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.helper.DateHelper; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4DateHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -170,15 +169,12 @@ public Interval getMeasurementPeriod( return getDefaultMeasurementPeriod(buildMeasurementPeriod(periodStart, periodEnd), context); } - // LUKETODO: use DateHelper method instead private Interval buildMeasurementPeriod(ZonedDateTime periodStart, ZonedDateTime periodEnd) { - Interval measurementPeriod = null; - if (periodStart != null && periodEnd != null) { - // Operation parameter defined measurementPeriod - var helper = new R4DateHelper(); - measurementPeriod = helper.buildMeasurementPeriodInterval(periodStart, periodEnd); + if (periodStart == null && periodEnd == null) { + return null; } - return measurementPeriod; + // Operation parameter defined measurementPeriod + return DateHelper.buildMeasurementPeriodInterval(periodStart, periodEnd); } /** @@ -314,7 +310,7 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( var measureDefs = multiLibraryIdMeasureEngineDetails.getMeasureDefsForLibrary(libraryVersionedIdentifier); - final List measureObservationResults = + final MeasureObservationResults measureObservationResults = ContinuousVariableObservationHandler.continuousVariableEvaluation( context, measureDefs, libraryIdentifiers, evaluationResult, subjectTypePart); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index f2e0e7898b..062f986be7 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -126,7 +126,7 @@ protected MeasureReport evaluateMeasure( new Dstu3PopulationBasisValidator()); } // Populate populationDefs that require MeasureDef results - ContinuousVariableObservationHandler.continuousVariableObservation(measureDef, context); + ContinuousVariableObservationHandler.continuousVariableObservationLegacyLogic(measureDef, context); // Build Measure Report with Results return new Dstu3MeasureReportBuilder() diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 85fb88de61..889ff78f8d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -35,13 +35,9 @@ import org.opencds.cqf.fhir.cql.VersionedIdentifiers; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; -import org.opencds.cqf.fhir.cr.measure.common.GroupDef; -import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; -import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4DateHelper; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; @@ -125,11 +121,6 @@ public MeasureReport evaluateMeasureResults( this.measureEvaluationOptions.getApplyScoringSetMembership(), new R4PopulationBasisValidator()); - // Populate populationDefs that require MeasureDef results - // blocking certain continuous-variable Measures due to need of CQL context - // LUKETODO: what do we do with this? - // continuousVariableObservationCheck(measureDef, measure); - // Build Measure Report with Results return new R4MeasureReportBuilder() .build( @@ -351,26 +342,6 @@ private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails return builder.build(); } - /** Temporary check for Measures that are being blocked from use by evaluateResults method - * - * @param measureDef defined measure definition object used to capture criteria expression results - * @param measure measure resource used for evaluation - */ - protected void continuousVariableObservationCheck(MeasureDef measureDef, Measure measure) { - for (GroupDef groupDef : measureDef.groups()) { - // Measure Observation defined? - if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) - && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { - throw new InvalidRequestException( - "Measure Evaluation Mode does not have CQL engine context to support: Measure Scoring Type: %s, Measure Population Type: %s, for Measure: %s" - .formatted( - MeasureScoring.CONTINUOUSVARIABLE, - MeasurePopulationType.MEASUREOBSERVATION, - measure.getUrl())); - } - } - } - /** * method used to extract appropriate Measure Report type from operation defined Evaluation Type * @param measureEvalType operation evaluation type diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 6d40581334..982f487534 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.r4; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import jakarta.annotation.Nullable; import java.util.ArrayList; @@ -7,10 +8,10 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.commons.collections4.CollectionUtils; import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; @@ -320,14 +321,29 @@ protected void scoreStratifier( GroupDef groupDef, MeasureScoring measureScoring, MeasureReportGroupStratifierComponent stratifierComponent) { + for (StratifierGroupComponent sgc : stratifierComponent.getStratum()) { - scoreStratum(measureUrl, groupDef, measureScoring, sgc); + + // This isn't fantastic, but it seems to work + final Optional optStratifierDef = groupDef.stratifiers().stream() + .filter(stratifierDef -> stratifierComponent.getId().equals(stratifierDef.id())) + .findFirst(); + + if (optStratifierDef.isEmpty()) { + throw new InternalErrorException("Stratifier component " + sgc.getId() + " does not exist."); + } + + scoreStratum(measureUrl, groupDef, optStratifierDef.get(), measureScoring, sgc); } } protected void scoreStratum( - String measureUrl, GroupDef groupDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { - final Quantity quantity = getStratumScoreOrNull(measureUrl, groupDef, measureScoring, stratum); + String measureUrl, + GroupDef groupDef, + StratifierDef stratifierDef, + MeasureScoring measureScoring, + StratifierGroupComponent stratum) { + final Quantity quantity = getStratumScoreOrNull(measureUrl, groupDef, stratifierDef, measureScoring, stratum); if (quantity != null) { stratum.setMeasureScore(quantity); @@ -336,7 +352,11 @@ protected void scoreStratum( @Nullable private Quantity getStratumScoreOrNull( - String measureUrl, GroupDef groupDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { + String measureUrl, + GroupDef groupDef, + StratifierDef stratifierDef, + MeasureScoring measureScoring, + StratifierGroupComponent stratum) { switch (measureScoring) { case PROPORTION, RATIO -> { @@ -350,15 +370,6 @@ private Quantity getStratumScoreOrNull( return null; } case CONTINUOUSVARIABLE -> { - final List stratifiers = groupDef.stratifiers(); - - if (CollectionUtils.isEmpty(stratifiers)) { - return null; - } - - // LUKETODO: what if we have more than one stratifier?????? - final StratifierDef stratifierDef = stratifiers.get(0); - return calculateContinuousVariableAggregateQuantity( measureUrl, groupDef, @@ -381,6 +392,7 @@ private Set getResultsForStratum( PopulationDef measureObservationPopulationDef, StratifierDef stratifierDef, StratifierGroupComponent stratum) { + final String stratumValue = stratum.getValue().getText(); final Set subjectsWithStratumValue = stratifierDef.getResults().entrySet().stream() @@ -388,25 +400,15 @@ private Set getResultsForStratum( .map(Entry::getKey) .collect(Collectors.toUnmodifiableSet()); - logger.info("1234: stratum value: {}, qualifying subjects: {}", stratumValue, subjectsWithStratumValue); - - final Set qualifyingStratumResources = - measureObservationPopulationDef.getSubjectResources().entrySet().stream() - .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) - .map(Entry::getValue) - .flatMap(Collection::stream) - .collect(Collectors.toUnmodifiableSet()); - - logger.info( - "1234: stratum value: {}, qualifying subjects: {}, qualifyingStratumResources: {}", - stratumValue, - subjectsWithStratumValue, - qualifyingStratumResources); - - return qualifyingStratumResources; + return measureObservationPopulationDef.getSubjectResources().entrySet().stream() + .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) + .map(Entry::getValue) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableSet()); } - // LUKETODO: do we need to address more use cases? + // TODO: LD: we may need to match more types of stratum here: The below logic deals with + // currently anticipated use cases private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { if (rawValueFromStratifier == null || stratumValueAsString == null) { return false; @@ -422,6 +424,10 @@ private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFro return stratumValueAsString.equals(rawValueFromStratifierAsEnumeration.asStringValue()); } + if (rawValueFromStratifier instanceof String rawValueFromStratifierAsString) { + return stratumValueAsString.equals(rawValueFromStratifierAsString); + } + return false; } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java index 3ce3e944bf..1ae146ee4d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasureTest.java @@ -24,7 +24,7 @@ void gettersContainExpectedData() { er.expressionResults.put("subject-123", null); // non-empty map is all the Builder checks CompositeEvaluationResultsPerMeasure.Builder builder = CompositeEvaluationResultsPerMeasure.builder(); - builder.addResult(measureDef1, "subject-123", er, List.of()); + builder.addResult(measureDef1, "subject-123", er, MeasureObservationResults.EMPTY); builder.addError(measureDef1, "oops-1"); builder.addError(measureDef2, "oops-2"); From 57b338e2474661fe2fe211c66dc6bf8f61247c1d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 14:40:58 -0400 Subject: [PATCH 12/48] Add a new record and resolve LibraryInitHandler TODOs. --- .../cqf/fhir/cr/measure/common/LibraryInitHandler.java | 7 ++++--- .../cr/measure/common/ObservationEvaluationResult.java | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java index 01fa7caa9c..419aacd58e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java @@ -12,9 +12,10 @@ */ public class LibraryInitHandler { - // LUKETODO: extend library evaluated context so library initialization isn't having to be built for - // both, and takes advantage of caching - // LUKETODO: are we reinitializing the cache here? if so, should we try not to, somehow? + private LibraryInitHandler() { + // static class + } + public static void initLibraries(CqlEngine context, List libraryIdentifiers) { var compiledLibraries = getCompiledLibraries(libraryIdentifiers, context); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java new file mode 100644 index 0000000000..0c1a3f2cc7 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import java.util.Set; + +/** + * A glorified Pair to capture both evaluation results and evaluated resources. + */ +public record ObservationEvaluationResult(Object result, Set evaluatedResources) {} From f387a04c82d004de0ba30740c5ae3f9d32043e5a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 14:59:06 -0400 Subject: [PATCH 13/48] Final round of refactoring to make code easier to reason about. Also, resolve Sonar issues. --- .../ContinuousVariableObservationHandler.java | 69 ++++++------------- .../cqf/fhir/cr/measure/common/GroupDef.java | 1 + .../common/MeasureObservationResult.java | 26 +++---- .../common/MeasureObservationResults.java | 16 ++--- .../measure/dstu3/Dstu3MeasureProcessor.java | 50 +++++++++++++- .../cr/measure/r4/R4MeasureProcessor.java | 2 +- .../cr/measure/r4/R4MeasureReportScorer.java | 1 + ...ariableResourceMeasureObservationTest.java | 24 +++---- .../cqf/fhir/cr/measure/r4/Measure.java | 5 -- 9 files changed, 98 insertions(+), 96 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 35509a65cd..30f2660ebc 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -1,7 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.common; -import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; - import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.ArrayList; @@ -13,6 +11,7 @@ import java.util.Optional; import java.util.Set; import org.hl7.elm.r1.FunctionDef; +import org.hl7.elm.r1.OperandDef; import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Observation; @@ -117,20 +116,20 @@ private static MeasureObservationResult processMeasureObservation( // make new expression name for uniquely extracting results // this will be used in MeasureEvaluator var expressionName = criteriaPopulationId + "-" + observationExpression; + // loop through measure-population results int index = 0; final Map functionResults = new HashMap<>(); final Set evaluatedResources = new HashSet<>(); + for (Object result : resultsIter) { final ObservationEvaluationResult observationResult = evaluateObservationCriteria(result, observationExpression, groupDef.isBooleanBasis(), context); - validateObservationResult(result, observationResult.result); - var observationId = expressionName + "-" + index; // wrap result in Observation resource to avoid duplicate results data loss // in set object - Observation observation = wrapResultAsObservation(observationId, observationId, observationResult.result); + var observation = wrapResultAsObservation(observationId, observationId, observationResult.result()); // add function results to existing EvaluationResult under new expression // name // need a way to capture input parameter here too, otherwise we have no way @@ -138,7 +137,7 @@ private static MeasureObservationResult processMeasureObservation( // key= input parameter to function // value= the output Observation resource containing calculated value functionResults.put(result, observation); - evaluatedResources.addAll(observationResult.evaluatedResources); + evaluatedResources.addAll(observationResult.evaluatedResources()); index++; } @@ -146,8 +145,6 @@ private static MeasureObservationResult processMeasureObservation( return new MeasureObservationResult(expressionName, evaluatedResources, functionResults); } - private record ObservationEvaluationResult(Object result, Set evaluatedResources) {} - /** * method used to evaluate cql expression defined for 'continuous variable' scoring type measures that have 'measure observation' to calculate * This method is called as a second round of processing given it uses 'measure population' results as input data for function @@ -158,7 +155,7 @@ private record ObservationEvaluationResult(Object result, Set evaluatedR * @return cql results for subject requested */ @SuppressWarnings({"deprecation", "removal"}) - private static ObservationEvaluationResult evaluateObservationCriteria( + public static ObservationEvaluationResult evaluateObservationCriteria( Object resource, String criteriaExpression, boolean isBooleanBasis, CqlEngine context) { var ed = Libraries.resolveExpressionRef( @@ -174,9 +171,15 @@ private static ObservationEvaluationResult evaluateObservationCriteria( try { if (!isBooleanBasis) { // subject based observations don't have a parameter to pass in - // LUKETODO: error handling - context.getState() - .push(new Variable(functionDef.getOperand().get(0).getName()).withValue(resource)); + final List operands = functionDef.getOperand(); + + if (operands.isEmpty()) { + throw new InternalErrorException("Measure observation %s does not reference a boolean basis"); + } + + final Variable variableToPush = new Variable(operands.get(0).getName()).withValue(resource); + + context.getState().push(variableToPush); } result = context.getEvaluationVisitor().visitExpression(ed.getExpression(), context.getState()); @@ -184,7 +187,11 @@ private static ObservationEvaluationResult evaluateObservationCriteria( context.getState().popActivationFrame(); } - return new ObservationEvaluationResult(result, captureEvaluatedResources(context)); + final Set evaluatedResources = captureEvaluatedResources(context); + + validateObservationResult(resource, result); + + return new ObservationEvaluationResult(result, evaluatedResources); } private static Optional tryGetExpressionResult( @@ -313,40 +320,4 @@ private static Quantity convertToQuantity(Object obj) { return q; } - - // LUKETODO: this is used by DSTU3 only: what to do with this? - /** - * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. - * This method is a downstream calculation given it requires calculated results before it can be called. - * Results are then added to associated MeasureDef - * @param measureDef measure defined objects that are populated from criteria expression results - * @param context cql engine context used to evaluate results - */ - public static void continuousVariableObservationLegacyLogic(MeasureDef measureDef, CqlEngine context) { - // Continuous Variable? - for (GroupDef groupDef : measureDef.groups()) { - // Measure Observation defined? - if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) - && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { - - PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); - PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); - - // Inject MeasurePopulation results into Measure Observation Function - Map> subjectResources = measurePopulation.getSubjectResources(); - - for (Map.Entry> entry : subjectResources.entrySet()) { - String subjectId = entry.getKey(); - Set resourcesForSubject = entry.getValue(); - - for (Object resource : resourcesForSubject) { - final ObservationEvaluationResult observationResult = evaluateObservationCriteria( - resource, measureObservation.expression(), groupDef.isBooleanBasis(), context); - measureObservation.addResource(observationResult.result); - measureObservation.addResource(subjectId, observationResult.evaluatedResources); - } - } - } - } - } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java index 68d9c24cdb..e5019bf8a0 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.stream.Collectors; +@SuppressWarnings("squid:S107") public class GroupDef { private final String id; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java index 68a15c5bef..4f4873b9b2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java @@ -4,29 +4,21 @@ import java.util.Map; import java.util.Set; -public class MeasureObservationResult { - private final String expressionName; - private final Set evaluatedResources; - private final Map functionResults; +/** + * Capture a single set of results from continuous variable observations for a single population + */ +public record MeasureObservationResult( + String expressionName, Set evaluatedResources, Map functionResults) { static final MeasureObservationResult EMPTY = new MeasureObservationResult(null, Set.of(), Map.of()); - MeasureObservationResult( - String expressionName, Set evaluatedResources, Map functionResults) { - this.expressionName = expressionName; - this.evaluatedResources = evaluatedResources; - this.functionResults = functionResults; - } - - String getExpressionName() { - return expressionName; - } - - Map getFunctionResults() { + @Override + public Map functionResults() { return new HashMap<>(functionResults); } - Set getEvaluatedResources() { + @Override + public Set evaluatedResources() { return new HashSetForFhirResources<>(evaluatedResources); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java index 771516ea26..de1d57c025 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java @@ -5,22 +5,20 @@ import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; -public class MeasureObservationResults { - private final List measureObservationResults; +/** + * Capture results for multiple continuous variable populations. + */ +public record MeasureObservationResults(List results) { static final MeasureObservationResults EMPTY = new MeasureObservationResults(List.of()); - MeasureObservationResults(List measureObservationResults) { - this.measureObservationResults = measureObservationResults; - } - EvaluationResult withNewEvaluationResult(EvaluationResult origEvaluationResult) { final EvaluationResult evaluationResult = new EvaluationResult(); var copyOfExpressionResults = new HashMap<>(origEvaluationResult.expressionResults); - measureObservationResults.forEach(measureObservationResult -> copyOfExpressionResults.put( - measureObservationResult.getExpressionName(), buildExpressionResult(measureObservationResult))); + results.forEach(measureObservationResult -> copyOfExpressionResults.put( + measureObservationResult.expressionName(), buildExpressionResult(measureObservationResult))); evaluationResult.expressionResults.putAll(copyOfExpressionResults); @@ -29,6 +27,6 @@ EvaluationResult withNewEvaluationResult(EvaluationResult origEvaluationResult) private ExpressionResult buildExpressionResult(MeasureObservationResult measureObservationResult) { return new ExpressionResult( - measureObservationResult.getFunctionResults(), measureObservationResult.getEvaluatedResources()); + measureObservationResult.functionResults(), measureObservationResult.evaluatedResources()); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index 062f986be7..88eec70199 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -1,5 +1,8 @@ package org.opencds.cqf.fhir.cr.measure.dstu3; +import static org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationHandler.evaluateObservationCriteria; +import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; + import ca.uhn.fhir.repository.IRepository; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.time.ZonedDateTime; @@ -10,6 +13,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.cqframework.cql.cql2elm.CqlIncludeException; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; @@ -26,11 +30,16 @@ import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationHandler; +import org.opencds.cqf.fhir.cr.measure.common.GroupDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; +import org.opencds.cqf.fhir.cr.measure.common.ObservationEvaluationResult; +import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; import org.opencds.cqf.fhir.utility.repository.FederatedRepository; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; @@ -109,7 +118,7 @@ protected MeasureReport evaluateMeasure( Optional.ofNullable(measure.getUrl()).map(List::of).orElse(List.of("Unknown Measure URL"))); // extract measurement Period from CQL to pass to report Builder Interval measurementPeriod = - measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context); + MeasureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context); // set offset of operation parameter measurement period ZonedDateTime zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval(measurementPeriod); // populate results from Library $evaluate @@ -126,13 +135,48 @@ protected MeasureReport evaluateMeasure( new Dstu3PopulationBasisValidator()); } // Populate populationDefs that require MeasureDef results - ContinuousVariableObservationHandler.continuousVariableObservationLegacyLogic(measureDef, context); + continuousVariableObservationLegacyLogic(measureDef, context); // Build Measure Report with Results return new Dstu3MeasureReportBuilder() .build(measure, measureDef, evalTypeToReportType(evalType), measurementPeriod, subjects); } + /** + * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. + * This method is a downstream calculation given it requires calculated results before it can be called. + * Results are then added to associated MeasureDef + * @param measureDef measure defined objects that are populated from criteria expression results + * @param context cql engine context used to evaluate results + */ + private static void continuousVariableObservationLegacyLogic(MeasureDef measureDef, CqlEngine context) { + // Continuous Variable? + for (GroupDef groupDef : measureDef.groups()) { + // Measure Observation defined? + if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) + && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { + + PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); + PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); + + // Inject MeasurePopulation results into Measure Observation Function + Map> subjectResources = measurePopulation.getSubjectResources(); + + for (Map.Entry> entry : subjectResources.entrySet()) { + String subjectId = entry.getKey(); + Set resourcesForSubject = entry.getValue(); + + for (Object resource : resourcesForSubject) { + final ObservationEvaluationResult observationResult = evaluateObservationCriteria( + resource, measureObservation.expression(), groupDef.isBooleanBasis(), context); + measureObservation.addResource(observationResult.result()); + measureObservation.addResource(subjectId, observationResult.evaluatedResources()); + } + } + } + } + } + // Ideally this would be done in MeasureProcessorUtils, but it's too much work to change for now private MultiLibraryIdMeasureEngineDetails buildLibraryIdEngineDetails( Measure measure, Parameters parameters, CqlEngine context) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 889ff78f8d..bf186bcf04 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -255,7 +255,7 @@ public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( var measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); var zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval( - measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context)); + MeasureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context)); // Do this to be backwards compatible with the previous single-library evaluation: // Trigger first-pass validation on measure scoring as well as other aspects of the Measures diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 982f487534..c5b6a7b11d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -83,6 +83,7 @@ * *

(v3.18.0 and below) Previous calculation of measure score from MeasureReport only interpreted Numerator, Denominator membership since exclusions and exceptions were already applied. Now exclusions and exceptions are present in Denominator and Numerator populations, the measure scorer calculation has to take into account additional population membership to determine Final-Numerator and Final-Denominator values

*/ +@SuppressWarnings("squid:S1135") public class R4MeasureReportScorer extends BaseMeasureReportScorer { private static final Logger logger = LoggerFactory.getLogger(R4MeasureReportScorer.class); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java index 70fd7d5bf9..a0c1c45817 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java @@ -48,7 +48,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisAvg() { .up() .hasScore("73.0") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("81.5") @@ -93,7 +93,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisAvg() { .up() .hasScore("230.54545454545453") .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") @@ -136,7 +136,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisCount() { .up() .hasScore("10.0") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("2.0") @@ -181,7 +181,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisCount() { .up() .hasScore("11.0") // I assume this is the straight-up count of encounters? .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("2.0") @@ -224,7 +224,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMedian() { .up() .hasScore("76.5") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("81.5") @@ -269,7 +269,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMedian() { .up() .hasScore("120.0") .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") @@ -312,7 +312,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { .up() .hasScore("54.0") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("79.0") @@ -357,7 +357,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMin() { .up() .hasScore("15.0") .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") @@ -400,7 +400,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { .up() .hasScore("84.0") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("84.0") @@ -445,7 +445,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMax() { .up() .hasScore("840.0") .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("120.0") @@ -488,7 +488,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisSum() { .up() .hasScore("730.0") .stratifierById("stratifier-gender") - .stratumCount(4) + .hasStratumCount(4) .firstStratum() .hasValue("male") .hasScore("163.0") @@ -533,7 +533,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisSum() { .up() .hasScore("2536.0") .stratifierById("stratifier-age") - .stratumCount(3) + .hasStratumCount(3) .firstStratum() .hasValue(Integer.toString(EXPECTED_ENCOUNTER_BASIS_AGE_STRATUM_1)) .hasScore("240.0") diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index 85aef0b4e0..6bac55ecdc 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -1027,11 +1027,6 @@ public SelectedStratum stratumByPosition(int position) { return new SelectedStratum(value().getStratum().get(position - 1), this); } - public SelectedStratifier stratumCount(int stratumCount) { - assertEquals(stratumCount, value().getStratum().size()); - return this; - } - public SelectedStratifier hasStratum(String textValue) { final SelectedStratum stratum = stratum(textValue); assertNotNull(stratum.value()); From dfcc0f14f384c6a1052e38afb8e524d5b7032ed0 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 16 Oct 2025 17:44:06 -0400 Subject: [PATCH 14/48] Optimize algorithm for initializing the library for a given measure observation evaluation. --- .../ContinuousVariableObservationHandler.java | 59 +++++++++++-------- .../cr/measure/common/LibraryInitHandler.java | 12 +++- .../measure/common/MeasureProcessorUtils.java | 8 ++- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 30f2660ebc..1e921e2d30 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -34,7 +34,7 @@ private ContinuousVariableObservationHandler() { static MeasureObservationResults continuousVariableEvaluation( CqlEngine context, List measureDefs, - List libraryIdentifiers, + VersionedIdentifier libraryIdentifier, EvaluationResult evaluationResult, String subjectTypePart) { @@ -48,28 +48,35 @@ static MeasureObservationResults continuousVariableEvaluation( return MeasureObservationResults.EMPTY; } - // measure Observation Path, have to re-initialize everything again - LibraryInitHandler.initLibraries(context, libraryIdentifiers); + final boolean hasLibraryInitialized = LibraryInitHandler.initLibrary(context, libraryIdentifier); final List finalResults = new ArrayList<>(); - // one Library may be linked to multiple Measures - for (MeasureDef measureDefWithMeasureObservations : measureDefsWithMeasureObservations) { - - // get function for measure-observation from populationDef - for (GroupDef groupDef : measureDefWithMeasureObservations.groups()) { - - final List measureObservationPopulations = groupDef.populations().stream() - .filter(populationDef -> MeasurePopulationType.MEASUREOBSERVATION.equals(populationDef.type())) - .toList(); - for (PopulationDef populationDef : measureObservationPopulations) { - // each measureObservation is evaluated - var result = processMeasureObservation( - context, evaluationResult, subjectTypePart, groupDef, populationDef); - - finalResults.add(result); + try { + // one Library may be linked to multiple Measures + for (MeasureDef measureDefWithMeasureObservations : measureDefsWithMeasureObservations) { + + // get function for measure-observation from populationDef + for (GroupDef groupDef : measureDefWithMeasureObservations.groups()) { + + final List measureObservationPopulations = groupDef.populations().stream() + .filter(populationDef -> + MeasurePopulationType.MEASUREOBSERVATION.equals(populationDef.type())) + .toList(); + for (PopulationDef populationDef : measureObservationPopulations) { + // each measureObservation is evaluated + var result = processMeasureObservation( + context, evaluationResult, subjectTypePart, groupDef, populationDef); + + finalResults.add(result); + } } } + } finally { + // We don't want to pop a non-existent library + if (hasLibraryInitialized) { + LibraryInitHandler.popLibrary(context); + } } return new MeasureObservationResults(finalResults); @@ -112,7 +119,7 @@ private static MeasureObservationResult processMeasureObservation( final ExpressionResult expressionResult = optExpressionResult.get(); // makes expression results iterable - var resultsIter = getResultIterable(evaluationResult, expressionResult, subjectTypePart); + final Iterable resultsIter = getResultIterable(evaluationResult, expressionResult, subjectTypePart); // make new expression name for uniquely extracting results // this will be used in MeasureEvaluator var expressionName = criteriaPopulationId + "-" + observationExpression; @@ -146,12 +153,14 @@ private static MeasureObservationResult processMeasureObservation( } /** - * method used to evaluate cql expression defined for 'continuous variable' scoring type measures that have 'measure observation' to calculate - * This method is called as a second round of processing given it uses 'measure population' results as input data for function - * @param resource object that stores results of cql - * @param criteriaExpression expression name to call - * @param isBooleanBasis the type of result created from expression - * @param context cql engine context used to evaluate expression + * method used to evaluate cql expression defined for 'continuous variable' scoring type + * measures that have 'measure observation' to calculate This method is called as a second round + * of processing given it uses 'measure population' results as input data for function + * + * @param resource object that stores results of cql + * @param criteriaExpression expression name to call + * @param isBooleanBasis the type of result created from expression + * @param context cql engine context used to evaluate expression * @return cql results for subject requested */ @SuppressWarnings({"deprecation", "removal"}) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java index 419aacd58e..e566128a9a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java @@ -16,7 +16,15 @@ private LibraryInitHandler() { // static class } - public static void initLibraries(CqlEngine context, List libraryIdentifiers) { + public static boolean initLibrary(CqlEngine context, VersionedIdentifier libraryIdentifiers) { + return initLibraries(context, List.of(libraryIdentifiers)); + } + + public static void popLibrary(CqlEngine context) { + context.getState().exitLibrary(true); + } + + private static boolean initLibraries(CqlEngine context, List libraryIdentifiers) { var compiledLibraries = getCompiledLibraries(libraryIdentifiers, context); var libraries = @@ -24,6 +32,8 @@ public static void initLibraries(CqlEngine context, List li // Add back the libraries to the stack, since we popped them off during CQL context.getState().init(libraries); + + return !libraries.isEmpty(); } private static List getCompiledLibraries(List ids, CqlEngine context) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index 7dd9d74bdc..b0e821daa1 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -304,7 +304,7 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( for (var libraryVersionedIdentifier : libraryIdentifiers) { validateEvaluationResultExistsForIdentifier( libraryVersionedIdentifier, evaluationResultsForMultiLib); - // standard CQL expression results: if there are + // standard CQL expression results var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); var measureDefs = @@ -312,7 +312,11 @@ public CompositeEvaluationResultsPerMeasure getEvaluationResults( final MeasureObservationResults measureObservationResults = ContinuousVariableObservationHandler.continuousVariableEvaluation( - context, measureDefs, libraryIdentifiers, evaluationResult, subjectTypePart); + context, + measureDefs, + libraryVersionedIdentifier, + evaluationResult, + subjectTypePart); resultsBuilder.addResults(measureDefs, subjectId, evaluationResult, measureObservationResults); From 04435730999544abb870662bb93fd5ced3e7021d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 22 Oct 2025 11:16:15 -0400 Subject: [PATCH 15/48] Implement optimization to PopulationDef subject and resource structures. All tests pass, but further optimizations are needed both to code quality and possible bugs not captured by tests. --- .../cr/measure/common/MeasureEvaluator.java | 52 ++++---- .../fhir/cr/measure/common/PopulationDef.java | 119 +++++++++++++++--- .../measure/dstu3/Dstu3MeasureProcessor.java | 2 +- .../cr/measure/common/PopulationDefTest.java | 12 +- .../r4/R4MeasureReportBuilderTest.java | 2 +- .../encounter/enc_in_progress_pat2_2025.json | 2 +- 6 files changed, 133 insertions(+), 56 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 0d827708e4..826cbbfb90 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -171,16 +171,11 @@ protected PopulationDef evaluatePopulationMembership( int i = 0; for (Object resource : evaluatePopulationCriteria( subjectType, matchingResult, evaluationResult, inclusionDef.getEvaluatedResources())) { - inclusionDef.addResource(resource); // hashmap instead of set inclusionDef.addResource(subjectId, resource); i++; } - // If SubjectId Added Resources to Population - if (i > 0) { - inclusionDef.addSubject(subjectId); - } return inclusionDef; } @@ -213,11 +208,11 @@ protected void evaluateProportion( numerator = evaluatePopulationMembership(subjectType, subjectId, numerator, evaluationResult); if (applyScoring) { // remove denominator values not in IP - denominator.getResources().retainAll(initialPopulation.getResources()); - denominator.getSubjects().retainAll(initialPopulation.getSubjects()); + denominator.retainAllResources(initialPopulation.getResources()); + denominator.retainAllSubjects(initialPopulation.getSubjects()); // remove numerator values if not in Denominator - numerator.getSubjects().retainAll(denominator.getSubjects()); - numerator.getResources().retainAll(denominator.getResources()); + numerator.retainAllSubjects(denominator.getSubjects()); + numerator.retainAllResources(denominator.getResources()); } // Evaluate Exclusions and Exception Populations if (denominatorExclusion != null) { @@ -237,29 +232,29 @@ protected void evaluateProportion( // Remove Subject and Resource Exclusions if (denominatorExclusion != null && applyScoring) { // numerator should not include den-exclusions - numerator.getSubjects().removeAll(denominatorExclusion.getSubjects()); + numerator.removeAllSubjects(denominatorExclusion.getSubjects()); numerator.removeOverlaps(denominatorExclusion.getSubjectResources()); // verify exclusion results are found in denominator - denominatorExclusion.getResources().retainAll(denominator.getResources()); - denominatorExclusion.getSubjects().retainAll(denominator.getSubjects()); + denominatorExclusion.retainAllResources(denominator.getResources()); + denominatorExclusion.retainAllSubjects(denominator.getSubjects()); denominatorExclusion.retainOverlaps(denominator.getSubjectResources()); } if (numeratorExclusion != null && applyScoring) { // verify results are in Numerator - numeratorExclusion.getResources().retainAll(numerator.getResources()); - numeratorExclusion.getSubjects().retainAll(numerator.getSubjects()); + numeratorExclusion.retainAllResources(numerator.getResources()); + numeratorExclusion.retainAllSubjects(numerator.getSubjects()); numeratorExclusion.retainOverlaps(numerator.getSubjectResources()); } if (denominatorException != null && applyScoring) { // Remove Subjects Exceptions that are present in Numerator - denominatorException.getSubjects().removeAll(numerator.getSubjects()); - denominatorException.getResources().removeAll(numerator.getResources()); + denominatorException.removeAllSubjects(numerator.getSubjects()); + denominatorException.removeAllResources(numerator.getResources()); denominatorException.removeOverlaps(numerator.getSubjectResources()); // verify exception results are found in denominator - denominatorException.getResources().retainAll(denominator.getResources()); - denominatorException.getSubjects().retainAll(denominator.getSubjects()); + denominatorException.retainAllResources(denominator.getResources()); + denominatorException.retainAllSubjects(denominator.getSubjects()); denominatorException.retainOverlaps(denominator.getSubjectResources()); } } else { @@ -268,29 +263,30 @@ protected void evaluateProportion( // * This is why we only remove resources and not subjects too for `Resource Basis`. if (denominatorExclusion != null && applyScoring) { // remove any denominator-exception subjects/resources found in Numerator - numerator.getResources().removeAll(denominatorExclusion.getResources()); + numerator.removeAllResources(denominatorExclusion.getResources()); numerator.removeOverlaps(denominatorExclusion.getSubjectResources()); // verify exclusion results are found in denominator - denominatorExclusion.getResources().retainAll(denominator.getResources()); + denominatorExclusion.retainAllResources(denominator.getResources()); denominatorExclusion.retainOverlaps(denominator.getSubjectResources()); } if (numeratorExclusion != null && applyScoring) { // verify exclusion results are found in numerator results, otherwise remove - numeratorExclusion.getResources().retainAll(numerator.getResources()); + numeratorExclusion.retainAllResources(numerator.getResources()); numeratorExclusion.retainOverlaps(numerator.getSubjectResources()); } if (denominatorException != null && applyScoring) { // Remove Resource Exceptions that are present in Numerator - denominatorException.getResources().removeAll(numerator.getResources()); + denominatorException.removeAllResources(numerator.getResources()); denominatorException.removeOverlaps(numerator.getSubjectResources()); // verify exception results are found in denominator - denominatorException.getResources().retainAll(denominator.getResources()); + denominatorException.retainAllResources(denominator.getResources()); denominatorException.retainOverlaps(denominator.getSubjectResources()); } } if (reportType.equals(MeasureReportType.INDIVIDUAL) && dateOfCompliance != null) { var doc = evaluateDateOfCompliance(dateOfCompliance, evaluationResult); - dateOfCompliance.addResource(doc); + // LUKETODO: similarly, we'd have to add the subject as well here + dateOfCompliance.addResource(subjectId, doc); } } @@ -314,8 +310,8 @@ protected void evaluateContinuousVariable( measurePopulation = evaluatePopulationMembership(subjectType, subjectId, measurePopulation, evaluationResult); if (measurePopulation != null && initialPopulation != null && applyScoring) { // verify initial-population are in measure-population - measurePopulation.getResources().retainAll(initialPopulation.getResources()); - measurePopulation.getSubjects().retainAll(initialPopulation.getSubjects()); + measurePopulation.retainAllResources(initialPopulation.getResources()); + measurePopulation.retainAllSubjects(initialPopulation.getSubjects()); } if (measurePopulationExclusion != null) { @@ -323,8 +319,8 @@ protected void evaluateContinuousVariable( subjectType, subjectId, groupDef.getSingle(MEASUREPOPULATIONEXCLUSION), evaluationResult); if (applyScoring && measurePopulation != null) { // verify exclusions are in measure-population - measurePopulationExclusion.getResources().retainAll(measurePopulation.getResources()); - measurePopulationExclusion.getSubjects().retainAll(measurePopulation.getSubjects()); + measurePopulationExclusion.retainAllResources(measurePopulation.getResources()); + measurePopulationExclusion.retainAllSubjects(measurePopulation.getSubjects()); } } if (measurePopulationObservation != null) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java index 1c1253f65a..f28e14bea4 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java @@ -1,12 +1,22 @@ package org.opencds.cqf.fhir.cr.measure.common; import jakarta.annotation.Nullable; +import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PopulationDef { + private static final Logger logger = LoggerFactory.getLogger(PopulationDef.class); + private final String id; private final String expression; private final ConceptDef code; @@ -49,10 +59,6 @@ public ConceptDef code() { return this.code; } - public void addEvaluatedResource(Object resource) { - this.getEvaluatedResources().add(resource); - } - public Set getEvaluatedResources() { if (this.evaluatedResources == null) { this.evaluatedResources = new HashSetForFhirResources<>(); @@ -61,32 +67,107 @@ public Set getEvaluatedResources() { return this.evaluatedResources; } - public void addSubject(String subject) { - this.getSubjects().add(subject); + public Set getSubjects() { + return this.getSubjectResources().keySet(); } - public void removeSubject(String subject) { - this.getSubjects().remove(subject); + public void retainAllResources(Set resourcesToRetain) { + var resourcesToRetainForDebug = resourcesToRetain.stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + var resourcesInDefForDebug = getResources().stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + + final Iterator>> entryIterator = + getSubjectResources().entrySet().iterator(); + + while (entryIterator.hasNext()) { + final Set resourcesFromEntry = entryIterator.next().getValue(); + + resourcesFromEntry.retainAll(resourcesToRetain); + } + + var postRetainResourcesInDef = getResources().stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + logger.info( + "resourcesInDef:\n{},\nresourcesToRetain:\n{},\npostRetainResourcesInDef:\n{}", + resourcesInDefForDebug, + resourcesToRetainForDebug, + postRetainResourcesInDef); + } + + public void retainAllSubjects(Set subjects) { + this.getSubjects().retainAll(subjects); + } + + public void removeAllResources(Set resourcesToRemove) { + var resourcesToRemoveForDebug = resourcesToRemove.stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + var resourcesInDefForDebug = getResources().stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + // LUKETODO: I think this is too aggressive, even though the tests seem to pass + getSubjectResources().entrySet().removeIf(entry -> containsResourceForRemove(resourcesToRemove, entry)); + var postRemoveResourcesInDef = getResources().stream() + .map(res -> (res instanceof IBaseResource base) + ? base.getIdElement().getValueAsString() + : "?") + .collect(Collectors.toSet()); + logger.info( + "resourcesInDef:\n{},\nresourcesToRemove:\n{},\npostRemoveResourcesInDef:\n{}", + resourcesInDefForDebug, + resourcesToRemoveForDebug, + postRemoveResourcesInDef); + } + + private boolean containsResourceForRetain(Set resources, Entry> entry) { + final HashSetForFhirResources resourcesToTestForRetain = new HashSetForFhirResources<>(resources); + + final Set resourcesInEntry = entry.getValue(); + + for (Object resourceInEntry : resourcesInEntry) { + if (resourcesToTestForRetain.contains(resourceInEntry)) { + return true; + } + } + + return false; } - public Set getSubjects() { - if (this.subjects == null) { - this.subjects = new HashSetForFhirResources<>(); + private boolean containsResourceForRemove(Set resources, Entry> entry) { + final Set resourcesInEntry = entry.getValue(); + + for (Object resource : resources) { + if (resourcesInEntry.contains(resource)) { + return true; + } } - return this.subjects; + return false; } - public void addResource(Object resource) { - this.getResources().add(resource); + public void removeAllSubjects(Set subjects) { + this.getSubjects().removeAll(subjects); } public Set getResources() { - if (this.resources == null) { - this.resources = new HashSetForFhirResources<>(); - } - - return this.resources; + return new HashSetForFhirResources<>(subjectResources.values().stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableSet())); } @Nullable diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index 88eec70199..8a360ed024 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -169,7 +169,7 @@ private static void continuousVariableObservationLegacyLogic(MeasureDef measureD for (Object resource : resourcesForSubject) { final ObservationEvaluationResult observationResult = evaluateObservationCriteria( resource, measureObservation.expression(), groupDef.isBooleanBasis(), context); - measureObservation.addResource(observationResult.result()); + measureObservation.addResource(subjectId, observationResult.result()); measureObservation.addResource(subjectId, observationResult.evaluatedResources()); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDefTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDefTest.java index 96d1493bad..343474db64 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDefTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDefTest.java @@ -15,8 +15,8 @@ void setHandlingStrings() { final PopulationDef popDef1 = new PopulationDef("one", null, null, null); final PopulationDef popDef2 = new PopulationDef("two", null, null, null); - popDef1.addResource("string1"); - popDef2.addResource("string1"); + popDef1.addResource("subj1", "string1"); + popDef2.addResource("subj1", "string1"); popDef1.getResources().retainAll(popDef2.getResources()); assertEquals(1, popDef1.getResources().size()); @@ -28,8 +28,8 @@ void setHandlingIntegers() { final PopulationDef popDef1 = new PopulationDef("one", null, null, null); final PopulationDef popDef2 = new PopulationDef("two", null, null, null); - popDef1.addResource(123); - popDef2.addResource(123); + popDef1.addResource("subj1", 123); + popDef2.addResource("subj1", 123); popDef1.getResources().retainAll(popDef2.getResources()); assertEquals(1, popDef1.getResources().size()); @@ -44,8 +44,8 @@ void setHandlingEncounters() { final Encounter enc1a = (Encounter) new Encounter().setId(new IdType(ResourceType.Encounter.name(), "enc1")); final Encounter enc1b = (Encounter) new Encounter().setId(new IdType(ResourceType.Encounter.name(), "enc1")); - popDef1.addResource(enc1a); - popDef2.addResource(enc1b); + popDef1.addResource("subj1", enc1a); + popDef2.addResource("subj1", enc1b); popDef1.getResources().retainAll(popDef2.getResources()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java index a81c68d34f..841d7812d1 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java @@ -265,7 +265,7 @@ private static PopulationDef buildPopulationRef(Collection resources) { null); if (resources != null) { - resources.forEach(populationDef::addResource); + resources.forEach(res -> populationDef.addResource("subj", res)); } return populationDef; diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CriteriaBasedStratifiersComplex/input/tests/encounter/enc_in_progress_pat2_2025.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CriteriaBasedStratifiersComplex/input/tests/encounter/enc_in_progress_pat2_2025.json index 9cdd7bd85b..8ff0b7219a 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CriteriaBasedStratifiersComplex/input/tests/encounter/enc_in_progress_pat2_2025.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CriteriaBasedStratifiersComplex/input/tests/encounter/enc_in_progress_pat2_2025.json @@ -1,6 +1,6 @@ { "resourceType": "Encounter", - "id": "enc_in_progress_pat2_22025", + "id": "enc_in_progress_pat2_2025", "status": "in-progress", "subject": { "reference": "Patient/patient2" From 756f0551ec498651df2a39bdb60faed4a17b943e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Wed, 22 Oct 2025 11:41:56 -0400 Subject: [PATCH 16/48] Optimize the algorithms for both removeAll() and retainAll(). --- .../cr/measure/common/MeasureEvaluator.java | 4 +- .../fhir/cr/measure/common/PopulationDef.java | 87 +------------------ 2 files changed, 3 insertions(+), 88 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 826cbbfb90..390105116f 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -168,14 +168,12 @@ protected PopulationDef evaluatePopulationMembership( } // Add Resources from SubjectId - int i = 0; for (Object resource : evaluatePopulationCriteria( subjectType, matchingResult, evaluationResult, inclusionDef.getEvaluatedResources())) { // hashmap instead of set inclusionDef.addResource(subjectId, resource); - - i++; } + return inclusionDef; } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java index f28e14bea4..d8f17cb6a6 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java @@ -3,20 +3,13 @@ import jakarta.annotation.Nullable; import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class PopulationDef { - private static final Logger logger = LoggerFactory.getLogger(PopulationDef.class); - private final String id; private final String expression; private final ConceptDef code; @@ -72,36 +65,7 @@ public Set getSubjects() { } public void retainAllResources(Set resourcesToRetain) { - var resourcesToRetainForDebug = resourcesToRetain.stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - var resourcesInDefForDebug = getResources().stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - - final Iterator>> entryIterator = - getSubjectResources().entrySet().iterator(); - - while (entryIterator.hasNext()) { - final Set resourcesFromEntry = entryIterator.next().getValue(); - - resourcesFromEntry.retainAll(resourcesToRetain); - } - - var postRetainResourcesInDef = getResources().stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - logger.info( - "resourcesInDef:\n{},\nresourcesToRetain:\n{},\npostRetainResourcesInDef:\n{}", - resourcesInDefForDebug, - resourcesToRetainForDebug, - postRetainResourcesInDef); + getSubjectResources().forEach((key, value) -> value.retainAll(resourcesToRetain)); } public void retainAllSubjects(Set subjects) { @@ -109,54 +73,7 @@ public void retainAllSubjects(Set subjects) { } public void removeAllResources(Set resourcesToRemove) { - var resourcesToRemoveForDebug = resourcesToRemove.stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - var resourcesInDefForDebug = getResources().stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - // LUKETODO: I think this is too aggressive, even though the tests seem to pass - getSubjectResources().entrySet().removeIf(entry -> containsResourceForRemove(resourcesToRemove, entry)); - var postRemoveResourcesInDef = getResources().stream() - .map(res -> (res instanceof IBaseResource base) - ? base.getIdElement().getValueAsString() - : "?") - .collect(Collectors.toSet()); - logger.info( - "resourcesInDef:\n{},\nresourcesToRemove:\n{},\npostRemoveResourcesInDef:\n{}", - resourcesInDefForDebug, - resourcesToRemoveForDebug, - postRemoveResourcesInDef); - } - - private boolean containsResourceForRetain(Set resources, Entry> entry) { - final HashSetForFhirResources resourcesToTestForRetain = new HashSetForFhirResources<>(resources); - - final Set resourcesInEntry = entry.getValue(); - - for (Object resourceInEntry : resourcesInEntry) { - if (resourcesToTestForRetain.contains(resourceInEntry)) { - return true; - } - } - - return false; - } - - private boolean containsResourceForRemove(Set resources, Entry> entry) { - final Set resourcesInEntry = entry.getValue(); - - for (Object resource : resources) { - if (resourcesInEntry.contains(resource)) { - return true; - } - } - - return false; + getSubjectResources().forEach((key, value) -> value.removeAll(resourcesToRemove)); } public void removeAllSubjects(Set subjects) { From a198593f4d51bcb9f36fcbac05f973f40bf9ca2a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 13:14:46 -0400 Subject: [PATCH 17/48] Get rid of call to "legacy" continuous variable observation logic from DSTU3. --- .../measure/dstu3/Dstu3MeasureProcessor.java | 47 ------------------- .../cr/measure/r4/R4MeasureReportScorer.java | 1 + 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index 0cf8f4535d..a6336e60d7 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -1,8 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.dstu3; -import static org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationHandler.evaluateObservationCriteria; -import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; - import ca.uhn.fhir.repository.IRepository; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.time.ZonedDateTime; @@ -13,7 +10,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import org.cqframework.cql.cql2elm.CqlIncludeException; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; @@ -25,21 +21,15 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.execution.CqlEngine; -import org.opencds.cqf.cql.engine.execution.ExpressionResult; import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver; import org.opencds.cqf.cql.engine.runtime.Interval; import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.common.GroupDef; -import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; -import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; -import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; import org.opencds.cqf.fhir.utility.repository.FederatedRepository; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; @@ -138,49 +128,12 @@ protected MeasureReport evaluateMeasure( measureEvaluationOptions.getApplyScoringSetMembership(), new Dstu3PopulationBasisValidator()); } - // Populate populationDefs that require MeasureDef results - continuousVariableObservationLegacyLogic(measureDef, context); // Build Measure Report with Results return new Dstu3MeasureReportBuilder() .build(measure, measureDef, evalTypeToReportType(evalType), measurementPeriod, subjects); } - /** - * Measures with defined scoring type of 'continuous-variable' where a defined 'measure-observation' population is used to evaluate results of 'measure-population'. - * This method is a downstream calculation given it requires calculated results before it can be called. - * Results are then added to associated MeasureDef - * @param measureDef measure defined objects that are populated from criteria expression results - * @param context cql engine context used to evaluate results - */ - private static void continuousVariableObservationLegacyLogic(MeasureDef measureDef, CqlEngine context) { - // Continuous Variable? - for (GroupDef groupDef : measureDef.groups()) { - // Measure Observation defined? - if (groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE) - && groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION) != null) { - - PopulationDef measurePopulation = groupDef.getSingle(MEASUREPOPULATION); - PopulationDef measureObservation = groupDef.getSingle(MeasurePopulationType.MEASUREOBSERVATION); - - // Inject MeasurePopulation results into Measure Observation Function - Map> subjectResources = measurePopulation.getSubjectResources(); - - for (Map.Entry> entry : subjectResources.entrySet()) { - String subjectId = entry.getKey(); - Set resourcesForSubject = entry.getValue(); - - for (Object resource : resourcesForSubject) { - final ExpressionResult observationResult = evaluateObservationCriteria( - resource, measureObservation.expression(), groupDef.isBooleanBasis(), context); - measureObservation.addResource(observationResult.value()); - measureObservation.addResource(subjectId, observationResult.evaluatedResources()); - } - } - } - } - } - // Ideally this would be done in MeasureProcessorUtils, but it's too much work to change for now private MultiLibraryIdMeasureEngineDetails buildLibraryIdEngineDetails( Measure measure, Parameters parameters, CqlEngine context) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 0e916eb895..34d444b40b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -307,6 +307,7 @@ private static List collectQuantities(Set resources) { for (Object resource : resources) { if (resource instanceof Map map) { for (Object value : map.values()) { + // LUKETODO: replace this with Quantity or QuantityHolder if (value instanceof Observation obs && obs.hasValueQuantity()) { quantities.add(obs.getValueQuantity()); } From f1edfa8201a45bdc2f0b29f24d32bef4aea7699a Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 13:46:18 -0400 Subject: [PATCH 18/48] Use Quantities instead of Observations for continuous variable scoring. Rough implementation and code cleanup needed but all tests pass. --- ...ontinuousVariableObservationConverter.java | 7 +- .../ContinuousVariableObservationHandler.java | 14 ++- .../measure/common/MeasureProcessorUtils.java | 4 +- .../cr/measure/common/QuantityHolder.java | 55 +++++++++ ...ontinuousVariableObservationConverter.java | 19 +-- ...ontinuousVariableObservationConverter.java | 22 ++-- .../cr/measure/r4/R4MeasureReportScorer.java | 8 +- .../cr/measure/common/QuantityHolderTest.java | 109 ++++++++++++++++++ 8 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java create mode 100644 cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java index 5923c264d7..ddd31f1535 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java @@ -1,14 +1,15 @@ package org.opencds.cqf.fhir.cr.measure.common; -import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.ICompositeType; /** * Convert continuous variable scoring function-returned resources to Observations and Quantities in * a FHIR version specific way. */ @SuppressWarnings("squid:S1135") -public interface ContinuousVariableObservationConverter { +public interface ContinuousVariableObservationConverter { // TODO: LD: We need to come up with something other than an Observation to wrap FHIR Quantities - T wrapResultAsObservation(String id, String observationName, Object result); + // QuantityHolder wrapResultAsQuantityHolder(String id, Object result); + T wrapResultAsQuantityHolder(String id, Object result); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 88a3212e9a..564704893a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -14,23 +14,26 @@ import org.hl7.elm.r1.FunctionDef; import org.hl7.elm.r1.OperandDef; import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.ICompositeType; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; import org.opencds.cqf.cql.engine.execution.Libraries; import org.opencds.cqf.cql.engine.execution.Variable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Capture all logic for measure evaluation for continuous variable scoring. */ public class ContinuousVariableObservationHandler { + private static final Logger logger = LoggerFactory.getLogger(ContinuousVariableObservationHandler.class); private ContinuousVariableObservationHandler() { // static class with private constructor } - static List continuousVariableEvaluation( + static List continuousVariableEvaluation( CqlEngine context, List measureDefs, VersionedIdentifier libraryIdentifier, @@ -93,7 +96,7 @@ static List continuousVariableEvalua * For a given measure observation population, do an ad-hoc function evaluation and * accumulate the results that will be subsequently added to the CQL evaluation result. */ - private static EvaluationResult processMeasureObservation( + private static EvaluationResult processMeasureObservation( CqlEngine context, EvaluationResult evaluationResult, String subjectTypePart, @@ -144,8 +147,8 @@ private static EvaluationResult processMeasureObservat var observationId = expressionName + "-" + index; // wrap result in Observation resource to avoid duplicate results data loss // in set object - var observation = continuousVariableObservationConverter.wrapResultAsObservation( - observationId, observationId, observationResult.value()); + var observation = continuousVariableObservationConverter.wrapResultAsQuantityHolder( + observationId, observationResult.value()); // add function results to existing EvaluationResult under new expression // name // need a way to capture input parameter here too, otherwise we have no way @@ -307,6 +310,7 @@ private static void clearEvaluatedResources(CqlEngine context) { @Nonnull private static EvaluationResult buildEvaluationResult( String expressionName, Map functionResults, Set evaluatedResources) { + final EvaluationResult evaluationResultToReturn = new EvaluationResult(); evaluationResultToReturn.expressionResults.put( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index 62927df274..b093f0f838 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -16,7 +16,7 @@ import org.hl7.elm.r1.NamedTypeSpecifier; import org.hl7.elm.r1.ParameterDef; import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.ICompositeType; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib; @@ -267,7 +267,7 @@ public Date truncateDateTime(DateTime dateTime) { * specific * @return CQL results for Library defined in the Measure resource */ - public CompositeEvaluationResultsPerMeasure getEvaluationResults( + public CompositeEvaluationResultsPerMeasure getEvaluationResults( List subjectIds, ZonedDateTime zonedMeasurementPeriod, CqlEngine context, diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java new file mode 100644 index 0000000000..007c7824e1 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java @@ -0,0 +1,55 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import jakarta.annotation.Nullable; +import java.util.Objects; +import java.util.StringJoiner; +import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.r4.model.Quantity; + +public class QuantityHolder { + + private final String id; + + @Nullable + private final T quantity; + + public QuantityHolder(String id, @Nullable T quantity) { + this.id = id; + this.quantity = quantity; + } + + @Nullable + public T getQuantity() { + return this.quantity; + } + + public boolean hasValueQuantity() { + return this.quantity != null; + } + + public Quantity getValueQuantity() { + return null; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + QuantityHolder that = (QuantityHolder) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return new StringJoiner(", ", QuantityHolder.class.getSimpleName() + "[", "]") + .add("id='" + id + "'") + .add("quantity=" + quantity) + .toString(); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java index 194cf0ccc0..e99eebce43 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java @@ -1,7 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.dstu3; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationConverter; @@ -10,21 +8,16 @@ * enforced by an enum. */ @SuppressWarnings("squid:S6548") -public enum Dstu3ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { +public enum Dstu3ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { INSTANCE; @Override - public Observation wrapResultAsObservation(String id, String observationName, Object result) { + // public QuantityHolder wrapResultAsQuantityHolder(String id, Object result) { + // return new QuantityHolder<>(id, convertToQuantity(result)); + // } - Observation obs = new Observation(); - obs.setStatus(Observation.ObservationStatus.FINAL); - obs.setId(id); - CodeableConcept cc = new CodeableConcept(); - cc.setText(observationName); - obs.setValue(convertToQuantity(result)); - obs.setCode(cc); - - return obs; + public Quantity wrapResultAsQuantityHolder(String id, Object result) { + return convertToQuantity(result); } private static Quantity convertToQuantity(Object obj) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java index de2b09b929..9dfc55d06c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java @@ -1,7 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.r4; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationConverter; @@ -10,21 +8,17 @@ * enforced by an enum. */ @SuppressWarnings("squid:S6548") -public enum R4ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { +public enum R4ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { INSTANCE; - @Override - public Observation wrapResultAsObservation(String id, String observationName, Object result) { - - Observation obs = new Observation(); - obs.setStatus(Observation.ObservationStatus.FINAL); - obs.setId(id); - CodeableConcept cc = new CodeableConcept(); - cc.setText(observationName); - obs.setValue(convertToQuantity(result)); - obs.setCode(cc); + // @Override + // public QuantityHolder wrapResultAsQuantityHolder(String id, Object result) { + // return new QuantityHolder<>(id, convertToQuantity(result)); + // } - return obs; + @Override + public Quantity wrapResultAsQuantityHolder(String id, Object result) { + return convertToQuantity(result); } private static Quantity convertToQuantity(Object obj) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 34d444b40b..e6ac373b12 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -19,7 +19,6 @@ import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; -import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; @@ -308,8 +307,11 @@ private static List collectQuantities(Set resources) { if (resource instanceof Map map) { for (Object value : map.values()) { // LUKETODO: replace this with Quantity or QuantityHolder - if (value instanceof Observation obs && obs.hasValueQuantity()) { - quantities.add(obs.getValueQuantity()); + // if (value instanceof Observation obs && obs.hasValueQuantity()) { + // if (value instanceof QuantityHolder quantityHolder && + // quantityHolder.hasValueQuantity()) { + if (value instanceof Quantity quantity) { + quantities.add(quantity); } } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java new file mode 100644 index 0000000000..2acdcbeede --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java @@ -0,0 +1,109 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.hl7.fhir.r4.model.Quantity; +import org.junit.jupiter.api.Test; + +class QuantityHolderTest { + + @Test + void sameIdDifferentQuantityR4() { + var quantityQuantityHolder500 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(500)); + var quantityQuantityHolder600 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(600)); + + assertEquals(quantityQuantityHolder500, quantityQuantityHolder600); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder500); + holders.add(quantityQuantityHolder600); + + assertEquals(1, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder500)); + } + + @Test + void differentIdSameQuantityR4() { + var quantityQuantityHolder123 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(500)); + var quantityQuantityHolder456 = new QuantityHolder<>("456", new org.hl7.fhir.r4.model.Quantity(500)); + + assertNotEquals(quantityQuantityHolder123, quantityQuantityHolder456); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder123); + holders.add(quantityQuantityHolder456); + + assertEquals(2, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder123)); + assertTrue(holders.contains(quantityQuantityHolder456)); + } + + @Test + void sameIdSameQuantityR4() { + var quantityQuantityHolder1 = new QuantityHolder<>("123", new Quantity(500)); + var quantityQuantityHolder2 = new QuantityHolder<>("123", new Quantity(500)); + + assertEquals(quantityQuantityHolder1, quantityQuantityHolder2); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder1); + holders.add(quantityQuantityHolder2); + + assertEquals(1, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder1)); + } + + @Test + void sameIdDifferentQuantityDstu3() { + var quantityQuantityHolder500 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); + var quantityQuantityHolder600 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(600)); + + assertEquals(quantityQuantityHolder500, quantityQuantityHolder600); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder500); + holders.add(quantityQuantityHolder600); + + assertEquals(1, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder500)); + } + + @Test + void differentIdSameQuantityDstu3() { + var quantityQuantityHolder123 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); + var quantityQuantityHolder456 = new QuantityHolder<>("456", new org.hl7.fhir.dstu3.model.Quantity(500)); + + assertNotEquals(quantityQuantityHolder123, quantityQuantityHolder456); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder123); + holders.add(quantityQuantityHolder456); + + assertEquals(2, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder123)); + assertTrue(holders.contains(quantityQuantityHolder456)); + } + + @Test + void sameIdSameQuantityDstu3() { + var quantityQuantityHolder1 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); + var quantityQuantityHolder2 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); + + assertEquals(quantityQuantityHolder1, quantityQuantityHolder2); + + var holders = new HashSetForFhirResources<>(); + holders.add(quantityQuantityHolder1); + holders.add(quantityQuantityHolder2); + + assertEquals(1, holders.size()); + + assertTrue(holders.contains(quantityQuantityHolder1)); + } +} From c333d1ef8aeeab8d873bb5acc831d1643ec531f5 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 15:32:29 -0400 Subject: [PATCH 19/48] delete QuantityHolder.java and test. --- .../cr/measure/common/QuantityHolder.java | 55 --------- .../cr/measure/common/QuantityHolderTest.java | 109 ------------------ 2 files changed, 164 deletions(-) delete mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java delete mode 100644 cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java deleted file mode 100644 index 007c7824e1..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.common; - -import jakarta.annotation.Nullable; -import java.util.Objects; -import java.util.StringJoiner; -import org.hl7.fhir.instance.model.api.ICompositeType; -import org.hl7.fhir.r4.model.Quantity; - -public class QuantityHolder { - - private final String id; - - @Nullable - private final T quantity; - - public QuantityHolder(String id, @Nullable T quantity) { - this.id = id; - this.quantity = quantity; - } - - @Nullable - public T getQuantity() { - return this.quantity; - } - - public boolean hasValueQuantity() { - return this.quantity != null; - } - - public Quantity getValueQuantity() { - return null; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - QuantityHolder that = (QuantityHolder) o; - return Objects.equals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hashCode(id); - } - - @Override - public String toString() { - return new StringJoiner(", ", QuantityHolder.class.getSimpleName() + "[", "]") - .add("id='" + id + "'") - .add("quantity=" + quantity) - .toString(); - } -} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java deleted file mode 100644 index 2acdcbeede..0000000000 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/QuantityHolderTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.common; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.hl7.fhir.r4.model.Quantity; -import org.junit.jupiter.api.Test; - -class QuantityHolderTest { - - @Test - void sameIdDifferentQuantityR4() { - var quantityQuantityHolder500 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(500)); - var quantityQuantityHolder600 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(600)); - - assertEquals(quantityQuantityHolder500, quantityQuantityHolder600); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder500); - holders.add(quantityQuantityHolder600); - - assertEquals(1, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder500)); - } - - @Test - void differentIdSameQuantityR4() { - var quantityQuantityHolder123 = new QuantityHolder<>("123", new org.hl7.fhir.r4.model.Quantity(500)); - var quantityQuantityHolder456 = new QuantityHolder<>("456", new org.hl7.fhir.r4.model.Quantity(500)); - - assertNotEquals(quantityQuantityHolder123, quantityQuantityHolder456); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder123); - holders.add(quantityQuantityHolder456); - - assertEquals(2, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder123)); - assertTrue(holders.contains(quantityQuantityHolder456)); - } - - @Test - void sameIdSameQuantityR4() { - var quantityQuantityHolder1 = new QuantityHolder<>("123", new Quantity(500)); - var quantityQuantityHolder2 = new QuantityHolder<>("123", new Quantity(500)); - - assertEquals(quantityQuantityHolder1, quantityQuantityHolder2); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder1); - holders.add(quantityQuantityHolder2); - - assertEquals(1, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder1)); - } - - @Test - void sameIdDifferentQuantityDstu3() { - var quantityQuantityHolder500 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); - var quantityQuantityHolder600 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(600)); - - assertEquals(quantityQuantityHolder500, quantityQuantityHolder600); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder500); - holders.add(quantityQuantityHolder600); - - assertEquals(1, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder500)); - } - - @Test - void differentIdSameQuantityDstu3() { - var quantityQuantityHolder123 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); - var quantityQuantityHolder456 = new QuantityHolder<>("456", new org.hl7.fhir.dstu3.model.Quantity(500)); - - assertNotEquals(quantityQuantityHolder123, quantityQuantityHolder456); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder123); - holders.add(quantityQuantityHolder456); - - assertEquals(2, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder123)); - assertTrue(holders.contains(quantityQuantityHolder456)); - } - - @Test - void sameIdSameQuantityDstu3() { - var quantityQuantityHolder1 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); - var quantityQuantityHolder2 = new QuantityHolder<>("123", new org.hl7.fhir.dstu3.model.Quantity(500)); - - assertEquals(quantityQuantityHolder1, quantityQuantityHolder2); - - var holders = new HashSetForFhirResources<>(); - holders.add(quantityQuantityHolder1); - holders.add(quantityQuantityHolder2); - - assertEquals(1, holders.size()); - - assertTrue(holders.contains(quantityQuantityHolder1)); - } -} From 2d6aa368fab0d9d2ce082694119254a0981caa67 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 15:33:23 -0400 Subject: [PATCH 20/48] TODOs. --- .../cqf/fhir/cr/measure/common/MeasureEvaluator.java | 1 - .../cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java | 9 ++------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 390105116f..68def47559 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -283,7 +283,6 @@ protected void evaluateProportion( } if (reportType.equals(MeasureReportType.INDIVIDUAL) && dateOfCompliance != null) { var doc = evaluateDateOfCompliance(dateOfCompliance, evaluationResult); - // LUKETODO: similarly, we'd have to add the subject as well here dateOfCompliance.addResource(subjectId, doc); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index e6ac373b12..c67cce6876 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -306,10 +306,6 @@ private static List collectQuantities(Set resources) { for (Object resource : resources) { if (resource instanceof Map map) { for (Object value : map.values()) { - // LUKETODO: replace this with Quantity or QuantityHolder - // if (value instanceof Observation obs && obs.hasValueQuantity()) { - // if (value instanceof QuantityHolder quantityHolder && - // quantityHolder.hasValueQuantity()) { if (value instanceof Quantity quantity) { quantities.add(quantity); } @@ -395,8 +391,7 @@ private Quantity getStratumScoreOrNull( so it's basically a hack to go from StratifierGroupComponent stratum value -> subject -> populationDef.subjectResources.get(subject) to get Set of resources on which to do measure scoring */ - - // TODO: LD: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder + // LUKETODO: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder private Set getResultsForStratum( PopulationDef measureObservationPopulationDef, StratifierDef stratifierDef, @@ -416,7 +411,7 @@ private Set getResultsForStratum( .collect(Collectors.toUnmodifiableSet()); } - // TODO: LD: we may need to match more types of stratum here: The below logic deals with + // LUKETODO:: we may need to match more types of stratum here: The below logic deals with // currently anticipated use cases private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { if (rawValueFromStratifier == null || stratumValueAsString == null) { From 4dd9026e29ff1baf9b755116bd3d0fdb85e881da Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 16:11:00 -0400 Subject: [PATCH 21/48] Push up logic for Set intersection comparison for stratifier populations. --- .../cr/measure/r4/R4StratifierBuilder.java | 95 ++++++++++++++++--- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index d173270bc4..dac924172b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.r4; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; @@ -11,6 +12,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.stream.Collector; @@ -19,6 +21,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.Expression; import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.Measure.MeasureGroupPopulationComponent; @@ -40,6 +43,8 @@ import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureReportBuilder.BuilderContext; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureReportBuilder.ValueWrapper; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Convenience class with functionality split out from {@link R4MeasureReportBuilder} to @@ -47,6 +52,7 @@ */ @SuppressWarnings("squid:S1135") class R4StratifierBuilder { + private static final Logger logger = LoggerFactory.getLogger(R4StratifierBuilder.class); static void buildStratifier( BuilderContext bc, @@ -302,9 +308,23 @@ private static void buildStratumPopulation( .equals(population.getCode().getCodingFirstRep().getCode())) .findFirst() .orElse(null); - assert populationDef != null; + + // LUKETODO: get rid of this assert and throw an actual Exception in this case + if (populationDef == null) { + throw new InvalidRequestException("Invalid population definition"); + } + + var popSubjectIds = populationDef.getSubjects().stream() + .map(R4ResourceIdUtils::addPatientQualifier) + .collect(Collectors.toUnmodifiableSet()); + + // TODO: LD: introduce a new StratumDef object to hold the results of the intersection, to + // be ultimately passed down to the measure scorer, instead of retaining only the count + // intersect population subjects to stratifier.value subjects + var subjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); + if (groupDef.isBooleanBasis()) { - buildBooleanBasisStratumPopulation(bc, sgpc, subjectIds, populationDef); + buildBooleanBasisStratumPopulation(bc, sgpc, subjectIds, populationDef, subjectIdsCommonToPopulation); } else { buildResourceBasisStratumPopulation(bc, stratifierDef, sgpc, subjectIds, populationDef, groupDef); } @@ -314,7 +334,9 @@ private static void buildBooleanBasisStratumPopulation( BuilderContext bc, StratifierGroupPopulationComponent sgpc, List subjectIds, - PopulationDef populationDef) { + PopulationDef populationDef, + SetView subjectIdsCommonToPopulation) { + var popSubjectIds = populationDef.getSubjects().stream() .map(R4ResourceIdUtils::addPatientQualifier) .toList(); @@ -323,24 +345,73 @@ private static void buildBooleanBasisStratumPopulation( return; } - // TODO: LD: introduce a new StratumDef object to hold the results of the intersection, to - // be ultimately passed down to the measure scorer, instead of retaining only the count - - // intersect population subjects to stratifier.value subjects - Set intersection = new HashSet<>(subjectIds); - intersection.retainAll(popSubjectIds); - sgpc.setCount(intersection.size()); + sgpc.setCount(subjectIdsCommonToPopulation.size()); // subject-list ListResource to match intersection of results - if (!intersection.isEmpty() + if (!subjectIdsCommonToPopulation.isEmpty() && bc.report().getType() == org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUBJECTLIST) { ListResource popSubjectList = - R4StratifierBuilder.createIdList(UUID.randomUUID().toString(), intersection); + R4StratifierBuilder.createIdList(UUID.randomUUID().toString(), subjectIdsCommonToPopulation); bc.addContained(popSubjectList); sgpc.setSubjectResults(new Reference("#" + popSubjectList.getId())); } } + /* + the existing algo takes the measure-observation population from the group definition and goes through all resources to get the quantities + MeasurePopulationType.MEASUREOBSERVATION + + but we don't want that: we want to filter only resources that belong to the patients captured by each stratum + so we want to do some sort of wizardry that involves getting the stratum values, and using those to retrieve the associated resources + + so it's basically a hack to go from StratifierGroupComponent stratum value -> subject -> populationDef.subjectResources.get(subject) + to get Set of resources on which to do measure scoring + */ + // LUKETODO: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder + private Set getResultsForStratum( + PopulationDef measureObservationPopulationDef, + StratifierDef stratifierDef, + StratifierGroupComponent stratum) { + + final String stratumValue = stratum.getValue().getText(); + + final Set subjectsWithStratumValue = stratifierDef.getResults().entrySet().stream() + .filter(entry -> doesStratumMatch(stratumValue, entry.getValue().rawValue())) + .map(Entry::getKey) + .collect(Collectors.toUnmodifiableSet()); + + return measureObservationPopulationDef.getSubjectResources().entrySet().stream() + .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) + .map(Entry::getValue) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableSet()); + } + + // LUKETODO: we may be able to do away with this + // LUKETODO:: we may need to match more types of stratum here: The below logic deals with + // currently anticipated use cases + private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { + if (rawValueFromStratifier == null || stratumValueAsString == null) { + return false; + } + + if (rawValueFromStratifier instanceof Integer rawValueFromStratifierAsInt) { + final int stratumValueAsInt = Integer.parseInt(stratumValueAsString); + + return stratumValueAsInt == rawValueFromStratifierAsInt; + } + + if (rawValueFromStratifier instanceof Enumeration rawValueFromStratifierAsEnumeration) { + return stratumValueAsString.equals(rawValueFromStratifierAsEnumeration.asStringValue()); + } + + if (rawValueFromStratifier instanceof String rawValueFromStratifierAsString) { + return stratumValueAsString.equals(rawValueFromStratifierAsString); + } + + return false; + } + private static void buildResourceBasisStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, From 3fd1f1d3bed5e981bbf4ad99e6bf872c13fa8099 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 23 Oct 2025 18:07:12 -0400 Subject: [PATCH 22/48] First stab at new algorithm to collect StratumDef and StratumPopulationDef and match them with stratum and stratum populations in the build in order to score continuous variable observations. --- .../fhir/cr/measure/common/StratifierDef.java | 14 ++- .../fhir/cr/measure/common/StratumDef.java | 26 ++++++ .../measure/common/StratumPopulationDef.java | 25 ++++++ .../cr/measure/r4/R4MeasureDefBuilder.java | 5 ++ .../cr/measure/r4/R4MeasureReportBuilder.java | 1 + .../cr/measure/r4/R4MeasureReportScorer.java | 85 ++++++++++++++++++- .../cr/measure/r4/R4StratifierBuilder.java | 25 +++++- .../cr/measure/r4/MeasureDefBuilderTest.java | 1 + .../r4/R4PopulationBasisValidatorTest.java | 2 +- 9 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java index 82f81c61c6..8e8b9e3675 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java @@ -20,12 +20,13 @@ public class StratifierDef { private final MeasureStratifierType stratifierType; private final List components; + private final List stratum; @Nullable private Map results; public StratifierDef(String id, ConceptDef code, String expression, MeasureStratifierType stratifierType) { - this(id, code, expression, stratifierType, Collections.emptyList()); + this(id, code, expression, stratifierType, Collections.emptyList(), Collections.emptyList()); } public StratifierDef( @@ -33,11 +34,13 @@ public StratifierDef( ConceptDef code, String expression, MeasureStratifierType stratifierType, + List stratum, List components) { this.id = id; this.code = code; this.expression = expression; this.stratifierType = stratifierType; + this.stratum = stratum; this.components = components; } @@ -53,6 +56,15 @@ public String id() { return this.id; } + public List getStratum() { + return stratum; + } + + // LUKETODO: try out this pattern + public void addStratumDef(StratumDef stratumDef) { + stratum.add(stratumDef); + } + public List components() { return this.components; } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java new file mode 100644 index 0000000000..cafedb8a10 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java @@ -0,0 +1,26 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import java.util.List; + +// LUKETODO: javadoc +public class StratumDef { + private final String text; + private final List stratumPopulations; + + public StratumDef(String text, List stratumPopulations) { + this.text = text; + this.stratumPopulations = stratumPopulations; + } + + public String getText() { + return text; + } + + public List getStratumPopulations() { + return stratumPopulations; + } + + public void addStratumPopulation(StratumPopulationDef stratumPopulationDef) { + stratumPopulations.add(stratumPopulationDef); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java new file mode 100644 index 0000000000..bfc07633b3 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -0,0 +1,25 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import java.util.Set; + +// LUKETODO: javadoc +// LUKETODO: more fields +// LUKETODO: this merges the concept of a Stratum and a Stratum population so we'll have to decouple these +public class StratumPopulationDef { + + private final String id; + private final Set subjects; + + public StratumPopulationDef(String id, Set subjects) { + this.id = id; + this.subjects = subjects; + } + + public String getId() { + return id; + } + + public Set getSubjects() { + return subjects; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index 630d98ccdb..99d70dff44 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -50,6 +50,7 @@ import org.opencds.cqf.fhir.cr.measure.common.StratifierComponentDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierUtils; +import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; public class R4MeasureDefBuilder implements MeasureDefBuilder { @@ -245,6 +246,9 @@ private boolean isMeasureObservation(MeasureGroupPopulationComponent pop) { private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { checkId(mgsc); + // LUKETODO: how do I fill this? + var stratum = new ArrayList(); + // Components var components = new ArrayList(); for (MeasureGroupStratifierComponentComponent scc : mgsc.getComponent()) { @@ -268,6 +272,7 @@ private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { conceptToConceptDef(mgsc.getCode()), mgsc.getCriteria().getExpression(), getStratifierType(mgsc), + new ArrayList<>(), components); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 4dba183a2f..2f7fb195fa 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -484,6 +484,7 @@ public int countObservations(PopulationDef populationDef) { .sum(); } + // LUKETODO: what's this for? protected void buildMeasureObservations(BuilderContext bc, String observationName, Set resources) { for (int i = 0; i < resources.size(); i++) { // TODO: Do something with the resource... diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index c67cce6876..904d75a1e2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -2,9 +2,11 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.apicatalog.jsonld.StringUtils; import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -12,6 +14,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; @@ -28,6 +31,8 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; +import org.opencds.cqf.fhir.cr.measure.common.StratumDef; +import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -333,7 +338,22 @@ protected void scoreStratifier( throw new InternalErrorException("Stratifier component " + sgc.getId() + " does not exist."); } - scoreStratum(measureUrl, groupDef, optStratifierDef.get(), measureScoring, sgc); + final StratifierDef stratifierDef = optStratifierDef.get(); + + final StratumDef stratumDef = stratifierDef.getStratum().stream() + .filter(stratumDefInner -> StringUtils.isNotBlank(stratumDefInner.getText())) + // LUKETODO: consider refining this logic: + .filter(stratumDefInner -> + stratumDefInner.getText().equals(sgc.getValue().getText())) + .findFirst() + .orElse(null); + + // LUKETODO: should we always expect these to match up? + if (stratumDef == null) { + logger.warn("1234: stratumDef is null"); + } + + scoreStratum(measureUrl, groupDef, optStratifierDef.get(), stratumDef, measureScoring, sgc); } } @@ -341,9 +361,11 @@ protected void scoreStratum( String measureUrl, GroupDef groupDef, StratifierDef stratifierDef, + StratumDef stratumDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { - final Quantity quantity = getStratumScoreOrNull(measureUrl, groupDef, stratifierDef, measureScoring, stratum); + final Quantity quantity = + getStratumScoreOrNull(measureUrl, groupDef, stratifierDef, stratumDef, measureScoring, stratum); if (quantity != null) { stratum.setMeasureScore(quantity); @@ -355,6 +377,7 @@ private Quantity getStratumScoreOrNull( String measureUrl, GroupDef groupDef, StratifierDef stratifierDef, + StratumDef stratumDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { @@ -370,10 +393,22 @@ private Quantity getStratumScoreOrNull( return null; } case CONTINUOUSVARIABLE -> { + logger.info("1234: calculateContinuousVariableAggregateQuantity()"); + + final StratumPopulationDef stratumPopulationDef; + if (stratumDef != null) { + stratumPopulationDef = stratumDef.getStratumPopulations().stream() + .filter(x -> x.getId().startsWith(MeasurePopulationType.MEASUREOBSERVATION.toCode())) + .findFirst() + .orElse(null); + } else { + stratumPopulationDef = null; + } return calculateContinuousVariableAggregateQuantity( measureUrl, groupDef, - populationDef -> getResultsForStratum(populationDef, stratifierDef, stratum)); + populationDef -> + getResultsForStratum(populationDef, stratifierDef, stratumPopulationDef, stratum)); } default -> { return null; @@ -395,6 +430,7 @@ private Quantity getStratumScoreOrNull( private Set getResultsForStratum( PopulationDef measureObservationPopulationDef, StratifierDef stratifierDef, + StratumPopulationDef stratumPopulationDef, StratifierGroupComponent stratum) { final String stratumValue = stratum.getValue().getText(); @@ -404,11 +440,52 @@ private Set getResultsForStratum( .map(Entry::getKey) .collect(Collectors.toUnmodifiableSet()); - return measureObservationPopulationDef.getSubjectResources().entrySet().stream() + final Set result = measureObservationPopulationDef.getSubjectResources().entrySet().stream() .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) .map(Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toUnmodifiableSet()); + + final Set resultNew = measureObservationPopulationDef.getSubjectResources().entrySet().stream() + // LUKETODO: split this the proper way using hapi-fhir classe + .filter(entry -> stratumPopulationDef.getSubjects().stream() + .map(subject -> subject.split("Patient/")[1]) + .collect(Collectors.toUnmodifiableSet()) + .contains(entry.getKey())) + .map(Entry::getValue) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableSet()); + + logger.info( + "1234: measureObservationPopulationDef: {}, subjectsWithStratumValue: {}, result: {}", + measureObservationPopulationDef.id(), + subjectsWithStratumValue, + print(result)); + + return resultNew; + } + + private Set print(Set results) { + + Set result = new HashSet<>(); + for (Object o : results) { + if (o instanceof Map map) { + final Set set = map.entrySet(); + + for (Object item : set) { + if (item instanceof Entry mapEntry) { + final Object key = mapEntry.getKey(); + final Object value = mapEntry.getValue(); + + if (key instanceof IBaseResource resource && value instanceof Quantity quantity) { + result.add(resource.getIdElement().getValueAsString() + ":" + quantity.getValue()); + } + } + } + } + } + + return result; } // LUKETODO:: we may need to match more types of stratum here: The below logic deals with diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index dac924172b..b0d999d2a7 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -39,6 +39,8 @@ import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierComponentDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; +import org.opencds.cqf.fhir.cr.measure.common.StratumDef; +import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureReportBuilder.BuilderContext; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureReportBuilder.ValueWrapper; @@ -260,6 +262,9 @@ private static void buildStratum( } } + var stratumDef = new StratumDef(stratum.getValue().getText(), new ArrayList<>()); + stratifierDef.addStratumDef(stratumDef); + // add stratum populations for stratifier // Group.populations // initial-population: subject1, subject 2 @@ -273,7 +278,7 @@ private static void buildStratum( // ** ** initial-population: subject2 for (MeasureGroupPopulationComponent mgpc : populations) { var stratumPopulation = stratum.addPopulation(); - buildStratumPopulation(bc, stratifierDef, stratumPopulation, subjectIds, mgpc, groupDef); + buildStratumPopulation(bc, stratifierDef, stratumDef, stratumPopulation, subjectIds, mgpc, groupDef); } } @@ -288,10 +293,14 @@ private record ValueDef(ValueWrapper value, StratifierComponentDef def) {} private static void buildStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, + StratumDef stratumDef, StratifierGroupPopulationComponent sgpc, List subjectIds, MeasureGroupPopulationComponent population, GroupDef groupDef) { + + logger.info("1234: buildStratumPopulation()"); + sgpc.setCode(population.getCode()); sgpc.setId(population.getId()); @@ -309,8 +318,8 @@ private static void buildStratumPopulation( .findFirst() .orElse(null); - // LUKETODO: get rid of this assert and throw an actual Exception in this case if (populationDef == null) { + // LUKETODO: add more details to Exception throw new InvalidRequestException("Invalid population definition"); } @@ -323,8 +332,17 @@ private static void buildStratumPopulation( // intersect population subjects to stratifier.value subjects var subjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); + logger.info( + "1234: populationDef: {}, subjectIdsCommonToPopulation = {}", + populationDef.id(), + subjectIdsCommonToPopulation); + + var stratumPopulationDef = new StratumPopulationDef(populationDef.id(), subjectIdsCommonToPopulation); + + stratumDef.addStratumPopulation(stratumPopulationDef); + if (groupDef.isBooleanBasis()) { - buildBooleanBasisStratumPopulation(bc, sgpc, subjectIds, populationDef, subjectIdsCommonToPopulation); + buildBooleanBasisStratumPopulation(bc, sgpc, populationDef, subjectIdsCommonToPopulation); } else { buildResourceBasisStratumPopulation(bc, stratifierDef, sgpc, subjectIds, populationDef, groupDef); } @@ -333,7 +351,6 @@ private static void buildStratumPopulation( private static void buildBooleanBasisStratumPopulation( BuilderContext bc, StratifierGroupPopulationComponent sgpc, - List subjectIds, PopulationDef populationDef, SetView subjectIdsCommonToPopulation) { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java index e25c08fe65..d839eb2dd6 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java @@ -588,6 +588,7 @@ private static StratifierDef buildOutputStratifierDef(int componentCount, String new ConceptDef(List.of(new CodeDef("system", "code")), expression), expression, MeasureStratifierType.VALUE, + List.of(), IntStream.range(0, componentCount) .mapToObj(num -> buildOutputStratifierComponentDef(expression + num)) .toList()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java index 5466fefeb8..80c8d58a5f 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java @@ -456,7 +456,7 @@ private static List buildStratifierDefs(String... populations) { @Nonnull private static StratifierDef buildStratifierDef(String expression) { - return new StratifierDef(null, null, expression, MeasureStratifierType.VALUE, List.of()); + return new StratifierDef(null, null, expression, MeasureStratifierType.VALUE, List.of(), List.of()); } @Nonnull From eed38b01c869d85049cd813f7dd277a59bf405f4 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 09:21:46 -0400 Subject: [PATCH 23/48] Start cleaning up dead code. --- .../fhir/cr/measure/common/StratifierDef.java | 30 ++----- .../cr/measure/r4/R4MeasureDefBuilder.java | 3 - .../cr/measure/r4/R4MeasureReportScorer.java | 79 +------------------ .../cr/measure/r4/R4StratifierBuilder.java | 28 +++++-- 4 files changed, 29 insertions(+), 111 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java index 8e8b9e3675..f95ef54010 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java @@ -65,6 +65,11 @@ public void addStratumDef(StratumDef stratumDef) { stratum.add(stratumDef); } + // LUKETODO: try out this pattern + public void addAllStratum(List stratumDefs) { + stratum.addAll(stratumDefs); + } + public List components() { return this.components; } @@ -105,29 +110,4 @@ private Set toSet(Object value) { return Set.of(value); } } - - @Nullable - public Class getResultType() { - if (this.results == null || this.results.isEmpty()) { - return null; - } - - var resultClasses = results.values().stream() - .map(CriteriaResult::rawValue) - .map(StratifierUtils::extractClassesFromSingleOrListResult) - .flatMap(Collection::stream) - .collect(Collectors.toUnmodifiableSet()); - - if (resultClasses.size() == 1) { - return resultClasses.iterator().next(); - } - - if (resultClasses.isEmpty()) { - return null; - } - - throw new InvalidRequestException( - "There should be only one result type for this StratifierDef but there was: %s" - .formatted(resultClasses)); - } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index 99d70dff44..c543581274 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -246,9 +246,6 @@ private boolean isMeasureObservation(MeasureGroupPopulationComponent pop) { private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { checkId(mgsc); - // LUKETODO: how do I fill this? - var stratum = new ArrayList(); - // Components var components = new ArrayList(); for (MeasureGroupStratifierComponentComponent scc : mgsc.getComponent()) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 904d75a1e2..f59c326d9d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -365,7 +365,7 @@ protected void scoreStratum( MeasureScoring measureScoring, StratifierGroupComponent stratum) { final Quantity quantity = - getStratumScoreOrNull(measureUrl, groupDef, stratifierDef, stratumDef, measureScoring, stratum); + getStratumScoreOrNull(measureUrl, groupDef, stratumDef, measureScoring, stratum); if (quantity != null) { stratum.setMeasureScore(quantity); @@ -376,7 +376,6 @@ protected void scoreStratum( private Quantity getStratumScoreOrNull( String measureUrl, GroupDef groupDef, - StratifierDef stratifierDef, StratumDef stratumDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { @@ -408,7 +407,7 @@ private Quantity getStratumScoreOrNull( measureUrl, groupDef, populationDef -> - getResultsForStratum(populationDef, stratifierDef, stratumPopulationDef, stratum)); + getResultsForStratum(populationDef, stratumPopulationDef)); } default -> { return null; @@ -429,24 +428,9 @@ private Quantity getStratumScoreOrNull( // LUKETODO: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder private Set getResultsForStratum( PopulationDef measureObservationPopulationDef, - StratifierDef stratifierDef, - StratumPopulationDef stratumPopulationDef, - StratifierGroupComponent stratum) { + StratumPopulationDef stratumPopulationDef) { - final String stratumValue = stratum.getValue().getText(); - - final Set subjectsWithStratumValue = stratifierDef.getResults().entrySet().stream() - .filter(entry -> doesStratumMatch(stratumValue, entry.getValue().rawValue())) - .map(Entry::getKey) - .collect(Collectors.toUnmodifiableSet()); - - final Set result = measureObservationPopulationDef.getSubjectResources().entrySet().stream() - .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) - .map(Entry::getValue) - .flatMap(Collection::stream) - .collect(Collectors.toUnmodifiableSet()); - - final Set resultNew = measureObservationPopulationDef.getSubjectResources().entrySet().stream() + return measureObservationPopulationDef.getSubjectResources().entrySet().stream() // LUKETODO: split this the proper way using hapi-fhir classe .filter(entry -> stratumPopulationDef.getSubjects().stream() .map(subject -> subject.split("Patient/")[1]) @@ -455,61 +439,6 @@ private Set getResultsForStratum( .map(Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toUnmodifiableSet()); - - logger.info( - "1234: measureObservationPopulationDef: {}, subjectsWithStratumValue: {}, result: {}", - measureObservationPopulationDef.id(), - subjectsWithStratumValue, - print(result)); - - return resultNew; - } - - private Set print(Set results) { - - Set result = new HashSet<>(); - for (Object o : results) { - if (o instanceof Map map) { - final Set set = map.entrySet(); - - for (Object item : set) { - if (item instanceof Entry mapEntry) { - final Object key = mapEntry.getKey(); - final Object value = mapEntry.getValue(); - - if (key instanceof IBaseResource resource && value instanceof Quantity quantity) { - result.add(resource.getIdElement().getValueAsString() + ":" + quantity.getValue()); - } - } - } - } - } - - return result; - } - - // LUKETODO:: we may need to match more types of stratum here: The below logic deals with - // currently anticipated use cases - private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { - if (rawValueFromStratifier == null || stratumValueAsString == null) { - return false; - } - - if (rawValueFromStratifier instanceof Integer rawValueFromStratifierAsInt) { - final int stratumValueAsInt = Integer.parseInt(stratumValueAsString); - - return stratumValueAsInt == rawValueFromStratifierAsInt; - } - - if (rawValueFromStratifier instanceof Enumeration rawValueFromStratifierAsEnumeration) { - return stratumValueAsString.equals(rawValueFromStratifierAsEnumeration.asStringValue()); - } - - if (rawValueFromStratifier instanceof String rawValueFromStratifierAsString) { - return stratumValueAsString.equals(rawValueFromStratifierAsString); - } - - return false; } private int getCountFromGroupPopulation( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index b0d999d2a7..2d02627370 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -57,22 +57,34 @@ class R4StratifierBuilder { private static final Logger logger = LoggerFactory.getLogger(R4StratifierBuilder.class); static void buildStratifier( - BuilderContext bc, - MeasureGroupStratifierComponent measureStratifier, - MeasureReportGroupStratifierComponent reportStratifier, - StratifierDef stratifierDef, - List populations, - GroupDef groupDef) { + BuilderContext bc, + MeasureGroupStratifierComponent measureStratifier, + MeasureReportGroupStratifierComponent reportStratifier, + StratifierDef stratifierDef, + List populations, + GroupDef groupDef) { + // the top level stratifier 'id' and 'code' reportStratifier.setCode(getCodeForReportStratifier(stratifierDef, measureStratifier)); reportStratifier.setId(measureStratifier.getId()); // if description is defined, add to MeasureReport if (measureStratifier.hasDescription()) { reportStratifier.addExtension( - MeasureConstants.EXT_POPULATION_DESCRIPTION_URL, - new StringType(measureStratifier.getDescription())); + MeasureConstants.EXT_POPULATION_DESCRIPTION_URL, + new StringType(measureStratifier.getDescription())); } + buildStratifier2(bc, measureStratifier, reportStratifier, stratifierDef, populations, groupDef); + } + + private static void buildStratifier2( + BuilderContext bc, + MeasureGroupStratifierComponent measureStratifier, + MeasureReportGroupStratifierComponent reportStratifier, + StratifierDef stratifierDef, + List populations, + GroupDef groupDef) { + if (!stratifierDef.components().isEmpty()) { Table subjectResultTable = HashBasedTable.create(); From 42dab54ae1be5d7e21c29f81d380e6daecbef741 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 09:41:03 -0400 Subject: [PATCH 24/48] Start moving up adding stratum population defs and stratum defs higher up the call chain. --- .../fhir/cr/measure/common/StratifierDef.java | 1 - .../cr/measure/r4/R4MeasureDefBuilder.java | 1 - .../cr/measure/r4/R4MeasureReportScorer.java | 40 ++++------ .../cr/measure/r4/R4StratifierBuilder.java | 75 +++++++++++++------ 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java index f95ef54010..591672ae75 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java @@ -1,6 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.common; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import jakarta.annotation.Nullable; import java.util.Collection; import java.util.Collections; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index c543581274..6916936d54 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -50,7 +50,6 @@ import org.opencds.cqf.fhir.cr.measure.common.StratifierComponentDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierUtils; -import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; public class R4MeasureDefBuilder implements MeasureDefBuilder { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index f59c326d9d..72eaeff880 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -6,7 +6,6 @@ import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -14,8 +13,6 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; @@ -364,8 +361,7 @@ protected void scoreStratum( StratumDef stratumDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { - final Quantity quantity = - getStratumScoreOrNull(measureUrl, groupDef, stratumDef, measureScoring, stratum); + final Quantity quantity = getStratumScoreOrNull(measureUrl, groupDef, stratumDef, measureScoring, stratum); if (quantity != null) { stratum.setMeasureScore(quantity); @@ -406,8 +402,7 @@ private Quantity getStratumScoreOrNull( return calculateContinuousVariableAggregateQuantity( measureUrl, groupDef, - populationDef -> - getResultsForStratum(populationDef, stratumPopulationDef)); + populationDef -> getResultsForStratum(populationDef, stratumPopulationDef)); } default -> { return null; @@ -415,32 +410,29 @@ private Quantity getStratumScoreOrNull( } } - /* - the existing algo takes the measure-observation population from the group definition and goes through all resources to get the quantities - MeasurePopulationType.MEASUREOBSERVATION - - but we don't want that: we want to filter only resources that belong to the patients captured by each stratum - so we want to do some sort of wizardry that involves getting the stratum values, and using those to retrieve the associated resources - - so it's basically a hack to go from StratifierGroupComponent stratum value -> subject -> populationDef.subjectResources.get(subject) - to get Set of resources on which to do measure scoring - */ - // LUKETODO: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder + // LUKETODO: new javadoc explaining what this does private Set getResultsForStratum( - PopulationDef measureObservationPopulationDef, - StratumPopulationDef stratumPopulationDef) { + PopulationDef measureObservationPopulationDef, StratumPopulationDef stratumPopulationDef) { return measureObservationPopulationDef.getSubjectResources().entrySet().stream() // LUKETODO: split this the proper way using hapi-fhir classe - .filter(entry -> stratumPopulationDef.getSubjects().stream() - .map(subject -> subject.split("Patient/")[1]) - .collect(Collectors.toUnmodifiableSet()) - .contains(entry.getKey())) + .filter(entry -> doesStratumPopDefMatchGroupPopDef(stratumPopulationDef, entry)) .map(Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toUnmodifiableSet()); } + private boolean doesStratumPopDefMatchGroupPopDef( + StratumPopulationDef stratumPopulationDef, + Entry> entry) { + + return stratumPopulationDef.getSubjects().stream() + // LUKETODO: push this up the the stratumPopulationDef building code? + .map(subject -> subject.split("Patient/")[1]) + .collect(Collectors.toUnmodifiableSet()) + .contains(entry.getKey()); + } + private int getCountFromGroupPopulation( List populations, String populationName) { return populations.stream() diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index 2d02627370..e791962038 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -57,12 +57,12 @@ class R4StratifierBuilder { private static final Logger logger = LoggerFactory.getLogger(R4StratifierBuilder.class); static void buildStratifier( - BuilderContext bc, - MeasureGroupStratifierComponent measureStratifier, - MeasureReportGroupStratifierComponent reportStratifier, - StratifierDef stratifierDef, - List populations, - GroupDef groupDef) { + BuilderContext bc, + MeasureGroupStratifierComponent measureStratifier, + MeasureReportGroupStratifierComponent reportStratifier, + StratifierDef stratifierDef, + List populations, + GroupDef groupDef) { // the top level stratifier 'id' and 'code' reportStratifier.setCode(getCodeForReportStratifier(stratifierDef, measureStratifier)); @@ -70,17 +70,23 @@ static void buildStratifier( // if description is defined, add to MeasureReport if (measureStratifier.hasDescription()) { reportStratifier.addExtension( - MeasureConstants.EXT_POPULATION_DESCRIPTION_URL, - new StringType(measureStratifier.getDescription())); + MeasureConstants.EXT_POPULATION_DESCRIPTION_URL, + new StringType(measureStratifier.getDescription())); } - buildStratifier2(bc, measureStratifier, reportStratifier, stratifierDef, populations, groupDef); + var stratumDefs = buildMultipleStratum( + bc, + reportStratifier, + stratifierDef, + populations, + groupDef); + +// stratifierDef.addAllStratum(stratumDefs); } - private static void buildStratifier2( + private static List buildMultipleStratum( BuilderContext bc, - MeasureGroupStratifierComponent measureStratifier, - MeasureReportGroupStratifierComponent reportStratifier, + MeasureReportGroupStratifierComponent reportStratifier, StratifierDef stratifierDef, List populations, GroupDef groupDef) { @@ -103,17 +109,17 @@ private static void buildStratifier2( // Stratifiers should be of the same basis as population // Split subjects by result values // ex. all Male Patients and all Female Patients - componentStratifier(bc, stratifierDef, reportStratifier, populations, groupDef, subjectResultTable); + return componentStratifier(bc, stratifierDef, reportStratifier, populations, groupDef, subjectResultTable); } else { // standard Stratifier // one criteria expression defined, one set of criteria results Map subjectValues = stratifierDef.getResults(); - nonComponentStratifier(bc, stratifierDef, reportStratifier, populations, groupDef, subjectValues); + return nonComponentStratifier(bc, stratifierDef, reportStratifier, populations, groupDef, subjectValues); } } - private static void componentStratifier( + private static List componentStratifier( BuilderContext bc, StratifierDef stratifierDef, MeasureReportGroupStratifierComponent reportStratifier, @@ -123,6 +129,8 @@ private static void componentStratifier( var componentSubjects = groupSubjectsByValueDefSet(subjectCompValues); + var stratumDefs = new ArrayList(); + componentSubjects.forEach((valueSet, subjects) -> { // converts table into component value combinations // | Stratum | Set | List | @@ -133,11 +141,15 @@ private static void componentStratifier( // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | var reportStratum = reportStratifier.addStratum(); - buildStratum(bc, stratifierDef, reportStratum, valueSet, subjects, populations, groupDef); + final StratumDef stratumDef = buildStratum(bc, stratifierDef, reportStratum, valueSet, + subjects, populations, groupDef); + stratumDefs.add(stratumDef); }); + + return stratumDefs; } - private static void nonComponentStratifier( + private static List nonComponentStratifier( BuilderContext bc, StratifierDef stratifierDef, MeasureReportGroupStratifierComponent reportStratifier, @@ -158,13 +170,16 @@ private static void nonComponentStratifier( // Seems to be irrelevant for criteria based stratifiers var patients = List.of(); - buildStratum(bc, stratifierDef, reportStratum, stratValues, patients, populations, groupDef); - return; + var stratum = buildStratum(bc, stratifierDef, reportStratum, stratValues, patients, populations, groupDef); + return List.of(stratum); } Map> subjectsByValue = subjectValues.keySet().stream() .collect(Collectors.groupingBy( x -> new ValueWrapper(subjectValues.get(x).rawValue()))); + + var stratumMultiple = new ArrayList(); + // Stratum 1 // Value: 'M'--> subjects: subject1 // Stratum 2 @@ -182,8 +197,11 @@ private static void nonComponentStratifier( // multiple criteria // TODO: build out nonComponent stratum method Set stratValues = Set.of(new ValueDef(stratValue.getKey(), null)); - buildStratum(bc, stratifierDef, reportStratum, stratValues, patients, populations, groupDef); + var stratum = buildStratum(bc, stratifierDef, reportStratum, stratValues, patients, populations, groupDef); + stratumMultiple.add(stratum); } + + return stratumMultiple; } private static Map, List> groupSubjectsByValueDefSet( @@ -228,7 +246,7 @@ private static Map, List> groupSubjectsByValueDefSet( }))); } - private static void buildStratum( + private static StratumDef buildStratum( BuilderContext bc, StratifierDef stratifierDef, StratifierGroupComponent stratum, @@ -290,8 +308,17 @@ private static void buildStratum( // ** ** initial-population: subject2 for (MeasureGroupPopulationComponent mgpc : populations) { var stratumPopulation = stratum.addPopulation(); - buildStratumPopulation(bc, stratifierDef, stratumDef, stratumPopulation, subjectIds, mgpc, groupDef); + var stratumPopulationDef = buildStratumPopulation( + bc, + stratifierDef, + stratumDef, + stratumPopulation, + subjectIds, + mgpc, + groupDef); } + + return stratumDef; } // This is weird pattern where we have multiple qualifying values within a single stratum, @@ -302,7 +329,7 @@ private static CodeableConcept expressionResultToCodableConcept(ValueWrapper val private record ValueDef(ValueWrapper value, StratifierComponentDef def) {} - private static void buildStratumPopulation( + private static StratumPopulationDef buildStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, StratumDef stratumDef, @@ -358,6 +385,8 @@ private static void buildStratumPopulation( } else { buildResourceBasisStratumPopulation(bc, stratifierDef, sgpc, subjectIds, populationDef, groupDef); } + + return stratumPopulationDef; } private static void buildBooleanBasisStratumPopulation( From 76be529e4c936f25b448ca5f4167d4b29768f11f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 10:01:14 -0400 Subject: [PATCH 25/48] Only add stratum and stratum populations in the outer methods so as not to mutate the enclosing structures while building what will be added to them. --- .../fhir/cr/measure/common/StratifierDef.java | 6 --- .../fhir/cr/measure/common/StratumDef.java | 5 ++ .../cr/measure/r4/R4MeasureReportScorer.java | 11 ++--- .../cr/measure/r4/R4StratifierBuilder.java | 49 ++++++------------- 4 files changed, 25 insertions(+), 46 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java index 591672ae75..3e9d4118a1 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierDef.java @@ -59,12 +59,6 @@ public List getStratum() { return stratum; } - // LUKETODO: try out this pattern - public void addStratumDef(StratumDef stratumDef) { - stratum.add(stratumDef); - } - - // LUKETODO: try out this pattern public void addAllStratum(List stratumDefs) { stratum.addAll(stratumDefs); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java index cafedb8a10..908277a50b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.common; +import java.util.ArrayList; import java.util.List; // LUKETODO: javadoc @@ -23,4 +24,8 @@ public List getStratumPopulations() { public void addStratumPopulation(StratumPopulationDef stratumPopulationDef) { stratumPopulations.add(stratumPopulationDef); } + + public void addAllPopulations(ArrayList stratumPopulationDefs) { + stratumPopulations.addAll(stratumPopulationDefs); + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 72eaeff880..591e87c2d3 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -423,14 +423,13 @@ private Set getResultsForStratum( } private boolean doesStratumPopDefMatchGroupPopDef( - StratumPopulationDef stratumPopulationDef, - Entry> entry) { + StratumPopulationDef stratumPopulationDef, Entry> entry) { return stratumPopulationDef.getSubjects().stream() - // LUKETODO: push this up the the stratumPopulationDef building code? - .map(subject -> subject.split("Patient/")[1]) - .collect(Collectors.toUnmodifiableSet()) - .contains(entry.getKey()); + // LUKETODO: push this up the the stratumPopulationDef building code? + .map(subject -> subject.split("Patient/")[1]) + .collect(Collectors.toUnmodifiableSet()) + .contains(entry.getKey()); } private int getCountFromGroupPopulation( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index e791962038..ffffae6822 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -74,19 +74,14 @@ static void buildStratifier( new StringType(measureStratifier.getDescription())); } - var stratumDefs = buildMultipleStratum( - bc, - reportStratifier, - stratifierDef, - populations, - groupDef); - -// stratifierDef.addAllStratum(stratumDefs); + var stratumDefs = buildMultipleStratum(bc, reportStratifier, stratifierDef, populations, groupDef); + + stratifierDef.addAllStratum(stratumDefs); } private static List buildMultipleStratum( BuilderContext bc, - MeasureReportGroupStratifierComponent reportStratifier, + MeasureReportGroupStratifierComponent reportStratifier, StratifierDef stratifierDef, List populations, GroupDef groupDef) { @@ -140,9 +135,11 @@ private static List componentStratifier( // | Stratum-3 | <'M','hispanic/latino'> | [subject-c] | // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | + // LUKETODO: should we push this up as well? var reportStratum = reportStratifier.addStratum(); - final StratumDef stratumDef = buildStratum(bc, stratifierDef, reportStratum, valueSet, - subjects, populations, groupDef); + + var stratumDef = buildStratum(bc, stratifierDef, reportStratum, valueSet, subjects, populations, groupDef); + stratumDefs.add(stratumDef); }); @@ -178,7 +175,7 @@ private static List nonComponentStratifier( .collect(Collectors.groupingBy( x -> new ValueWrapper(subjectValues.get(x).rawValue()))); - var stratumMultiple = new ArrayList(); + var stratumMultiple = new ArrayList(); // Stratum 1 // Value: 'M'--> subjects: subject1 @@ -293,7 +290,6 @@ private static StratumDef buildStratum( } var stratumDef = new StratumDef(stratum.getValue().getText(), new ArrayList<>()); - stratifierDef.addStratumDef(stratumDef); // add stratum populations for stratifier // Group.populations @@ -306,18 +302,16 @@ private static StratumDef buildStratum( // ** subjects with stratifier value: 'F': subject2 // ** stratum.population // ** ** initial-population: subject2 + var stratumPopulationDefs = new ArrayList(); for (MeasureGroupPopulationComponent mgpc : populations) { var stratumPopulation = stratum.addPopulation(); - var stratumPopulationDef = buildStratumPopulation( - bc, - stratifierDef, - stratumDef, - stratumPopulation, - subjectIds, - mgpc, - groupDef); + var stratumPopulationDef = + buildStratumPopulation(bc, stratifierDef, stratumPopulation, subjectIds, mgpc, groupDef); + stratumPopulationDefs.add(stratumPopulationDef); } + stratumDef.addAllPopulations(stratumPopulationDefs); + return stratumDef; } @@ -332,14 +326,11 @@ private record ValueDef(ValueWrapper value, StratifierComponentDef def) {} private static StratumPopulationDef buildStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, - StratumDef stratumDef, StratifierGroupPopulationComponent sgpc, List subjectIds, MeasureGroupPopulationComponent population, GroupDef groupDef) { - logger.info("1234: buildStratumPopulation()"); - sgpc.setCode(population.getCode()); sgpc.setId(population.getId()); @@ -366,20 +357,10 @@ private static StratumPopulationDef buildStratumPopulation( .map(R4ResourceIdUtils::addPatientQualifier) .collect(Collectors.toUnmodifiableSet()); - // TODO: LD: introduce a new StratumDef object to hold the results of the intersection, to - // be ultimately passed down to the measure scorer, instead of retaining only the count - // intersect population subjects to stratifier.value subjects var subjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); - logger.info( - "1234: populationDef: {}, subjectIdsCommonToPopulation = {}", - populationDef.id(), - subjectIdsCommonToPopulation); - var stratumPopulationDef = new StratumPopulationDef(populationDef.id(), subjectIdsCommonToPopulation); - stratumDef.addStratumPopulation(stratumPopulationDef); - if (groupDef.isBooleanBasis()) { buildBooleanBasisStratumPopulation(bc, sgpc, populationDef, subjectIdsCommonToPopulation); } else { From 03867eed3142aa158fb21ef6ccc28abefb854f24 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 10:48:26 -0400 Subject: [PATCH 26/48] Add more test data for continuous variable boolean basis to prove that duplicate quantities are handled. Address TODOs and delete cruft. --- .../fhir/cr/measure/common/PopulationDef.java | 2 - .../fhir/cr/measure/common/StratumDef.java | 12 ++- .../measure/common/StratumPopulationDef.java | 10 +- .../cr/measure/r4/R4MeasureReportBuilder.java | 7 +- .../cr/measure/r4/R4MeasureReportScorer.java | 70 +++++++++----- .../cr/measure/r4/R4StratifierBuilder.java | 64 +------------ ...ariableResourceMeasureObservationTest.java | 94 +++++++++---------- ...n => patient-1940-male-1-encounter-1.json} | 4 +- .../patient-1940-male-2-encounter-1.json | 12 +++ ...940-male.json => patient-1940-male-1.json} | 2 +- .../tests/patient/patient-1940-male-2.json | 6 ++ 11 files changed, 141 insertions(+), 142 deletions(-) rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/{patient-1940-male-encounter-1.json => patient-1940-male-1-encounter-1.json} (65%) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-2-encounter-1.json rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/{patient-1940-male.json => patient-1940-male-1.json} (72%) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-2.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java index d8f17cb6a6..4f6f56ce16 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java @@ -19,8 +19,6 @@ public class PopulationDef { private final String criteriaReference; protected Set evaluatedResources; - protected Set resources; - protected Set subjects; protected Map> subjectResources = new HashMap<>(); public PopulationDef(String id, ConceptDef code, MeasurePopulationType measurePopulationType, String expression) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java index 908277a50b..79a512cdf7 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java @@ -3,8 +3,14 @@ import java.util.ArrayList; import java.util.List; -// LUKETODO: javadoc +/** + * Equivalent to StratifierDef, but for Stratum. + *

+ * For now, this contains the code text and stratum population defs, in order to help with + * continuous variable scoring, but will probably need to be enhanced for more use cases. + */ public class StratumDef { + // Equivalent to the FHIR stratum code text private final String text; private final List stratumPopulations; @@ -21,10 +27,6 @@ public List getStratumPopulations() { return stratumPopulations; } - public void addStratumPopulation(StratumPopulationDef stratumPopulationDef) { - stratumPopulations.add(stratumPopulationDef); - } - public void addAllPopulations(ArrayList stratumPopulationDefs) { stratumPopulations.addAll(stratumPopulationDefs); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java index bfc07633b3..6b8bab1331 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -2,9 +2,13 @@ import java.util.Set; -// LUKETODO: javadoc -// LUKETODO: more fields -// LUKETODO: this merges the concept of a Stratum and a Stratum population so we'll have to decouple these +// TODO: LD: more fields +/** + * Equivalent to the FHIR stratum population. + *

+ * For now, this contains only an id to help match it to the population def in question and subjects + * used to + */ public class StratumPopulationDef { private final String id; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 2f7fb195fa..2fea971b02 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -89,6 +89,11 @@ public BuilderContext(Measure measure, MeasureDef measureDef, MeasureReport meas this.measureReport = measureReport; } + // For error messages: + public String getMeasureUrl() { + return this.measure.getUrl(); + } + public Map contained() { return this.contained; } @@ -484,7 +489,7 @@ public int countObservations(PopulationDef populationDef) { .sum(); } - // LUKETODO: what's this for? + // LUKETODO: ask Justin about this: can we convert this to a Quantity? protected void buildMeasureObservations(BuilderContext bc, String observationName, Set resources) { for (int i = 0; i < resources.size(); i++) { // TODO: Do something with the resource... diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 591e87c2d3..f27308150a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -4,7 +4,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.apicatalog.jsonld.StringUtils; import jakarta.annotation.Nullable; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -303,19 +302,18 @@ private static Quantity aggregate(List quantities, ContinuousVariableO } private static List collectQuantities(Set resources) { - List quantities = new ArrayList<>(); - for (Object resource : resources) { - if (resource instanceof Map map) { - for (Object value : map.values()) { - if (value instanceof Quantity quantity) { - quantities.add(quantity); - } - } - } - } + var mapValues = resources.stream() + .filter(x -> x instanceof Map) + .map(x -> (Map) x) + .map(Map::values) + .flatMap(Collection::stream) + .toList(); - return quantities; + return mapValues.stream() + .filter(Quantity.class::isInstance) + .map(Quantity.class::cast) + .toList(); } protected void scoreStratifier( @@ -339,21 +337,24 @@ protected void scoreStratifier( final StratumDef stratumDef = stratifierDef.getStratum().stream() .filter(stratumDefInner -> StringUtils.isNotBlank(stratumDefInner.getText())) - // LUKETODO: consider refining this logic: - .filter(stratumDefInner -> - stratumDefInner.getText().equals(sgc.getValue().getText())) + .filter(stratumDefInner -> doesStratumDefMatchStratum(sgc, stratumDefInner)) .findFirst() .orElse(null); - // LUKETODO: should we always expect these to match up? + // TODO: LD: should we always expect these to match up? if (stratumDef == null) { - logger.warn("1234: stratumDef is null"); + logger.warn("stratumDef is null"); } scoreStratum(measureUrl, groupDef, optStratifierDef.get(), stratumDef, measureScoring, sgc); } } + // TODO: LD: consider refining this logic: + private boolean doesStratumDefMatchStratum(StratifierGroupComponent sgc, StratumDef stratumDefInner) { + return stratumDefInner.getText().equals(sgc.getValue().getText()); + } + protected void scoreStratum( String measureUrl, GroupDef groupDef, @@ -393,7 +394,9 @@ private Quantity getStratumScoreOrNull( final StratumPopulationDef stratumPopulationDef; if (stratumDef != null) { stratumPopulationDef = stratumDef.getStratumPopulations().stream() - .filter(x -> x.getId().startsWith(MeasurePopulationType.MEASUREOBSERVATION.toCode())) + // Ex: match "measure-observation-1" with "measure-observation" + .filter(stratumPopDef -> + stratumPopDef.getId().startsWith(MeasurePopulationType.MEASUREOBSERVATION.toCode())) .findFirst() .orElse(null); } else { @@ -410,12 +413,36 @@ private Quantity getStratumScoreOrNull( } } - // LUKETODO: new javadoc explaining what this does + /** + * The goal here is to extract the resources references by the population def for the subjects + * in the stratum populationDef. + *

+ * So, for example, if the stratum population def has subjects: + *

    + *
  • patient123
  • + *
  • patient456
  • + *
  • patient567
  • + *
+ * and the population has: + *
    + *
  • patient000 -> Patient000 -> Quantity(57)
  • + *
  • patient100 -> Patient100 -> Quantity(36)
  • + *
  • patient123 -> Patient123 -> Quantity(57)
  • + *
  • patient456 -> Patient456 -> Quantity(3)
  • + *
  • patient500 -> Patient500 -> Quantity(5)
  • + *
  • patient567 -> Patient567 -> Quantity(57)
  • + *
+ * Then the method returns: + *
    + *
  • Patient123 -> Quantity(57)
  • + *
  • Patient456 -> Quantity(3)
  • + *
  • Patient567 -> Quantity(57)
  • + *
+ */ private Set getResultsForStratum( PopulationDef measureObservationPopulationDef, StratumPopulationDef stratumPopulationDef) { return measureObservationPopulationDef.getSubjectResources().entrySet().stream() - // LUKETODO: split this the proper way using hapi-fhir classe .filter(entry -> doesStratumPopDefMatchGroupPopDef(stratumPopulationDef, entry)) .map(Entry::getValue) .flatMap(Collection::stream) @@ -426,7 +453,8 @@ private boolean doesStratumPopDefMatchGroupPopDef( StratumPopulationDef stratumPopulationDef, Entry> entry) { return stratumPopulationDef.getSubjects().stream() - // LUKETODO: push this up the the stratumPopulationDef building code? + // LUKETODO: split this the proper way using hapi-fhir classes + // LUKETODO: test for other resource types as well .map(subject -> subject.split("Patient/")[1]) .collect(Collectors.toUnmodifiableSet()) .contains(entry.getKey()); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index ffffae6822..eb02158c7f 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -12,7 +12,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.stream.Collector; @@ -21,7 +20,6 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.Expression; import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.Measure.MeasureGroupPopulationComponent; @@ -135,7 +133,6 @@ private static List componentStratifier( // | Stratum-3 | <'M','hispanic/latino'> | [subject-c] | // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | - // LUKETODO: should we push this up as well? var reportStratum = reportStratifier.addStratum(); var stratumDef = buildStratum(bc, stratifierDef, reportStratum, valueSet, subjects, populations, groupDef); @@ -349,8 +346,10 @@ private static StratumPopulationDef buildStratumPopulation( .orElse(null); if (populationDef == null) { - // LUKETODO: add more details to Exception - throw new InvalidRequestException("Invalid population definition"); + throw new InvalidRequestException("Invalid population definition for measure: %s since it's missing %s" + .formatted( + bc.getMeasureUrl(), + population.getCode().getCodingFirstRep().getCode())); } var popSubjectIds = populationDef.getSubjects().stream() @@ -396,61 +395,6 @@ private static void buildBooleanBasisStratumPopulation( } } - /* - the existing algo takes the measure-observation population from the group definition and goes through all resources to get the quantities - MeasurePopulationType.MEASUREOBSERVATION - - but we don't want that: we want to filter only resources that belong to the patients captured by each stratum - so we want to do some sort of wizardry that involves getting the stratum values, and using those to retrieve the associated resources - - so it's basically a hack to go from StratifierGroupComponent stratum value -> subject -> populationDef.subjectResources.get(subject) - to get Set of resources on which to do measure scoring - */ - // LUKETODO: Integrate this algorithm with a new StratumDef that will be populated in R4StratifierBuilder - private Set getResultsForStratum( - PopulationDef measureObservationPopulationDef, - StratifierDef stratifierDef, - StratifierGroupComponent stratum) { - - final String stratumValue = stratum.getValue().getText(); - - final Set subjectsWithStratumValue = stratifierDef.getResults().entrySet().stream() - .filter(entry -> doesStratumMatch(stratumValue, entry.getValue().rawValue())) - .map(Entry::getKey) - .collect(Collectors.toUnmodifiableSet()); - - return measureObservationPopulationDef.getSubjectResources().entrySet().stream() - .filter(entry -> subjectsWithStratumValue.contains(entry.getKey())) - .map(Entry::getValue) - .flatMap(Collection::stream) - .collect(Collectors.toUnmodifiableSet()); - } - - // LUKETODO: we may be able to do away with this - // LUKETODO:: we may need to match more types of stratum here: The below logic deals with - // currently anticipated use cases - private boolean doesStratumMatch(String stratumValueAsString, Object rawValueFromStratifier) { - if (rawValueFromStratifier == null || stratumValueAsString == null) { - return false; - } - - if (rawValueFromStratifier instanceof Integer rawValueFromStratifierAsInt) { - final int stratumValueAsInt = Integer.parseInt(stratumValueAsString); - - return stratumValueAsInt == rawValueFromStratifierAsInt; - } - - if (rawValueFromStratifier instanceof Enumeration rawValueFromStratifierAsEnumeration) { - return stratumValueAsString.equals(rawValueFromStratifierAsEnumeration.asStringValue()); - } - - if (rawValueFromStratifier instanceof String rawValueFromStratifierAsString) { - return stratumValueAsString.equals(rawValueFromStratifierAsString); - } - - return false; - } - private static void buildResourceBasisStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java index caed457ce5..cccd7009d9 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java @@ -35,49 +35,49 @@ void continuousVariableResourceMeasureObservationBooleanBasisAvg() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() - .hasScore("73.0") + .hasScore("74.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(10) + .hasCount(11) .up() .stratifierById("stratifier-gender") .hasStratumCount(4) .firstStratum() .hasValue("male") - .hasScore("81.5") + .hasScore("82.33333333333333") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) @@ -227,36 +227,36 @@ void continuousVariableResourceMeasureObservationBooleanBasisCount() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() - .hasScore("10.0") + .hasScore("11.0") .stratifierById("stratifier-gender") .hasStratumCount(4) .firstStratum() .hasValue("male") - .hasScore("2.0") + .hasScore("3.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) @@ -406,36 +406,36 @@ void continuousVariableResourceMeasureObservationBooleanBasisMedian() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() - .hasScore("76.5") + .hasScore("79.0") .stratifierById("stratifier-gender") .hasStratumCount(4) .firstStratum() .hasValue("male") - .hasScore("81.5") + .hasScore("84.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) @@ -585,17 +585,17 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() .hasScore("54.0") .stratifierById("stratifier-gender") @@ -605,16 +605,16 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { .hasScore("79.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) @@ -764,17 +764,17 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() .hasScore("84.0") .stratifierById("stratifier-gender") @@ -784,16 +784,16 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { .hasScore("84.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) @@ -943,36 +943,36 @@ void continuousVariableResourceMeasureObservationBooleanBasisSum() { .firstGroup() .population("initial-population") // 10 encounters in all - .hasCount(10) + .hasCount(11) .up() .population("measure-population") - .hasCount(10) + .hasCount(11) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") // There are 10 patients in all - .hasCount(10) + .hasCount(11) .up() - .hasScore("730.0") + .hasScore("814.0") .stratifierById("stratifier-gender") .hasStratumCount(4) .firstStratum() .hasValue("male") - .hasScore("163.0") + .hasScore("247.0") .hasPopulationCount(4) .population("initial-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population") - .hasCount(2) + .hasCount(3) .up() .population("measure-population-exclusion") .hasCount(0) .up() .population("measure-observation") - .hasCount(2) + .hasCount(3) .up() .up() .stratumByPosition(2) diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-1-encounter-1.json similarity index 65% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-1-encounter-1.json index 073378f93a..5a2f759c38 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-encounter-1.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-1-encounter-1.json @@ -1,9 +1,9 @@ { "resourceType": "Encounter", - "id": "patient-1940-male-encounter-1", + "id": "patient-1940-male-1-encounter-1", "status": "in-progress", "subject": { - "reference": "Patient/patient-1940-male" + "reference": "Patient/patient-1940-male-1" }, "period": { "start": "2024-01-01T00:00:00Z", diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-2-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-2-encounter-1.json new file mode 100644 index 0000000000..f4680f7f37 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/encounter/patient-1940-male-2-encounter-1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "patient-1940-male-2-encounter-1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient-1940-male-2" + }, + "period": { + "start": "2024-01-01T00:00:00Z", + "end": "2024-01-01T02:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-1.json similarity index 72% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-1.json index 5445ccc313..1befbb0f30 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-1.json @@ -1,6 +1,6 @@ { "resourceType": "Patient", - "id": "patient-1940-male", + "id": "patient-1940-male-1", "gender": "male", "birthDate": "1940-01-01" } \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-2.json new file mode 100644 index 0000000000..9a42bb1962 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableObservationBooleanBasis/input/tests/patient/patient-1940-male-2.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient-1940-male-2", + "gender": "male", + "birthDate": "1940-01-01" +} \ No newline at end of file From 92be178f74bc46a5844e126e10a305be489a5c79 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 11:16:56 -0400 Subject: [PATCH 27/48] Move measure evaluation logic to a new dedicate class: MeasureEvaluationResultHandler. Make use of LibraryInitHandler for pre-evaluation. --- .../cr/measure/common/LibraryInitHandler.java | 10 +- .../MeasureEvaluationResultHandler.java | 185 ++++++++++++++++++ .../measure/common/MeasureProcessorUtils.java | 164 ---------------- .../measure/dstu3/Dstu3MeasureProcessor.java | 5 +- .../cr/measure/r4/R4MeasureProcessor.java | 51 +++-- 5 files changed, 220 insertions(+), 195 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java index e566128a9a..9e9b05b315 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java @@ -17,14 +17,18 @@ private LibraryInitHandler() { } public static boolean initLibrary(CqlEngine context, VersionedIdentifier libraryIdentifiers) { - return initLibraries(context, List.of(libraryIdentifiers)); + return !initLibraries(context, List.of(libraryIdentifiers)).isEmpty(); + } + + public static void popLibraries(CqlEngine context, List compiledLibraries) { + compiledLibraries.forEach(library -> popLibrary(context)); } public static void popLibrary(CqlEngine context) { context.getState().exitLibrary(true); } - private static boolean initLibraries(CqlEngine context, List libraryIdentifiers) { + public static List initLibraries(CqlEngine context, List libraryIdentifiers) { var compiledLibraries = getCompiledLibraries(libraryIdentifiers, context); var libraries = @@ -33,7 +37,7 @@ private static boolean initLibraries(CqlEngine context, List getCompiledLibraries(List ids, CqlEngine context) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java new file mode 100644 index 0000000000..b136ab9169 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java @@ -0,0 +1,185 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.tuple.Pair; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.ICompositeType; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Exclusively responsible for calling CQL evaluation and collating the results among multiple + * measure defs in a FHIR version agnostic way. + */ +public class MeasureEvaluationResultHandler { + + private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluationResultHandler.class); + + private static final String EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE = "Exception for subjectId: %s, Message: %s"; + + /** + * Method that processes CQL Results into Measure defined fields that reference associated CQL expressions + * This is meant to be called by CQL CLI. + * + * @param results criteria expression results + * @param measureDef Measure defined objects + * @param measureEvalType the type of evaluation algorithm to apply to Criteria results + * @param applyScoring whether Measure Evaluator will apply set membership per measure scoring algorithm + * @param populationBasisValidator the validator class to use for checking consistency of results + */ + public static void processResults( + Map results, + MeasureDef measureDef, + @Nonnull MeasureEvalType measureEvalType, + boolean applyScoring, + PopulationBasisValidator populationBasisValidator) { + MeasureEvaluator evaluator = new MeasureEvaluator(populationBasisValidator); + // Populate MeasureDef using MeasureEvaluator + for (Map.Entry entry : results.entrySet()) { + // subject + String subjectId = entry.getKey(); + var sub = getSubjectTypeAndId(subjectId); + var subjectIdPart = sub.getRight(); + var subjectTypePart = sub.getLeft(); + // cql results + EvaluationResult evalResult = entry.getValue(); + try { + // populate results into MeasureDef + evaluator.evaluate( + measureDef, measureEvalType, subjectTypePart, subjectIdPart, evalResult, applyScoring); + } catch (Exception e) { + // Catch Exceptions from evaluation per subject, but allow rest of subjects to be processed (if + // applicable) + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); + // Capture error for MeasureReportBuilder + measureDef.addError(error); + logger.error(error, e); + } + } + } + + /** + * method used to execute generate CQL results via Library $evaluate, $evaluate-measure, etc + * + * @param subjectIds subjects to generate results for + * @param zonedMeasurementPeriod offset defined measurement period for evaluation + * @param context cql engine context + * @param multiLibraryIdMeasureEngineDetails container for engine, library and measure IDs + * @param continuousVariableObservationConverter used for continuous variable scoring FHIR version + * specific + * @return CQL results for Library defined in the Measure resource + */ + public static CompositeEvaluationResultsPerMeasure getEvaluationResults( + List subjectIds, + ZonedDateTime zonedMeasurementPeriod, + CqlEngine context, + MultiLibraryIdMeasureEngineDetails multiLibraryIdMeasureEngineDetails, + ContinuousVariableObservationConverter continuousVariableObservationConverter) { + + // measure -> subject -> results + var resultsBuilder = CompositeEvaluationResultsPerMeasure.builder(); + + // Library $evaluate each subject + // The goal here is to do each measure/library evaluation within the context of a single subject. + // This means that we will not switch between subject contexts while evaluating measures. + // Once we've switched to a different subject context, the previous expression cache is dropped. + for (String subjectId : subjectIds) { + if (subjectId == null) { + throw new InternalErrorException("SubjectId is required in order to calculate."); + } + Pair subjectInfo = getSubjectTypeAndId(subjectId); + String subjectTypePart = subjectInfo.getLeft(); + String subjectIdPart = subjectInfo.getRight(); + context.getState().setContextValue(subjectTypePart, subjectIdPart); + try { + var libraryIdentifiers = multiLibraryIdMeasureEngineDetails.getLibraryIdentifiers(); + + var evaluationResultsForMultiLib = multiLibraryIdMeasureEngineDetails + .getLibraryEngine() + .getEvaluationResult( + libraryIdentifiers, + subjectId, + null, + null, + null, + null, + null, + zonedMeasurementPeriod, + context); + + for (var libraryVersionedIdentifier : libraryIdentifiers) { + validateEvaluationResultExistsForIdentifier( + libraryVersionedIdentifier, evaluationResultsForMultiLib); + // standard CQL expression results + var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); + + var measureDefs = + multiLibraryIdMeasureEngineDetails.getMeasureDefsForLibrary(libraryVersionedIdentifier); + + final List measureObservationResults = + ContinuousVariableObservationHandler.continuousVariableEvaluation( + context, + measureDefs, + libraryVersionedIdentifier, + evaluationResult, + subjectTypePart, + continuousVariableObservationConverter); + + resultsBuilder.addResults(measureDefs, subjectId, evaluationResult, measureObservationResults); + + Optional.ofNullable(evaluationResultsForMultiLib.getExceptionFor(libraryVersionedIdentifier)) + .ifPresent(exception -> { + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted( + subjectId, exception.getMessage()); + resultsBuilder.addErrors(measureDefs, error); + logger.error(error, exception); + }); + } + + } catch (Exception e) { + // If there's any error we didn't anticipate, catch it here: + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); + var measureDefs = multiLibraryIdMeasureEngineDetails.getAllMeasureDefs(); + + resultsBuilder.addErrors(measureDefs, error); + logger.error(error, e); + } + } + + return resultsBuilder.build(); + } + + private static Pair getSubjectTypeAndId(String subjectId) { + if (subjectId.contains("/")) { + String[] subjectIdParts = subjectId.split("/"); + return Pair.of(subjectIdParts[0], subjectIdParts[1]); + } else { + throw new InvalidRequestException( + "Unable to determine Subject type for id: %s. SubjectIds must be in the format {subjectType}/{subjectId} (e.g. Patient/123)" + .formatted(subjectId)); + } + } + + private static void validateEvaluationResultExistsForIdentifier( + VersionedIdentifier versionedIdentifierFromQuery, + EvaluationResultsForMultiLib evaluationResultsForMultiLib) { + + var containsResults = evaluationResultsForMultiLib.containsResultsFor(versionedIdentifierFromQuery); + var containsExceptions = evaluationResultsForMultiLib.containsExceptionsFor(versionedIdentifierFromQuery); + + if (!containsResults && !containsExceptions) { + throw new InternalErrorException( + "Evaluation result in versionless search not found for identifier with ID: %s" + .formatted(versionedIdentifierFromQuery.getId())); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index b093f0f838..d1db180e43 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -1,25 +1,17 @@ package org.opencds.cqf.fhir.cr.measure.common; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; -import java.util.Map; import java.util.Optional; -import org.apache.commons.lang3.tuple.Pair; import org.hl7.elm.r1.IntervalTypeSpecifier; import org.hl7.elm.r1.NamedTypeSpecifier; import org.hl7.elm.r1.ParameterDef; -import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.ICompositeType; 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.cql.engine.runtime.Date; import org.opencds.cqf.cql.engine.runtime.DateTime; import org.opencds.cqf.cql.engine.runtime.Interval; @@ -30,46 +22,6 @@ public class MeasureProcessorUtils { private static final Logger logger = LoggerFactory.getLogger(MeasureProcessorUtils.class); - private static final String EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE = "Exception for subjectId: %s, Message: %s"; - - /** - * Method that processes CQL Results into Measure defined fields that reference associated CQL expressions - * @param results criteria expression results - * @param measureDef Measure defined objects - * @param measureEvalType the type of evaluation algorithm to apply to Criteria results - * @param applyScoring whether Measure Evaluator will apply set membership per measure scoring algorithm - * @param populationBasisValidator the validator class to use for checking consistency of results - */ - public void processResults( - Map results, - MeasureDef measureDef, - @Nonnull MeasureEvalType measureEvalType, - boolean applyScoring, - PopulationBasisValidator populationBasisValidator) { - MeasureEvaluator evaluator = new MeasureEvaluator(populationBasisValidator); - // Populate MeasureDef using MeasureEvaluator - for (Map.Entry entry : results.entrySet()) { - // subject - String subjectId = entry.getKey(); - var sub = getSubjectTypeAndId(subjectId); - var subjectIdPart = sub.getRight(); - var subjectTypePart = sub.getLeft(); - // cql results - EvaluationResult evalResult = entry.getValue(); - try { - // populate results into MeasureDef - evaluator.evaluate( - measureDef, measureEvalType, subjectTypePart, subjectIdPart, evalResult, applyScoring); - } catch (Exception e) { - // Catch Exceptions from evaluation per subject, but allow rest of subjects to be processed (if - // applicable) - var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); - // Capture error for MeasureReportBuilder - measureDef.addError(error); - logger.error(error, e); - } - } - } /** * method used to convert measurement period Interval object into ZonedDateTime @@ -256,122 +208,6 @@ public Date truncateDateTime(DateTime dateTime) { return new Date(odt.getYear(), odt.getMonthValue(), odt.getDayOfMonth()); } - /** - * method used to execute generate CQL results via Library $evaluate - * - * @param subjectIds subjects to generate results for - * @param zonedMeasurementPeriod offset defined measurement period for evaluation - * @param context cql engine context - * @param multiLibraryIdMeasureEngineDetails container for engine, library and measure IDs - * @param continuousVariableObservationConverter used for continuous variable scoring FHIR version - * specific - * @return CQL results for Library defined in the Measure resource - */ - public CompositeEvaluationResultsPerMeasure getEvaluationResults( - List subjectIds, - ZonedDateTime zonedMeasurementPeriod, - CqlEngine context, - MultiLibraryIdMeasureEngineDetails multiLibraryIdMeasureEngineDetails, - ContinuousVariableObservationConverter continuousVariableObservationConverter) { - - // measure -> subject -> results - var resultsBuilder = CompositeEvaluationResultsPerMeasure.builder(); - - // Library $evaluate each subject - // The goal here is to do each measure/library evaluation within the context of a single subject. - // This means that we will not switch between subject contexts while evaluating measures. - // Once we've switched to a different subject context, the previous expression cache is dropped. - for (String subjectId : subjectIds) { - if (subjectId == null) { - throw new InternalErrorException("SubjectId is required in order to calculate."); - } - Pair subjectInfo = this.getSubjectTypeAndId(subjectId); - String subjectTypePart = subjectInfo.getLeft(); - String subjectIdPart = subjectInfo.getRight(); - context.getState().setContextValue(subjectTypePart, subjectIdPart); - try { - var libraryIdentifiers = multiLibraryIdMeasureEngineDetails.getLibraryIdentifiers(); - - var evaluationResultsForMultiLib = multiLibraryIdMeasureEngineDetails - .getLibraryEngine() - .getEvaluationResult( - libraryIdentifiers, - subjectId, - null, - null, - null, - null, - null, - zonedMeasurementPeriod, - context); - - for (var libraryVersionedIdentifier : libraryIdentifiers) { - validateEvaluationResultExistsForIdentifier( - libraryVersionedIdentifier, evaluationResultsForMultiLib); - // standard CQL expression results - var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); - - var measureDefs = - multiLibraryIdMeasureEngineDetails.getMeasureDefsForLibrary(libraryVersionedIdentifier); - - final List measureObservationResults = - ContinuousVariableObservationHandler.continuousVariableEvaluation( - context, - measureDefs, - libraryVersionedIdentifier, - evaluationResult, - subjectTypePart, - continuousVariableObservationConverter); - - resultsBuilder.addResults(measureDefs, subjectId, evaluationResult, measureObservationResults); - - Optional.ofNullable(evaluationResultsForMultiLib.getExceptionFor(libraryVersionedIdentifier)) - .ifPresent(exception -> { - var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted( - subjectId, exception.getMessage()); - resultsBuilder.addErrors(measureDefs, error); - logger.error(error, exception); - }); - } - - } catch (Exception e) { - // If there's any error we didn't anticipate, catch it here: - var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); - var measureDefs = multiLibraryIdMeasureEngineDetails.getAllMeasureDefs(); - - resultsBuilder.addErrors(measureDefs, error); - logger.error(error, e); - } - } - - return resultsBuilder.build(); - } - - private void validateEvaluationResultExistsForIdentifier( - VersionedIdentifier versionedIdentifierFromQuery, - EvaluationResultsForMultiLib evaluationResultsForMultiLib) { - - var containsResults = evaluationResultsForMultiLib.containsResultsFor(versionedIdentifierFromQuery); - var containsExceptions = evaluationResultsForMultiLib.containsExceptionsFor(versionedIdentifierFromQuery); - - if (!containsResults && !containsExceptions) { - throw new InternalErrorException( - "Evaluation result in versionless search not found for identifier with ID: %s" - .formatted(versionedIdentifierFromQuery.getId())); - } - } - - public Pair getSubjectTypeAndId(String subjectId) { - if (subjectId.contains("/")) { - String[] subjectIdParts = subjectId.split("/"); - return Pair.of(subjectIdParts[0], subjectIdParts[1]); - } else { - throw new InvalidRequestException( - "Unable to determine Subject type for id: %s. SubjectIds must be in the format {subjectType}/{subjectId} (e.g. Patient/123)" - .formatted(subjectId)); - } - } - public MeasureEvalType getEvalType(MeasureEvalType evalType, String reportType, List subjectIds) { if (evalType == null) { evalType = MeasureEvalType.fromCode(reportType) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index a6336e60d7..5c2214220b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -27,6 +27,7 @@ import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEvaluationResultHandler; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; @@ -113,7 +114,7 @@ protected MeasureReport evaluateMeasure( ZonedDateTime zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval(measurementPeriod); // populate results from Library $evaluate if (!subjects.isEmpty()) { - var results = measureProcessorUtils.getEvaluationResults( + var results = MeasureEvaluationResultHandler.getEvaluationResults( subjectIds, zonedMeasurementPeriod, context, @@ -121,7 +122,7 @@ protected MeasureReport evaluateMeasure( Dstu3ContinuousVariableObservationConverter.INSTANCE); // Process Criteria Expression Results - measureProcessorUtils.processResults( + MeasureEvaluationResultHandler.processResults( results.processMeasureForSuccessOrFailure(measureDef), measureDef, evalType, diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 86e37f4b9c..3b3f720a19 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -35,7 +35,9 @@ import org.opencds.cqf.fhir.cql.VersionedIdentifiers; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; +import org.opencds.cqf.fhir.cr.measure.common.LibraryInitHandler; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEvaluationResultHandler; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; @@ -114,7 +116,7 @@ public MeasureReport evaluateMeasureResults( var measureDef = new R4MeasureDefBuilder().build(measure); // Process Criteria Expression Results - measureProcessorUtils.processResults( + MeasureEvaluationResultHandler.processResults( results, measureDef, evaluationType, @@ -159,7 +161,7 @@ public MeasureReport evaluateMeasure( final Map resultForThisMeasure = compositeEvaluationResultsPerMeasure.processMeasureForSuccessOrFailure(measureDef); - measureProcessorUtils.processResults( + MeasureEvaluationResultHandler.processResults( resultForThisMeasure, measureDef, evaluationType, @@ -273,7 +275,7 @@ public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( measurementPeriodParams); // populate results from Library $evaluate - return measureProcessorUtils.getEvaluationResults( + return MeasureEvaluationResultHandler.getEvaluationResults( subjects, zonedMeasurementPeriod, context, @@ -300,30 +302,27 @@ private void preLibraryEvaluationPeriodProcessing( CqlEngine context, Interval measurementPeriodParams) { - var compiledLibraries = getCompiledLibraries(libraryVersionedIdentifiers, context); + var compiledLibraries = LibraryInitHandler.initLibraries(context, libraryVersionedIdentifiers); - var libraries = - compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); - - // We need the libraries on the stack for setMeasurementPeriod(), - // specifically for .getMeasurementPeriodParameterDef() - context.getState().init(libraries); - - // if we comment this out MeasureScorerTest and other tests will fail with NPEs - setArgParameters(parameters, context, compiledLibraries); - - // set measurement Period from CQL if operation parameters are empty - measureProcessorUtils.setMeasurementPeriod( - measurementPeriodParams, - context, - measures.stream() - .map(Measure::getUrl) - .map(url -> Optional.ofNullable(url).orElse("Unknown Measure URL")) - .toList()); - - // Now pop the libraries off the stack, because we'll be adding them back during - // CQL library evaluation - popAllLibrariesFromCqlEngine(context, libraries); + try { + // if we comment this out MeasureScorerTest and other tests will fail with NPEs + setArgParameters(parameters, context, compiledLibraries); + + // set measurement Period from CQL if operation parameters are empty + measureProcessorUtils.setMeasurementPeriod( + measurementPeriodParams, + context, + measures.stream() + .map(Measure::getUrl) + .map(url -> Optional.ofNullable(url).orElse("Unknown Measure URL")) + .toList()); + } finally { + // Now pop the libraries off the stack, because we'll be adding them back during + // CQL library evaluation + // If no libraries were initialized, the List of compiledLibraries will be empty + // and this will no-op + LibraryInitHandler.popLibraries(context, compiledLibraries); + } } private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails(List measures) { From 7636a41afbcaea67af826afbfef3169bc5dd374e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 11:41:52 -0400 Subject: [PATCH 28/48] Move logic to strip resource qualifiers from measure scorer to stratifier builder. Address sonar feedback. --- ...ontinuousVariableObservationConverter.java | 1 - .../MeasureEvaluationResultHandler.java | 4 ++ .../fhir/cr/measure/common/StratumDef.java | 3 +- .../measure/common/StratumPopulationDef.java | 1 - ...ontinuousVariableObservationConverter.java | 5 +- ...ontinuousVariableObservationConverter.java | 6 +-- .../cr/measure/r4/R4MeasureProcessor.java | 48 ------------------- .../cr/measure/r4/R4MeasureReportScorer.java | 6 +-- .../r4/R4PopulationBasisValidator.java | 25 ---------- .../cr/measure/r4/R4StratifierBuilder.java | 17 +++++-- .../measure/r4/utils/R4ResourceIdUtils.java | 24 +++++++++- 11 files changed, 45 insertions(+), 95 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java index ddd31f1535..040e5a772c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java @@ -10,6 +10,5 @@ public interface ContinuousVariableObservationConverter { // TODO: LD: We need to come up with something other than an Observation to wrap FHIR Quantities - // QuantityHolder wrapResultAsQuantityHolder(String id, Object result); T wrapResultAsQuantityHolder(String id, Object result); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java index b136ab9169..475437a9bf 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java @@ -26,6 +26,10 @@ public class MeasureEvaluationResultHandler { private static final String EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE = "Exception for subjectId: %s, Message: %s"; + private MeasureEvaluationResultHandler() { + // static class + } + /** * Method that processes CQL Results into Measure defined fields that reference associated CQL expressions * This is meant to be called by CQL CLI. diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java index 79a512cdf7..c2a23950e5 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java @@ -1,6 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.common; -import java.util.ArrayList; import java.util.List; /** @@ -27,7 +26,7 @@ public List getStratumPopulations() { return stratumPopulations; } - public void addAllPopulations(ArrayList stratumPopulationDefs) { + public void addAllPopulations(List stratumPopulationDefs) { stratumPopulations.addAll(stratumPopulationDefs); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java index 6b8bab1331..3224c73e4c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -2,7 +2,6 @@ import java.util.Set; -// TODO: LD: more fields /** * Equivalent to the FHIR stratum population. *

diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java index e99eebce43..29a5699d79 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java @@ -9,13 +9,10 @@ */ @SuppressWarnings("squid:S6548") public enum Dstu3ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { + INSTANCE; @Override - // public QuantityHolder wrapResultAsQuantityHolder(String id, Object result) { - // return new QuantityHolder<>(id, convertToQuantity(result)); - // } - public Quantity wrapResultAsQuantityHolder(String id, Object result) { return convertToQuantity(result); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java index 9dfc55d06c..afb2608eff 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java @@ -9,12 +9,8 @@ */ @SuppressWarnings("squid:S6548") public enum R4ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { - INSTANCE; - // @Override - // public QuantityHolder wrapResultAsQuantityHolder(String id, Object result) { - // return new QuantityHolder<>(id, convertToQuantity(result)); - // } + INSTANCE; @Override public Quantity wrapResultAsQuantityHolder(String id, Object result) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 3b3f720a19..a1da2a3ba9 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -413,50 +413,6 @@ protected LibraryEngine getLibraryEngine(Parameters parameters, VersionedIdentif return new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); } - private List getCompiledLibraries(List ids, CqlEngine context) { - try { - var resolvedLibraryResults = - context.getEnvironment().getLibraryManager().resolveLibraries(ids); - - var allErrors = resolvedLibraryResults.allErrors(); - if (resolvedLibraryResults.hasErrors() || ids.size() > allErrors.size()) { - return resolvedLibraryResults.allCompiledLibraries(); - } - - if (ids.size() == 1) { - final List cqlCompilerExceptions = - resolvedLibraryResults.getErrorsFor(ids.get(0)); - - if (cqlCompilerExceptions.size() == 1) { - throw new IllegalStateException( - "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." - .formatted(ids.get(0).getId()), - cqlCompilerExceptions.get(0)); - } else { - throw new IllegalStateException( - "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" - .formatted( - ids.get(0).getId(), - cqlCompilerExceptions.stream() - .map(CqlCompilerException::getMessage) - .reduce((s1, s2) -> s1 + "; " + s2) - .orElse("No error messages found."))); - } - } - - throw new IllegalStateException( - "Unable to load CQL/ELM for libraries: %s Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" - .formatted(ids, allErrors)); - - } catch (CqlIncludeException exception) { - throw new IllegalStateException( - "Unable to load CQL/ELM for libraries: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." - .formatted( - ids.stream().map(VersionedIdentifier::getId).toList()), - exception); - } - } - protected void checkMeasureLibrary(Measure measure) { if (!measure.hasLibrary()) { throw new InvalidRequestException( @@ -548,8 +504,4 @@ public Interval buildMeasurementPeriod(ZonedDateTime periodStart, ZonedDateTime } return measurementPeriod; } - - private void popAllLibrariesFromCqlEngine(CqlEngine context, List libraries) { - libraries.forEach(lib -> context.getState().exitLibrary(true)); - } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index f27308150a..604c30e352 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -346,7 +346,7 @@ protected void scoreStratifier( logger.warn("stratumDef is null"); } - scoreStratum(measureUrl, groupDef, optStratifierDef.get(), stratumDef, measureScoring, sgc); + scoreStratum(measureUrl, groupDef, stratumDef, measureScoring, sgc); } } @@ -358,7 +358,6 @@ private boolean doesStratumDefMatchStratum(StratifierGroupComponent sgc, Stratum protected void scoreStratum( String measureUrl, GroupDef groupDef, - StratifierDef stratifierDef, StratumDef stratumDef, MeasureScoring measureScoring, StratifierGroupComponent stratum) { @@ -453,9 +452,6 @@ private boolean doesStratumPopDefMatchGroupPopDef( StratumPopulationDef stratumPopulationDef, Entry> entry) { return stratumPopulationDef.getSubjects().stream() - // LUKETODO: split this the proper way using hapi-fhir classes - // LUKETODO: test for other resource types as well - .map(subject -> subject.split("Patient/")[1]) .collect(Collectors.toUnmodifiableSet()) .contains(entry.getKey()); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index 2a427d0936..89c4712d08 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -191,31 +191,6 @@ private Optional> extractResourceType(String groupPopulationBasisCode) return Optional.empty(); } - private List> extractClassesFromSingleOrListResult(Object result) { - if (result == null) { - return Collections.emptyList(); - } - - if (!(result instanceof Iterable iterable)) { - return Collections.singletonList(result.getClass()); - } - - // Need to this to return List> and get rid of Sonar warnings. - final Stream> classStream = - getStream(iterable).filter(Objects::nonNull).map(Object::getClass); - - return classStream.toList(); - } - - private Stream getStream(Iterable iterable) { - if (iterable instanceof List list) { - return list.stream(); - } - - // It's entirely possible CQL returns an Iterable that is not a List, so we need to handle that case - return StreamSupport.stream(iterable.spliterator(), false); - } - private List prettyClassNames(List> classes) { return classes.stream().map(Class::getSimpleName).toList(); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index eb02158c7f..370c7b334d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -12,6 +12,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collector; @@ -356,12 +357,18 @@ private static StratumPopulationDef buildStratumPopulation( .map(R4ResourceIdUtils::addPatientQualifier) .collect(Collectors.toUnmodifiableSet()); - var subjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); + var qualifiedSubjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); - var stratumPopulationDef = new StratumPopulationDef(populationDef.id(), subjectIdsCommonToPopulation); + var unqualifiedSubjectIdsCommonToPopulation = qualifiedSubjectIdsCommonToPopulation.stream() + .filter(Objects::nonNull) + .map(R4StratifierBuilder::processSubjectId) + .collect(Collectors.toUnmodifiableSet()); + + var stratumPopulationDef = + new StratumPopulationDef(populationDef.id(), unqualifiedSubjectIdsCommonToPopulation); if (groupDef.isBooleanBasis()) { - buildBooleanBasisStratumPopulation(bc, sgpc, populationDef, subjectIdsCommonToPopulation); + buildBooleanBasisStratumPopulation(bc, sgpc, populationDef, qualifiedSubjectIdsCommonToPopulation); } else { buildResourceBasisStratumPopulation(bc, stratifierDef, sgpc, subjectIds, populationDef, groupDef); } @@ -369,6 +376,10 @@ private static StratumPopulationDef buildStratumPopulation( return stratumPopulationDef; } + private static String processSubjectId(String rawSubjectId) { + return R4ResourceIdUtils.stripAnyResourceQualifier(rawSubjectId); + } + private static void buildBooleanBasisStratumPopulation( BuilderContext bc, StratifierGroupPopulationComponent sgpc, diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4ResourceIdUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4ResourceIdUtils.java index 696eaf0826..78ab830626 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4ResourceIdUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4ResourceIdUtils.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.r4.utils; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import org.hl7.fhir.r4.model.ResourceType; @@ -7,6 +8,7 @@ * Various utilities for dealing with R4 resource IDs or their Strings. */ public class R4ResourceIdUtils { + private static final Pattern PATTERN_SLASH = Pattern.compile("/"); private R4ResourceIdUtils() { // Static utility class @@ -19,6 +21,26 @@ public static String addPatientQualifier(String t) { @Nonnull public static String stripPatientQualifier(String subjectId) { - return subjectId.replace(ResourceType.Patient.toString().concat("/"), ""); + return stripSpecificResourceQualifier(subjectId, ResourceType.Patient); + } + + @Nonnull + public static String stripSpecificResourceQualifier(String subjectId, ResourceType resourceType) { + return subjectId.replace(resourceType.toString().concat("/"), ""); + } + + public static String stripAnyResourceQualifier(String subjectId) { + + if (subjectId == null) { + return null; + } + + final String[] split = PATTERN_SLASH.split(subjectId); + + if (split.length >= 2) { + return split[1]; + } + + return split[0]; } } From 18d23e8a413edb4f417b82712e75b60ca3024713 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 11:44:29 -0400 Subject: [PATCH 29/48] Spotless. --- .../dstu3/Dstu3ContinuousVariableObservationConverter.java | 1 - .../measure/r4/R4ContinuousVariableObservationConverter.java | 1 - .../opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java | 1 - .../cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java | 4 ---- 4 files changed, 7 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java index 29a5699d79..af628be94b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java @@ -9,7 +9,6 @@ */ @SuppressWarnings("squid:S6548") public enum Dstu3ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { - INSTANCE; @Override diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java index afb2608eff..695a3e7fb6 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java @@ -9,7 +9,6 @@ */ @SuppressWarnings("squid:S6548") public enum R4ContinuousVariableObservationConverter implements ContinuousVariableObservationConverter { - INSTANCE; @Override diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index a1da2a3ba9..f505829aeb 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -14,7 +14,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.cqframework.cql.cql2elm.CqlCompilerException; import org.cqframework.cql.cql2elm.CqlIncludeException; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index 89c4712d08..399280a4ae 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -3,14 +3,10 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Enumeration; From 673ed4795c0b280d9e0678a731684d97566a854c Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 12:01:48 -0400 Subject: [PATCH 30/48] Renames and clean up cruft from bad merge with master. --- ...ontinuousVariableObservationConverter.java | 2 +- .../ContinuousVariableObservationHandler.java | 2 +- .../common/MeasureObservationResult.java | 24 -------------- .../common/MeasureObservationResults.java | 32 ------------------- .../common/ObservationEvaluationResult.java | 8 ----- ...ontinuousVariableObservationConverter.java | 2 +- ...ontinuousVariableObservationConverter.java | 2 +- 7 files changed, 4 insertions(+), 68 deletions(-) delete mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java delete mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java delete mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java index 040e5a772c..b78eed5b44 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationConverter.java @@ -10,5 +10,5 @@ public interface ContinuousVariableObservationConverter { // TODO: LD: We need to come up with something other than an Observation to wrap FHIR Quantities - T wrapResultAsQuantityHolder(String id, Object result); + T wrapResultAsQuantity(String id, Object result); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java index 564704893a..3c10148f85 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ContinuousVariableObservationHandler.java @@ -147,7 +147,7 @@ private static EvaluationResult processMeasureObserva var observationId = expressionName + "-" + index; // wrap result in Observation resource to avoid duplicate results data loss // in set object - var observation = continuousVariableObservationConverter.wrapResultAsQuantityHolder( + var observation = continuousVariableObservationConverter.wrapResultAsQuantity( observationId, observationResult.value()); // add function results to existing EvaluationResult under new expression // name diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java deleted file mode 100644 index 4f4873b9b2..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.common; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Capture a single set of results from continuous variable observations for a single population - */ -public record MeasureObservationResult( - String expressionName, Set evaluatedResources, Map functionResults) { - - static final MeasureObservationResult EMPTY = new MeasureObservationResult(null, Set.of(), Map.of()); - - @Override - public Map functionResults() { - return new HashMap<>(functionResults); - } - - @Override - public Set evaluatedResources() { - return new HashSetForFhirResources<>(evaluatedResources); - } -} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java deleted file mode 100644 index de1d57c025..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureObservationResults.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.common; - -import java.util.HashMap; -import java.util.List; -import org.opencds.cqf.cql.engine.execution.EvaluationResult; -import org.opencds.cqf.cql.engine.execution.ExpressionResult; - -/** - * Capture results for multiple continuous variable populations. - */ -public record MeasureObservationResults(List results) { - - static final MeasureObservationResults EMPTY = new MeasureObservationResults(List.of()); - - EvaluationResult withNewEvaluationResult(EvaluationResult origEvaluationResult) { - final EvaluationResult evaluationResult = new EvaluationResult(); - - var copyOfExpressionResults = new HashMap<>(origEvaluationResult.expressionResults); - - results.forEach(measureObservationResult -> copyOfExpressionResults.put( - measureObservationResult.expressionName(), buildExpressionResult(measureObservationResult))); - - evaluationResult.expressionResults.putAll(copyOfExpressionResults); - - return evaluationResult; - } - - private ExpressionResult buildExpressionResult(MeasureObservationResult measureObservationResult) { - return new ExpressionResult( - measureObservationResult.functionResults(), measureObservationResult.evaluatedResources()); - } -} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java deleted file mode 100644 index 0c1a3f2cc7..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ObservationEvaluationResult.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.common; - -import java.util.Set; - -/** - * A glorified Pair to capture both evaluation results and evaluated resources. - */ -public record ObservationEvaluationResult(Object result, Set evaluatedResources) {} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java index af628be94b..c57bc1d793 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3ContinuousVariableObservationConverter.java @@ -12,7 +12,7 @@ public enum Dstu3ContinuousVariableObservationConverter implements ContinuousVar INSTANCE; @Override - public Quantity wrapResultAsQuantityHolder(String id, Object result) { + public Quantity wrapResultAsQuantity(String id, Object result) { return convertToQuantity(result); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java index 695a3e7fb6..2ad69fae4d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4ContinuousVariableObservationConverter.java @@ -12,7 +12,7 @@ public enum R4ContinuousVariableObservationConverter implements ContinuousVariab INSTANCE; @Override - public Quantity wrapResultAsQuantityHolder(String id, Object result) { + public Quantity wrapResultAsQuantity(String id, Object result) { return convertToQuantity(result); } From 7603328c3ec8bc609555ce5cd261303f3da8adaf Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 24 Oct 2025 12:03:52 -0400 Subject: [PATCH 31/48] Cleanup more cruft. --- .../opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 604c30e352..820d19edac 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -206,7 +206,6 @@ protected void scoreGroup( } protected void scoreContinuousVariable(String measureUrl, MeasureReportGroupComponent mrgc, GroupDef groupDef) { - logger.info("1234: scoreContinuousVariable"); final Quantity aggregateQuantity = calculateContinuousVariableAggregateQuantity(measureUrl, groupDef, PopulationDef::getResources); @@ -388,8 +387,6 @@ private Quantity getStratumScoreOrNull( return null; } case CONTINUOUSVARIABLE -> { - logger.info("1234: calculateContinuousVariableAggregateQuantity()"); - final StratumPopulationDef stratumPopulationDef; if (stratumDef != null) { stratumPopulationDef = stratumDef.getStratumPopulations().stream() From ea945e3441c4b9df21ccece29fb616dcf48bdb99 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 27 Oct 2025 09:36:54 -0400 Subject: [PATCH 32/48] Start implementing tests for component criteria stratifiers and sprinkle TODOs for parts of the code that need to change. --- .../cr/measure/common/MeasureEvaluator.java | 31 ++-- .../r4/R4PopulationBasisValidator.java | 22 ++- .../cr/measure/r4/R4StratifierBuilder.java | 10 +- .../r4/ComponentCriteriaStratifierTest.java | 134 ++++++++++++++++++ .../cqf/fhir/cr/measure/r4/Measure.java | 14 +- .../cr/measure/r4/MeasureStratifierTest.java | 9 +- .../input/cql/ComponentCriteriaStratifier.cql | 44 ++++++ .../library/ComponentCriteriaStratifier.json | 18 +++ ...aStratifierBooleanBasisNoIntersection.json | 82 +++++++++++ ...tratifierBooleanBasisWithIntersection.json | 82 +++++++++++ ...eriaStratifierDateBasisNoIntersection.json | 82 +++++++++++ ...iaStratifierDateBasisWithIntersection.json | 82 +++++++++++ .../tests/encounter/enc_finished_pat1_1.json | 12 ++ .../tests/encounter/enc_finished_pat2_1.json | 12 ++ .../encounter/enc_in_progress_pat1_1.json | 12 ++ .../encounter/enc_in_progress_pat2_1.json | 12 ++ .../encounter/enc_in_progress_pat2_2.json | 12 ++ .../tests/encounter/enc_planned_pat1_1.json | 12 ++ .../tests/encounter/enc_planned_pat2_1.json | 12 ++ .../tests/encounter/enc_triaged_pat1_1.json | 12 ++ .../tests/encounter/enc_triaged_pat2_1.json | 12 ++ .../input/tests/patient/patient1.json | 6 + .../input/tests/patient/patient2.json | 6 + 23 files changed, 712 insertions(+), 18 deletions(-) create mode 100644 cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/library/ComponentCriteriaStratifier.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat1_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat2_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat1_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_2.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat1_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat2_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat1_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat2_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient2.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 68def47559..fcc93ac3fd 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -22,6 +22,8 @@ import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureScoringTypePopulations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This class implements the core Measure evaluation logic that's defined in the @@ -43,6 +45,9 @@ */ @SuppressWarnings({"removal", "squid:S1135", "squid:S3776"}) public class MeasureEvaluator { + + private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluator.class); + private final PopulationBasisValidator populationBasisValidator; public MeasureEvaluator(PopulationBasisValidator populationBasisValidator) { @@ -430,6 +435,9 @@ protected void evaluateGroup( evaluateStratifiers(subjectId, groupDef.stratifiers(), evaluationResult); + // LUKETODO: do we want to validate criteria stratifiers versus scoring? for example, a ratio scoring seems + // incompatible with criteria stratifiers + var scoring = groupDef.measureScoring(); switch (scoring) { case PROPORTION, RATIO: @@ -465,9 +473,13 @@ protected void addStratifierComponentResult( List components, EvaluationResult evaluationResult, String subjectId) { for (StratifierComponentDef component : components) { var expressionResult = evaluationResult.forExpression(component.expression()); - Optional.ofNullable(expressionResult.value()) - .ifPresent(nonNullValue -> - component.putResult(subjectId, nonNullValue, expressionResult.evaluatedResources())); + Optional.ofNullable(expressionResult) + .ifPresentOrElse( + nonNullExpressionResult -> component.putResult( + subjectId, + nonNullExpressionResult.value(), + nonNullExpressionResult.evaluatedResources()), + () -> logger.warn("Could not find CQL expression result for: {}", component.expression())); } } @@ -478,14 +490,15 @@ protected void evaluateStratifiers( if (!stratifierDef.components().isEmpty()) { addStratifierComponentResult(stratifierDef.components(), evaluationResult, subjectId); } else { - var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); Optional.ofNullable(expressionResult) - .map(ExpressionResult::value) - .ifPresent(nonNullValue -> stratifierDef.putResult( - subjectId, // context of CQL expression ex: Patient based - nonNullValue, - expressionResult.evaluatedResources())); + .ifPresentOrElse( + nonNullExpressionResult -> stratifierDef.putResult( + subjectId, // context of CQL expression ex: Patient based + nonNullExpressionResult.value(), + nonNullExpressionResult.evaluatedResources()), + () -> logger.warn( + "Could not find CQL expression result for: {}", stratifierDef.expression())); } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index 399280a4ae..e05830d60e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -17,6 +17,7 @@ import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.runtime.Code; import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; +import org.opencds.cqf.fhir.cr.measure.common.CodeDef; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.PopulationBasisValidator; @@ -127,16 +128,22 @@ private void validateExpressionResultType( } var resultClasses = StratifierUtils.extractClassesFromSingleOrListResult(expressionResult.value()); - var groupPopulationBasisCode = groupDef.getPopulationBasis().code(); + var groupPopulationBasis = groupDef.getPopulationBasis(); if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { + // LUKETODO: refine this error handling because we may get an empty expression result, as opposed to a + // non-empty wrong expression result if (resultClasses.stream() - .map(Class::getSimpleName) - .noneMatch(simpleName -> simpleName.equals(groupPopulationBasisCode))) { + .noneMatch( + resourceClass -> doesResourceMatchPopulationBasis(resourceClass, groupPopulationBasis))) { throw new InvalidRequestException( "criteria-based stratifier is invalid for expression: [%s] due to mismatch between population basis: [%s] and result types: %s for measure URL: %s" - .formatted(expression, groupPopulationBasisCode, prettyClassNames(resultClasses), url)); + .formatted( + expression, groupPopulationBasis.code(), prettyClassNames(resultClasses), url)); + + // LUKETODO: add validation for component criteria stratifiers, which needs the initial population + // resources as well } // skip validation below since for criteria-based stratifier, the boolean basis test is irrelevant @@ -153,13 +160,18 @@ private void validateExpressionResultType( "stratifier expression criteria results for expression: [%s] must fall within accepted types for population-basis: [%s] for Measure: [%s] due to mismatch between total result classes: %s and matching result classes: %s" .formatted( expression, - groupPopulationBasisCode, + groupPopulationBasis.code(), url, prettyClassNames(resultClasses), prettyClassNames(resultMatchingClasses))); } } + // LUKETODO: refine this to deal with all kinds of different basis types + private boolean doesResourceMatchPopulationBasis(Class resourceClass, CodeDef groupPopulationBasisCode) { + return resourceClass.getSimpleName().equalsIgnoreCase(groupPopulationBasisCode.code()); + } + private Optional> extractResourceType(String groupPopulationBasisCode) { if (BOOLEAN_BASIS.equals(groupPopulationBasisCode)) { return Optional.of(Boolean.class); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index 370c7b334d..4e7038808c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -113,6 +113,7 @@ private static List buildMultipleStratum( } } + // LUKETODO: I think we need to add logic here: private static List componentStratifier( BuilderContext bc, StratifierDef stratifierDef, @@ -250,6 +251,7 @@ private static StratumDef buildStratum( List populations, GroupDef groupDef) { boolean isComponent = values.size() > 1; + String stratumDefText = null; for (ValueDef valuePair : values) { ValueWrapper value = valuePair.value; var componentDef = valuePair.def; @@ -270,6 +272,7 @@ private static StratumDef buildStratum( // non-component stratifiers only set stratified value, code is set on stratifier object // value being stratified: 'M' stratum.setValue((CodeableConcept) value.getValue()); + stratumDefText = stratum.getValue().getText(); } } else if (isComponent) { // component stratifier example: code: "gender", value: 'M' @@ -284,10 +287,15 @@ private static StratumDef buildStratum( // non-component stratifiers only set stratified value, code is set on stratifier object // value being stratified: 'M' stratum.setValue(expressionResultToCodableConcept(value)); + stratumDefText = stratum.getValue().getText(); + } else if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { + stratumDefText = value.getValueAsString(); } } - var stratumDef = new StratumDef(stratum.getValue().getText(), new ArrayList<>()); + // LUKETODO: if this is a criteria + // var stratumDef = new StratumDef(stratum.getValue().getText(), new ArrayList<>()); + var stratumDef = new StratumDef(stratumDefText, new ArrayList<>()); // add stratum populations for stratifier // Group.populations diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java new file mode 100644 index 0000000000..63da1ac7dd --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -0,0 +1,134 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import org.hl7.fhir.r4.model.MeasureReport; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; + +public class ComponentCriteriaStratifierTest { + + private static final Given GIVEN = Measure.given().repositoryFor("ComponentCriteriaStratifier"); + + /* + population=1/1/2024, 1/2/2024 + Components + criteria stratifier 1 + raw result: 2/1/2024, 1/2/2024 + criteria stratifier 2 + raw result: 2/3/2024, 1/2/2024 + stratum population: 1/2/2024 + */ + @Test + void cohortDateComponentCriteriaStratWithIntersection() { + + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisWithIntersection") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-jan2-feb3-jan2") + .firstStratum() + .hasPopulationCount(1) + .up() + .up() + .up() + .report(); + + System.out.println("report = " + report); + } + + /* + population=1/1/2024, 1/2/2024 + Components + criteria stratifier 1 + raw result: 2/1/2024, 1/2/2024 + criteria stratifier 2 + raw result: 2/3/2024, 1/2/2024 + stratum population: 1/2/2024 + */ + @Test + void cohortDateComponentCriteriaStratNoIntersection() { + + // LUKETODO: Justin simple example 2 + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersection") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .firstStratum() + .hasPopulationCount(0) + .up() + .up() + .up() + .report(); + + System.out.println("report = " + report); + } + + // LUKETODO: boolean basis + + @Test + void cohortBooleanComponentCriteriaStratWithIntersection() { + + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisWithIntersection") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-jan2-feb3-jan2") + .firstStratum() + .hasPopulationCount(1) + .up() + .up() + .up() + .report(); + + System.out.println("report = " + report); + } + + @Test + void cohortBooleanComponentCriteriaStratNoIntersection() { + + // LUKETODO: Justin simple example 2 + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersection") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .firstStratum() + .hasPopulationCount(0) + .up() + .up() + .up() + .report(); + + System.out.println("report = " + report); + } + + // LUKETODO: encounter basis +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index c124757a74..133c0b6866 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -898,7 +898,7 @@ public SelectedStratifier firstStratifier() { } public SelectedStratifier stratifierById(String stratId) { - final SelectedStratifier stratifier = this.stratifier(g -> g.getStratifier().stream() + var stratifier = this.stratifier(g -> g.getStratifier().stream() .filter(t -> t.getId().equals(stratId)) .findFirst() .orElse(null)); @@ -911,6 +911,9 @@ public SelectedStratifier stratifierById(String stratId) { public SelectedStratifier stratifier( Selector stratifierSelector) { var s = stratifierSelector.select(value()); + if (s == null) { + return null; + } return new SelectedStratifier(s, this); } @@ -1041,11 +1044,15 @@ public SelectedStratum stratum(CodeableConcept value) { } public SelectedStratum stratum(String textValue) { - return stratum(s -> s.getStratum().stream() + var stratum = stratum(s -> s.getStratum().stream() .filter(x -> x.hasValue() && x.getValue().hasText()) .filter(x -> x.getValue().getText().equals(textValue)) .findFirst() .orElse(null)); + + assertNotNull(stratum); + + return stratum; } public SelectedStratum stratumByComponentValueText(String textValue) { @@ -1068,6 +1075,9 @@ public SelectedStratum stratum( Selector stratumSelector) { var s = stratumSelector.select(value()); + if (s == null) { + return null; + } return new SelectedStratum(s, this); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java index 2184dd6337..c57ab03287 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureStratifierTest.java @@ -4,8 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; @@ -212,7 +214,7 @@ void cohortBooleanValueStratDifferentStratTypeFromBasisInvalid() { */ @Test void cohortBooleanValueStratComponentStrat() { - GIVEN_MEASURE_STRATIFIER_TEST + final MeasureReport report = GIVEN_MEASURE_STRATIFIER_TEST .when() .measureId("CohortBooleanStratComponent") .evaluate() @@ -243,6 +245,11 @@ void cohortBooleanValueStratComponentStrat() { .up() .up() .report(); + + final String json = + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report); + + System.out.println("json = " + json); } /** diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql new file mode 100644 index 0000000000..272350a3b9 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -0,0 +1,44 @@ +library ComponentCriteriaStratifier + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +parameter "Measurement Period" Interval default Interval[@2024-01-01T00:00:00, @2024-12-31T23:59:59] + +context Patient + +define "Initial Population Boolean": + exists("All Encounters") + +define "All Encounters": + [Encounter] E + +define "Stratifier Encounter Finished Boolean": + exists("Stratifier Encounter Finished") + +define "Stratifier Encounter Finished": + [Encounter] E + where E.status = 'finished' + +define "Stratifier Encounter In-Progress Boolean": + exists("Stratifier Encounter In-Progress") + +define "Stratifier Encounter In-Progress": + [Encounter] E + where E.status = 'in-progress' + +define "Initial Population Date": + { @2024-01-01, @2024-01-02 } + +define "Stratifier Feb1 Jan2": + { @2024-02-01, @2024-01-02 } + +define "Stratifier Feb3 Jan2": + { @2024-02-03, @2024-01-02 } + +define "Stratifier Feb1 Mar2": + { @2024-02-01, @2024-03-02 } + +define "Stratifier Feb1 Mar2 Jan2": + { @2024-02-03, @2024-03-02, @2024-01-02 } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/library/ComponentCriteriaStratifier.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/library/ComponentCriteriaStratifier.json new file mode 100644 index 0000000000..adbbb006ea --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/library/ComponentCriteriaStratifier.json @@ -0,0 +1,18 @@ +{ + "resourceType": "Library", + "id": "ComponentCriteriaStratifier", + "url": "http://example.com/Library/ComponentCriteriaStratifier", + "name": "ComponentCriteriaStratifier", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/ComponentCriteriaStratifier.cql" + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json new file mode 100644 index 0000000000..a22423c940 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisNoIntersection", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisNoIntersection", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersection", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-mar2", + "code" : { + "text": "Stratifier Feb1 Mar2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2" + } + }, + { + "id": "stratifier-comp-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json new file mode 100644 index 0000000000..5448a3883c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisWithIntersection", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisWithIntersection", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleanBasisWithIntersection", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-jan2-feb3-jan2", + "code" : { + "text": "Stratifier Feb1 Jan2 + Feb3 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished", + "code" : { + "text": "Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Boolean" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Boolean" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json new file mode 100644 index 0000000000..99f77a3b18 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisNoIntersection", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisNoIntersection", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersection", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-mar2", + "code" : { + "text": "Stratifier Feb1 Mar2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2" + } + }, + { + "id": "stratifier-comp-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json new file mode 100644 index 0000000000..878b889a21 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisWithIntersection", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisWithIntersection", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisWithIntersection", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-jan2-feb3-jan2", + "code" : { + "text": "Stratifier Feb1 Jan2 + Feb3 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-jan2", + "code" : { + "text": "Stratifier Feb1 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Jan2" + } + }, + { + "id": "stratifier-comp-feb3-jan2", + "code" : { + "text": "Stratifier Feb3 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb3 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat1_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat1_1.json new file mode 100644 index 0000000000..533cb9c4cd --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat1_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_finished_pat1_1", + "status": "finished", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat2_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat2_1.json new file mode 100644 index 0000000000..4b041f661f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_finished_pat2_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_finished_pat2_1", + "status": "finished", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat1_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat1_1.json new file mode 100644 index 0000000000..a8ed56744d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat1_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_in_progress_pat1_1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_1.json new file mode 100644 index 0000000000..31ec9ccea3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_in_progress_pat2_1", + "status": "in-progress", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_2.json new file mode 100644 index 0000000000..98a28e953e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_in_progress_pat2_2.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_in_progress_pat2_2", + "status": "in-progress", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat1_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat1_1.json new file mode 100644 index 0000000000..d44d2bdef1 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat1_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_pat1_1", + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat2_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat2_1.json new file mode 100644 index 0000000000..a8861480ac --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_planned_pat2_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_pat2_1", + "status": "planned", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat1_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat1_1.json new file mode 100644 index 0000000000..cdc0fdf403 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat1_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_triaged_pat1_1", + "status": "triaged", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat2_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat2_1.json new file mode 100644 index 0000000000..138ed12705 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_triaged_pat2_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_triaged_pat2_1", + "status": "triaged", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient1.json new file mode 100644 index 0000000000..179f54afb8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient1.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient1", + "gender": "female", + "birthDate": "1904-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient2.json new file mode 100644 index 0000000000..1ab526f851 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/patient/patient2.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient2", + "gender": "male", + "birthDate": "1924-06-01" +} \ No newline at end of file From 56dcaccbfd58a08831d7b8b6349685b310d80782 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 27 Oct 2025 16:05:45 -0400 Subject: [PATCH 33/48] Add CQL for 5 distinct scenarios for component criteria stratifiers. Add more verification methods to Measure. Add TODOs for design and functional changes. --- .../MeasureEvaluationResultHandler.java | 1 + .../cr/measure/r4/R4MeasureReportScorer.java | 6 ++ .../cr/measure/r4/R4StratifierBuilder.java | 18 +++- .../cqf/fhir/cr/measure/r4/Measure.java | 20 ++++- .../input/cql/ComponentCriteriaStratifier.cql | 86 +++++++++++++++++-- 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java index 475437a9bf..cdbf0fd70e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java @@ -126,6 +126,7 @@ public static CompositeEvaluationResultsPerMeasure ge // standard CQL expression results var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); + // LUKETODO: add functionality for warnings versus errors from CQL results and some clear tests var measureDefs = multiLibraryIdMeasureEngineDetails.getMeasureDefsForLibrary(libraryVersionedIdentifier); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 820d19edac..725901ad03 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -84,6 +84,9 @@ *

(v3.18.0 and below) Previous calculation of measure score from MeasureReport only interpreted Numerator, Denominator membership since exclusions and exceptions were already applied. Now exclusions and exceptions are present in Denominator and Numerator populations, the measure scorer calculation has to take into account additional population membership to determine Final-Numerator and Final-Denominator values

*/ @SuppressWarnings("squid:S1135") +// LUKETODO: we does this have to be an R4 class at all? why can't we base this entirely off defs? +// LUKETODO: as a migration path, why can't we push up logic gradually to the BaseMeasureReportScorer, moving away from +// FHIR-version specific logic public class R4MeasureReportScorer extends BaseMeasureReportScorer { private static final Logger logger = LoggerFactory.getLogger(R4MeasureReportScorer.class); @@ -176,6 +179,9 @@ protected void scoreGroup( switch (measureScoring) { case PROPORTION, RATIO: + // LUKETODO: here, we're taking the counts from the R4 populations, but why do we have to store them + // there? + // why can't we put the counts in the population defs? var score = calcProportionScore( getCountFromGroupPopulation(mrgc.getPopulation(), NUMERATOR) - getCountFromGroupPopulation(mrgc.getPopulation(), NUMERATOR_EXCLUSION), diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index 4e7038808c..aea5451302 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -293,8 +293,22 @@ private static StratumDef buildStratum( } } - // LUKETODO: if this is a criteria - // var stratumDef = new StratumDef(stratum.getValue().getText(), new ArrayList<>()); + // LUKETODO: so my SPECIFIC problem is this: I don't want to rely on the TEXT, or any other + // identifier to link between the FHIR stratum/population and the Def stratum, + // This is unreliable, because in component stratifier use cases, we don't necessarily have + // the text or another ID to link with + + // LUKETODO: this sucks and I need a better design: + /* + 1. I'm trying to work with a FHIR stratum and a StratumDef, at the same time, as well as + stratifier and stratum population + 2. we're setting both of these things at the same time, and then trying to use one to find the + other when doing the scoring + 3. so it looks like we may need to populate the defs with absolutely everything right off the bat + 4. we hold the counts in the FHIR measure population: does that make sense? + 5. should we consider just doing away with the concept of "R4 scoring" altogether, and just pass + the results from the Defs to the FHIR classes at the last minute? + */ var stratumDef = new StratumDef(stratumDefText, new ArrayList<>()); // add stratum populations for stratifier diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index 133c0b6866..b3f7d054c9 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -870,17 +870,23 @@ public SelectedGroup hasDateOfCompliance() { } public SelectedPopulation population(String name) { - return this.population(g -> g.getPopulation().stream() + var population = this.population(g -> g.getPopulation().stream() .filter(x -> x.hasCode() && x.getCode().hasCoding() && x.getCode().getCoding().get(0).getCode().equals(name)) .findFirst() - .get()); + .orElse(null)); + + assertNotNull(population); + return population; } public SelectedPopulation population( Selector populationSelector) { var p = populationSelector.select(value()); + if (p == null) { + return null; + } return new SelectedPopulation(p, this); } @@ -989,6 +995,11 @@ public SelectedPopulation(MeasureReportGroupPopulationComponent value, SelectedG super(value, parent); } + public SelectedPopulation hasName(String name) { + assertEquals(name, value().getCode().getCodingFirstRep().getCode()); + return this; + } + public SelectedPopulation hasCount(int count) { MeasureValidationUtils.validatePopulation(value(), count); return this; @@ -1184,5 +1195,10 @@ public SelectedStratumPopulation hasNoStratumPopulationSubjectResults() { assertNull(value().getSubjectResults().getReference()); return this; } + + public SelectedStratumPopulation hasName(String name) { + assertEquals(name, value().getCode().getCodingFirstRep().getCode()); + return this; + } } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql index 272350a3b9..a6726ac542 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -11,22 +11,67 @@ context Patient define "Initial Population Boolean": exists("All Encounters") -define "All Encounters": +define "Initial Population Resource": + "Encounters Arrived Planned" + +define "Encounters Arrived Planned": [Encounter] E + where E.status.value in { 'arrived', 'planned' } + +// +// Boolean/Resource +// -define "Stratifier Encounter Finished Boolean": - exists("Stratifier Encounter Finished") +// Scenario 1: Overlap on Arrived + +define "Stratifier Encounters Arrived Triaged": + [Encounter] E + where E.status.value in { 'arrived', 'triaged' } -define "Stratifier Encounter Finished": +define "Stratifier Encounters Arrived In-Progress": [Encounter] E - where E.status = 'finished' + where E.status.value in { 'arrived', 'in-progress' } -define "Stratifier Encounter In-Progress Boolean": - exists("Stratifier Encounter In-Progress") +// Scenario 2: No overlap due to intersection only between initial-pop and strat2 -define "Stratifier Encounter In-Progress": +define "Stratifier Encounters Planned Triaged": [Encounter] E - where E.status = 'in-progress' + where E.status.value in { 'planned', 'triaged' } + +define "Stratifier Encounters Arrived Cancelled": + [Encounter] E + where E.status.value in { 'arrived', 'cancelled' } + +// Scenario 3: No overlap despite total intersection between initial-pop and strat2 + +define "Stratifier Encounters Cancelled Finished": + [Encounter] E + where E.status.value in { 'cancelled', 'finished' } + +define "Stratifier Encounters Arrived Planned": + "Encounters Arrived Planned": + +// Scenario 4: No overlap despite total intersection between strat1 and strat2 + +define "Stratifier Encounters Cancelled Triaged": + [Encounter] E + where E.status.value in { 'cancelled', 'triaged' } + +// Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all + +define "Stratifier Encounters Cancelled In-Progress": + [Encounter] E + where E.status.value in { 'cancelled', 'in-progress' } + +define "Stratifier Encounters Finished Triaged": + [Encounter] E + where E.status.value in { 'finished', 'triaged' } + +// +// Date +// + +// Scenario 1: Overlap on 2024-01-02 define "Initial Population Date": { @2024-01-01, @2024-01-02 } @@ -37,8 +82,31 @@ define "Stratifier Feb1 Jan2": define "Stratifier Feb3 Jan2": { @2024-02-03, @2024-01-02 } +// Scenario 2: No overlap due to intersection only between initial-pop and strat2 + define "Stratifier Feb1 Mar2": { @2024-02-01, @2024-03-02 } define "Stratifier Feb1 Mar2 Jan2": { @2024-02-03, @2024-03-02, @2024-01-02 } + +// Scenario 3: No overlap despite total intersection between initial-pop and strat2 + +define "Stratifier Mar1 Apr1": + { @2024-03-01, @2024-04-01 } + +define "Stratifier Jan1 Jan2": + { @2024-01-01, @2024-02-01 } + +// Scenario 4: No overlap despite total intersection between strat1 and strat2 + +define "Stratifier Mar1 Apr1": + { @2024-03-01, @2024-04-01 } + +// Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all + +define "Stratifier May1 Jun1": + { @2024-05-01, @2024-06-01 } + +define "Stratifier Jul1 Aug1": + { @2024-07-01, @2024-08-01 } \ No newline at end of file From d222b857f67d1c2cfbc236be2a60a00b8904a600 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 27 Oct 2025 17:06:23 -0400 Subject: [PATCH 34/48] More work to set up the tests for scenarios 1 and 2 but still more work to be done. --- .../cr/measure/r4/R4StratifierBuilder.java | 2 + .../r4/ComponentCriteriaStratifierTest.java | 122 ++++++++++++++---- .../input/cql/ComponentCriteriaStratifier.cql | 34 +++-- ...rBooleanBasisNoIntersectionScenario2.json} | 14 +- ...ooleanBasisWithIntersectionScenario1.json} | 22 ++-- ...fierDateBasisNoIntersectionScenario2.json} | 6 +- ...erDateBasisWithIntersectionScenario1.json} | 6 +- ...EncounterBasisNoIntersectionScenario2.json | 82 ++++++++++++ ...counterBasisWithIntersectionScenario1.json | 82 ++++++++++++ .../tests/encounter/enc_arrived_pat1_1.json | 12 ++ .../tests/encounter/enc_arrived_pat1_2.json | 12 ++ 11 files changed, 333 insertions(+), 61 deletions(-) rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/{ComponentCriteriaStratifierBooleanBasisWithIntersection.json => ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json} (80%) rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/{ComponentCriteriaStratifierBooleanBasisNoIntersection.json => ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1.json} (69%) rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/{ComponentCriteriaStratifierDateBasisNoIntersection.json => ComponentCriteriaStratifierDateBasisNoIntersectionScenario2.json} (92%) rename cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/{ComponentCriteriaStratifierDateBasisWithIntersection.json => ComponentCriteriaStratifierDateBasisWithIntersectionScenario1.json} (91%) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_1.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_2.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index aea5451302..9916f041a3 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -460,6 +460,8 @@ private static int getStratumCountUpper( if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { final Set resources = populationDef.getResources(); + // LUKETODO: for Date scenario1, these are EMPTY: WHY??? + // LUKETODO: for the component criteria scenario, we don't add the results directly to the stratifierDef, but to each of the component defs, which is why this is empty final Set results = stratifierDef.getAllCriteriaResultValues(); if (resources.isEmpty() || results.isEmpty()) { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 63da1ac7dd..8d89218f94 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.r4; +import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.r4.model.MeasureReport; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; @@ -18,72 +19,138 @@ public class ComponentCriteriaStratifierTest { stratum population: 1/2/2024 */ @Test - void cohortDateComponentCriteriaStratWithIntersection() { + void cohortDateComponentCriteriaStratWithIntersectionScenario1() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisWithIntersection") + .measureId("ComponentCriteriaStratifierDateBasisWithIntersectionScenario1") .evaluate() .then() .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() + .hasName("initial-population") .hasCount(4) .up() .hasStratifierCount(1) .stratifierById("stratifier-feb1-jan2-feb3-jan2") .firstStratum() .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() .up() .up() .up() .report(); - System.out.println("report = " + report); + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } /* population=1/1/2024, 1/2/2024 Components criteria stratifier 1 - raw result: 2/1/2024, 1/2/2024 + raw result: 2/1/2024, 3/2/2024 criteria stratifier 2 - raw result: 2/3/2024, 1/2/2024 - stratum population: 1/2/2024 + raw result: 2/3/2024, 3/2/2024, 1/2/2024 + stratum population: NONE: lack of intersection between components and population */ @Test - void cohortDateComponentCriteriaStratNoIntersection() { + void cohortDateComponentCriteriaStratNoIntersectionScenario2() { - // LUKETODO: Justin simple example 2 final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisNoIntersection") + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario2") .evaluate() .then() .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() + .hasName("initial-population") .hasCount(4) .up() .hasStratifierCount(1) .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") .firstStratum() - .hasPopulationCount(0) + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() .up() .up() .up() .report(); - System.out.println("report = " + report); + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } - // LUKETODO: boolean basis + @Test + void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { + + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounters-arrived-triaged-arrived-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + } @Test - void cohortBooleanComponentCriteriaStratWithIntersection() { + void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisWithIntersection") + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + } + + @Test + void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { + + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1") .evaluate() .then() .hasGroupCount(1) @@ -93,23 +160,26 @@ void cohortBooleanComponentCriteriaStratWithIntersection() { .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-feb1-jan2-feb3-jan2") + .stratifierById("stratifier-encounters-arrived-triaged-arrived-in-progress-resource") .firstStratum() .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() .up() .up() .up() .report(); - System.out.println("report = " + report); + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test - void cohortBooleanComponentCriteriaStratNoIntersection() { + void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { - // LUKETODO: Justin simple example 2 final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersection") + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2") .evaluate() .then() .hasGroupCount(1) @@ -119,16 +189,22 @@ void cohortBooleanComponentCriteriaStratNoIntersection() { .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .stratifierById("stratifier-encounter-finished-in-progress-encounter") .firstStratum() - .hasPopulationCount(0) + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() .up() .up() .up() .report(); - System.out.println("report = " + report); + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } - // LUKETODO: encounter basis + // LUKETODO: test that explicitly handles mismatches and asserts error handling: + +// 9. 1 of n Component stratifier criteria expression has non-compliant population basis (population = Resource, Stratifier expression result is "String" or something). Throws error } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql index a6726ac542..150cc07d63 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -9,11 +9,14 @@ parameter "Measurement Period" Interval default Interval[@2024-01-01T0 context Patient define "Initial Population Boolean": - exists("All Encounters") + exists("Initial Population Resource") define "Initial Population Resource": "Encounters Arrived Planned" +define "Initial Population Date": + { @2024-01-01, @2024-01-02 } + define "Encounters Arrived Planned": [Encounter] E where E.status.value in { 'arrived', 'planned' } @@ -22,13 +25,19 @@ define "Encounters Arrived Planned": // Boolean/Resource // -// Scenario 1: Overlap on Arrived +//// Scenario 1: Overlap on Arrived + +define "Stratifier Encounters Arrived Triaged Boolean": + exists("Stratifier Encounters Arrived Triaged Resource") -define "Stratifier Encounters Arrived Triaged": +define "Stratifier Encounters Arrived Triaged Resource": [Encounter] E where E.status.value in { 'arrived', 'triaged' } -define "Stratifier Encounters Arrived In-Progress": +define "Stratifier Encounters Arrived In-Progress Boolean": + exists("Stratifier Encounters Arrived In-Progress Resource") + +define "Stratifier Encounters Arrived In-Progress Resource": [Encounter] E where E.status.value in { 'arrived', 'in-progress' } @@ -49,7 +58,7 @@ define "Stratifier Encounters Cancelled Finished": where E.status.value in { 'cancelled', 'finished' } define "Stratifier Encounters Arrived Planned": - "Encounters Arrived Planned": + "Encounters Arrived Planned" // Scenario 4: No overlap despite total intersection between strat1 and strat2 @@ -73,9 +82,6 @@ define "Stratifier Encounters Finished Triaged": // Scenario 1: Overlap on 2024-01-02 -define "Initial Population Date": - { @2024-01-01, @2024-01-02 } - define "Stratifier Feb1 Jan2": { @2024-02-01, @2024-01-02 } @@ -100,13 +106,13 @@ define "Stratifier Jan1 Jan2": // Scenario 4: No overlap despite total intersection between strat1 and strat2 -define "Stratifier Mar1 Apr1": - { @2024-03-01, @2024-04-01 } - -// Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all - define "Stratifier May1 Jun1": { @2024-05-01, @2024-06-01 } +// Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all + define "Stratifier Jul1 Aug1": - { @2024-07-01, @2024-08-01 } \ No newline at end of file + { @2024-07-01, @2024-08-01 } + +define "Stratifier Sep1 Oct1": + { @2024-09-01, @2024-10-01 } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json similarity index 80% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json index 5448a3883c..a2ebe7f5fa 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersection.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json @@ -1,8 +1,8 @@ { - "id": "ComponentCriteriaStratifierBooleanBasisWithIntersection", + "id": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2", "resourceType": "Measure", - "name": "ComponentCriteriaStratifierBooleanBasisWithIntersection", - "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleanBasisWithIntersection", + "name": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersectionScenario2", "library": [ "http://example.com/Library/ComponentCriteriaStratifier" ], @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-feb1-jan2-feb3-jan2", + "id": "stratifier-encounter-finished-in-progress-boolean", "code" : { - "text": "Stratifier Feb1 Jan2 + Feb3 Jan2" + "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" }, "extension": [ { @@ -55,9 +55,9 @@ ], "component": [ { - "id": "stratifier-encounter-finished", + "id": "stratifier-encounter-finished-in-progress", "code" : { - "text": "Encounter Finished" + "text": "Stratifier Stratifier Encounter Finished" }, "criteria": { "language": "text/cql.identifier", diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1.json similarity index 69% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1.json index a22423c940..be1b76dedf 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersection.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1.json @@ -1,8 +1,8 @@ { - "id": "ComponentCriteriaStratifierBooleanBasisNoIntersection", + "id": "ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1", "resourceType": "Measure", - "name": "ComponentCriteriaStratifierBooleanBasisNoIntersection", - "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersection", + "name": "ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1", "library": [ "http://example.com/Library/ComponentCriteriaStratifier" ], @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "id": "stratifier-encounters-arrived-triaged-arrived-in-progress-boolean", "code" : { - "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + "text": "Stratifier Encounters Arrived Triaged Arrived In-Progress Boolean" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-comp-feb1-mar2", + "id": "stratifier-encounters-arrived-triaged", "code" : { - "text": "Stratifier Feb1 Mar2" + "text": "Encounters Arrived Triaged" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2" + "expression": "Stratifier Encounters Arrived Triaged Boolean" } }, { - "id": "stratifier-comp-feb1-mar2-jan2", + "id": "stratifier-encounters-in-progress", "code" : { - "text": "Stratifier Feb1 Mar2 Jan2" + "text": "Encounters Arrived In-Progress" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2 Jan2" + "expression": "Stratifier Encounters Arrived In-Progress Boolean" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario2.json similarity index 92% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario2.json index 99f77a3b18..50fb869fb5 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersection.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario2.json @@ -1,8 +1,8 @@ { - "id": "ComponentCriteriaStratifierDateBasisNoIntersection", + "id": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario2", "resourceType": "Measure", - "name": "ComponentCriteriaStratifierDateBasisNoIntersection", - "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersection", + "name": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario2", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario2", "library": [ "http://example.com/Library/ComponentCriteriaStratifier" ], diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersectionScenario1.json similarity index 91% rename from cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json rename to cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersectionScenario1.json index 878b889a21..cd258c95a8 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersection.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisWithIntersectionScenario1.json @@ -1,8 +1,8 @@ { - "id": "ComponentCriteriaStratifierDateBasisWithIntersection", + "id": "ComponentCriteriaStratifierDateBasisWithIntersectionScenario1", "resourceType": "Measure", - "name": "ComponentCriteriaStratifierDateBasisWithIntersection", - "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisWithIntersection", + "name": "ComponentCriteriaStratifierDateBasisWithIntersectionScenario1", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisWithIntersectionScenario1", "library": [ "http://example.com/Library/ComponentCriteriaStratifier" ], diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json new file mode 100644 index 0000000000..6053950de8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-encounter", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished", + "code" : { + "text": "Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Stratifier Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Resource" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1.json new file mode 100644 index 0000000000..8f71d8b950 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounters-arrived-triaged-arrived-in-progress-resource", + "code" : { + "text": "Stratifier Encounters Arrived Triaged Arrived In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounters-arrived-triaged", + "code" : { + "text": "Encounters Arrived Triaged" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived Triaged Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounters Arrived In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived In-Progress Resource" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_1.json new file mode 100644 index 0000000000..6e4892d86c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_1.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_pat1_1", + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_2.json new file mode 100644 index 0000000000..2cf05b37bd --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/tests/encounter/enc_arrived_pat1_2.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_pat1_2", + "status": "arrived", + "subject": { + "reference": "Patient/patient2" + }, + "period": { + "start": "2024-01-01T00:00:00-05:00", + "end": "2024-12-31T00:00:00-05:00" + } +} \ No newline at end of file From 19bd44a1f5b9f8ae85d915e41da9f7b88e20720c Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 10:12:40 -0400 Subject: [PATCH 35/48] Fix compile errors. --- .../opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java | 1 - .../opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java | 1 - .../cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index 2f19d4c83e..7c76429b41 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -270,7 +270,6 @@ private StratifierDef buildStratifierDef(MeasureGroupStratifierComponent mgsc) { conceptToConceptDef(mgsc.getCode()), mgsc.getCriteria().getExpression(), getStratifierType(mgsc), - new ArrayList<>(), components); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java index d839eb2dd6..e25c08fe65 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java @@ -588,7 +588,6 @@ private static StratifierDef buildOutputStratifierDef(int componentCount, String new ConceptDef(List.of(new CodeDef("system", "code")), expression), expression, MeasureStratifierType.VALUE, - List.of(), IntStream.range(0, componentCount) .mapToObj(num -> buildOutputStratifierComponentDef(expression + num)) .toList()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java index 80c8d58a5f..6d890e73e3 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java @@ -456,7 +456,7 @@ private static List buildStratifierDefs(String... populations) { @Nonnull private static StratifierDef buildStratifierDef(String expression) { - return new StratifierDef(null, null, expression, MeasureStratifierType.VALUE, List.of(), List.of()); + return new StratifierDef(null, null, expression, MeasureStratifierType.VALUE, List.of()); } @Nonnull @@ -467,6 +467,7 @@ private static EvaluationResult buildEvaluationResult(Map expres return evaluationResult; } + // LUKETODO: what's this for? @Nonnull private static EvaluationResult buildEvaluationResult(Object expressionResult) { final EvaluationResult evaluationResult = new EvaluationResult(); From 891eff50acdaca0933fe9ccea423e5e2a4539507 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 13:52:27 -0400 Subject: [PATCH 36/48] Capture more scenarios in tests with comments and empty test methods. --- .../r4/ComponentCriteriaStratifierTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 8d89218f94..366c582b9d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -87,6 +87,21 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario2() { System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } + @Test + void cohortDateComponentCriteriaStratNoIntersectionScenario3() { + + } + + @Test + void cohortDateComponentCriteriaStratNoIntersectionScenario4() { + + } + + @Test + void cohortDateComponentCriteriaStratNoIntersectionScenario5() { + + } + @Test void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { @@ -146,6 +161,21 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } + @Test + void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { + + } + + @Test + void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { + + } + + @Test + void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { + + } + @Test void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { @@ -204,7 +234,39 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } + @Test + void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { + + } + + + @Test + void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { + + } + + + @Test + void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { + + } + // LUKETODO: test that explicitly handles mismatches and asserts error handling: // 9. 1 of n Component stratifier criteria expression has non-compliant population basis (population = Resource, Stratifier expression result is "String" or something). Throws error + + @Test + void cohortBooleanComponentCriteriaStratPopulationStratExpressionMismatchEncounter() { + + } + + @Test + void cohortEncounterComponentCriteriaStratPopulationStratExpressionMismatchDate() { + + } + + @Test + void cohortDateComponentCriteriaStratPopulationStratExpressionMismatchBoolean() { + + } } From b0e4fe8bbad61c34b353dacde293cdfff19e9979 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 14:22:12 -0400 Subject: [PATCH 37/48] Set up mismatch test files and assertions. JSON and SQL expressions still not correct. --- .../r4/ComponentCriteriaStratifierTest.java | 246 ++++++++++++++++++ ...ratifierBooleanBasisMismatchEncounter.json | 82 ++++++ ...erBooleanBasisNoIntersectionScenario3.json | 82 ++++++ ...erBooleanBasisNoIntersectionScenario4.json | 82 ++++++ ...erBooleanBasisNoIntersectionScenario5.json | 82 ++++++ ...riaStratifierDateBasisMismatchBoolean.json | 82 ++++++ ...ifierDateBasisNoIntersectionScenario3.json | 82 ++++++ ...ifierDateBasisNoIntersectionScenario4.json | 82 ++++++ ...ifierDateBasisNoIntersectionScenario5.json | 82 ++++++ ...aStratifierEncounterBasisMismatchDate.json | 82 ++++++ ...EncounterBasisNoIntersectionScenario3.json | 82 ++++++ ...EncounterBasisNoIntersectionScenario4.json | 82 ++++++ ...EncounterBasisNoIntersectionScenario5.json | 82 ++++++ 13 files changed, 1230 insertions(+) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisMismatchEncounter.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisMismatchBoolean.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisMismatchDate.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 366c582b9d..5f41cd4481 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -90,16 +90,91 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario2() { @Test void cohortDateComponentCriteriaStratNoIntersectionScenario3() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario3") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortDateComponentCriteriaStratNoIntersectionScenario4() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario4") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortDateComponentCriteriaStratNoIntersectionScenario5() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario5") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -164,16 +239,88 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -237,18 +384,90 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } // LUKETODO: test that explicitly handles mismatches and asserts error handling: @@ -258,15 +477,42 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { @Test void cohortBooleanComponentCriteriaStratPopulationStratExpressionMismatchEncounter() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisMismatchEncounter") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratPopulationStratExpressionMismatchDate() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisMismatchDate") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortDateComponentCriteriaStratPopulationStratExpressionMismatchBoolean() { + final MeasureReport report = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisMismatchBoolean") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisMismatchEncounter.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisMismatchEncounter.json new file mode 100644 index 0000000000..49002cbeac --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisMismatchEncounter.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisMismatchEncounter", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisMismatchEncounter", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleanBasisMismatchEncounter", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounters-arrived-triaged-arrived-in-progress-boolean", + "code" : { + "text": "Stratifier Encounters Arrived Triaged Arrived In-Progress Boolean" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounters-arrived-triaged", + "code" : { + "text": "Encounters Arrived Triaged" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived Triaged Boolean" + } + }, + { + "id": "stratifier-encounters-in-progress", + "code" : { + "text": "Encounters Arrived In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived In-Progress Boolean" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json new file mode 100644 index 0000000000..c146e934b6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersectionScenario3", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-boolean", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished-in-progress", + "code" : { + "text": "Stratifier Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Boolean" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Boolean" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json new file mode 100644 index 0000000000..9a711657df --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersectionScenario4", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-boolean", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished-in-progress", + "code" : { + "text": "Stratifier Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Boolean" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Boolean" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json new file mode 100644 index 0000000000..b14f16c435 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5", + "url": "http://example.com/Measure/ComponentCriteriaStratifierBooleaBasisNoIntersectionScenario5", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Boolean" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-boolean", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished-in-progress", + "code" : { + "text": "Stratifier Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Boolean" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Boolean" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisMismatchBoolean.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisMismatchBoolean.json new file mode 100644 index 0000000000..7efcd742c1 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisMismatchBoolean.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisMismatchBoolean", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisMismatchBoolean", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisMismatchBoolean", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-jan2-feb3-jan2", + "code" : { + "text": "Stratifier Feb1 Jan2 + Feb3 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-jan2", + "code" : { + "text": "Stratifier Feb1 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Jan2" + } + }, + { + "id": "stratifier-comp-feb3-jan2", + "code" : { + "text": "Stratifier Feb3 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb3 Jan2" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json new file mode 100644 index 0000000000..a13102acbc --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario3", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario3", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-mar2", + "code" : { + "text": "Stratifier Feb1 Mar2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2" + } + }, + { + "id": "stratifier-comp-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json new file mode 100644 index 0000000000..71a1f0458e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario4", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario4", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-mar2", + "code" : { + "text": "Stratifier Feb1 Mar2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2" + } + }, + { + "id": "stratifier-comp-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json new file mode 100644 index 0000000000..6732c0cb09 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario5", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierDateBasisNoIntersectionScenario5", + "url": "http://example.com/Measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "date" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Date" + } + } + ], + "stratifier": [ + { + "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-comp-feb1-mar2", + "code" : { + "text": "Stratifier Feb1 Mar2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2" + } + }, + { + "id": "stratifier-comp-feb1-mar2-jan2", + "code" : { + "text": "Stratifier Feb1 Mar2 Jan2" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Feb1 Mar2 Jan2" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisMismatchDate.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisMismatchDate.json new file mode 100644 index 0000000000..f24b0885df --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisMismatchDate.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisMismatchDate", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisMismatchDate", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisMismatchDate", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounters-arrived-triaged-arrived-in-progress-resource", + "code" : { + "text": "Stratifier Encounters Arrived Triaged Arrived In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounters-arrived-triaged", + "code" : { + "text": "Encounters Arrived Triaged" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived Triaged Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Encounters Arrived In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounters Arrived In-Progress Resource" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json new file mode 100644 index 0000000000..31eb0ea25a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-encounter", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished", + "code" : { + "text": "Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Stratifier Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Resource" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json new file mode 100644 index 0000000000..6c38a832a7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-encounter", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished", + "code" : { + "text": "Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Stratifier Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Resource" + } + } + ] + } + ] + } + ] +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json new file mode 100644 index 0000000000..6edb653f5f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json @@ -0,0 +1,82 @@ +{ + "id": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5", + "resourceType": "Measure", + "name": "ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5", + "url": "http://example.com/Measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5", + "library": [ + "http://example.com/Library/ComponentCriteriaStratifier" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "id": "group-1", + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population Resource" + } + } + ], + "stratifier": [ + { + "id": "stratifier-encounter-finished-in-progress-encounter", + "code" : { + "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-population-stratifier-type", + "valueCode": "criteria" + } + ], + "component": [ + { + "id": "stratifier-encounter-finished", + "code" : { + "text": "Stratifier Encounter Finished" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter Finished Resource" + } + }, + { + "id": "stratifier-encounter-in-progress", + "code" : { + "text": "Stratifier Encounter In-Progress" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratifier Encounter In-Progress Resource" + } + } + ] + } + ] + } + ] +} From e8e76037b4fb3f9b39350eff1eddee6056ccf21e Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 15:13:10 -0400 Subject: [PATCH 38/48] Start fixing test measures and assertions. Improve error handling for measure evaluation stratifier def processing. --- .../cr/measure/common/MeasureEvaluator.java | 25 +- .../cr/measure/r4/R4StratifierBuilder.java | 13 +- .../r4/ComponentCriteriaStratifierTest.java | 536 +++++++++--------- .../input/cql/ComponentCriteriaStratifier.cql | 5 +- ...erBooleanBasisNoIntersectionScenario3.json | 14 +- ...ifierDateBasisNoIntersectionScenario3.json | 16 +- ...ifierDateBasisNoIntersectionScenario4.json | 16 +- ...ifierDateBasisNoIntersectionScenario5.json | 16 +- 8 files changed, 337 insertions(+), 304 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 4a8c69fbdf..a427039a47 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -500,9 +500,15 @@ private void addStratifierComponentResult( for (StratifierComponentDef component : components) { var expressionResult = evaluationResult.forExpression(component.expression()); - Optional.ofNullable(expressionResult.value()) - .ifPresent(nonNullValue -> - component.putResult(subjectId, nonNullValue, expressionResult.evaluatedResources())); + Optional.ofNullable(expressionResult) + .ifPresentOrElse( + nonNullExpressionResult -> component.putResult( + subjectId, + nonNullExpressionResult.value(), + nonNullExpressionResult.evaluatedResources()), + () -> logger.warn( + "Could not find CQL expression result for stratifier component expression: {}", + component.expression())); } } @@ -511,11 +517,14 @@ private void addStratifierNonComponentResult( var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); Optional.ofNullable(expressionResult) - .map(ExpressionResult::value) - .ifPresent(nonNullValue -> stratifierDef.putResult( - subjectId, // context of CQL expression ex: Patient based - nonNullValue, - expressionResult.evaluatedResources())); + .ifPresentOrElse( + nonNullExpressionResult -> stratifierDef.putResult( + subjectId, // context of CQL expression ex: Patient based + nonNullExpressionResult.value(), + nonNullExpressionResult.evaluatedResources()), + () -> logger.warn( + "Could not find CQL expression result for stratifier expression: {}", + stratifierDef.expression())); } /** diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index 673c700134..f039863414 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -2,8 +2,6 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import com.google.common.collect.HashBasedTable; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import java.util.ArrayList; @@ -11,8 +9,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import javax.annotation.Nonnull; @@ -38,8 +34,6 @@ import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueWrapper; -import org.opencds.cqf.fhir.cr.measure.common.StratumDef; -import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureReportBuilder.BuilderContext; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; @@ -222,9 +216,9 @@ private static void buildStratum( // non-component stratifiers only set stratified value, code is set on stratifier object // value being stratified: 'M' stratum.setValue(expressionResultToCodableConcept(value)); -// stratumDefText = stratum.getValue().getText(); + // stratumDefText = stratum.getValue().getText(); } else if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { -// stratumDefText = value.getValueAsString(); + // stratumDefText = value.getValueAsString(); } } @@ -373,7 +367,8 @@ private static int getStratumCountUpper( if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { final Set resources = populationDef.getResources(); - // LUKETODO: for the component criteria scenario, we don't add the results directly to the stratifierDef, but to each of the component defs, which is why this is empty + // LUKETODO: for the component criteria scenario, we don't add the results directly to the stratifierDef, + // but to each of the component defs, which is why this is empty final Set results = stratifierDef.getAllCriteriaResultValues(); if (resources.isEmpty() || results.isEmpty()) { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 5f41cd4481..29fc199692 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -4,6 +4,7 @@ import org.hl7.fhir.r4.model.MeasureReport; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; +import org.opencds.cqf.fhir.cr.measure.r4.Measure.SelectedReport; public class ComponentCriteriaStratifierTest { @@ -21,11 +22,15 @@ public class ComponentCriteriaStratifierTest { @Test void cohortDateComponentCriteriaStratWithIntersectionScenario1() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierDateBasisWithIntersectionScenario1") .evaluate() - .then() - .hasGroupCount(1) + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -44,8 +49,6 @@ void cohortDateComponentCriteriaStratWithIntersectionScenario1() { .up() .up() .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } /* @@ -60,11 +63,15 @@ void cohortDateComponentCriteriaStratWithIntersectionScenario1() { @Test void cohortDateComponentCriteriaStratNoIntersectionScenario2() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario2") .evaluate() - .then() - .hasGroupCount(1) + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -83,98 +90,104 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario2() { .up() .up() .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortDateComponentCriteriaStratNoIntersectionScenario3() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario3") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario3") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-mar1-apr1-jan1-jan2") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); } @Test void cohortDateComponentCriteriaStratNoIntersectionScenario4() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario4") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + // LUKETODO: why is initial-population 0? + + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario4") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-may1-jun1-may1-jun1") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); } @Test void cohortDateComponentCriteriaStratNoIntersectionScenario5() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario5") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario5") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-jul1-aug1-sep1-oct1") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); } @Test @@ -204,7 +217,8 @@ void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { .up() .report(); - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -233,94 +247,99 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { .up() .report(); - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(2) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-cancelled-finished-arrived-planned-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(2) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(2) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(0) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(2) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(0) + .up() + .up() + .up() + .up() + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -349,7 +368,8 @@ void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { .up() .report(); - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -378,141 +398,147 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { .up() .report(); - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(1) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); - } + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(1) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); - } + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") - .evaluate() - .then() - .hasGroupCount(1) - .firstGroup() - .hasPopulationCount(1) - .firstPopulation() - .hasCount(4) - .up() - .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") - .firstStratum() - .hasPopulationCount(1) - .firstPopulation() - .hasName("initial-population") - .hasCount(1) - .up() - .up() - .up() - .up() - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") + .evaluate() + .then() + .hasGroupCount(1) + .firstGroup() + .hasPopulationCount(1) + .firstPopulation() + .hasCount(4) + .up() + .hasStratifierCount(1) + .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .firstStratum() + .hasPopulationCount(1) + .firstPopulation() + .hasName("initial-population") + .hasCount(1) + .up() + .up() + .up() + .up() + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } // LUKETODO: test that explicitly handles mismatches and asserts error handling: -// 9. 1 of n Component stratifier criteria expression has non-compliant population basis (population = Resource, Stratifier expression result is "String" or something). Throws error + // 9. 1 of n Component stratifier criteria expression has non-compliant population basis (population = Resource, + // Stratifier expression result is "String" or something). Throws error @Test void cohortBooleanComponentCriteriaStratPopulationStratExpressionMismatchEncounter() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisMismatchEncounter") - .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierBooleanBasisMismatchEncounter") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratPopulationStratExpressionMismatchDate() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisMismatchDate") - .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierEncounterBasisMismatchDate") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortDateComponentCriteriaStratPopulationStratExpressionMismatchBoolean() { final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierDateBasisMismatchBoolean") - .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); - - System.out.println(FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + .measureId("ComponentCriteriaStratifierDateBasisMismatchBoolean") + .evaluate() + .then() + .hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql index 150cc07d63..06c5de9fbc 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -106,7 +106,10 @@ define "Stratifier Jan1 Jan2": // Scenario 4: No overlap despite total intersection between strat1 and strat2 -define "Stratifier May1 Jun1": +define "Stratifier May1 Jun1 1": + { @2024-05-01, @2024-06-01 } + +define "Stratifier May1 Jun1 2": { @2024-05-01, @2024-06-01 } // Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json index c146e934b6..8c32802994 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-boolean", + "id": "stratifier-encounter-cancelled-finished-arrived-planned-boolean", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + "text": "Stratifier Stratifier Encounter Cancelled Finished Arrived Planned Boolean" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished-in-progress", + "id": "stratifier-encounter-cancelled-finished", "code" : { - "text": "Stratifier Stratifier Encounter Finished" + "text": "Stratifier Stratifier Encounter Cancelled Finished" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Boolean" + "expression": "Stratifier Encounter Cancelled Finished Boolean" } }, { "id": "stratifier-encounter-in-progress", "code" : { - "text": "Encounter In-Progress" + "text": "Encounter Arrived Planned" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Boolean" + "expression": "Stratifier Encounter Arrived Planned Boolean" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json index a13102acbc..8cb9c9dbdf 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario3.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "id": "stratifier-mar1-apr1-jan1-jan2", "code" : { - "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + "text": "Stratifier Mat1 Apr1 + Jan1 Jan2" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-comp-feb1-mar2", + "id": "stratifier-comp-mar1-apr1", "code" : { - "text": "Stratifier Feb1 Mar2" + "text": "Stratifier Mar1 Apr1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2" + "expression": "Stratifier Mar1 Apr1" } }, { - "id": "stratifier-comp-feb1-mar2-jan2", + "id": "stratifier-comp-jan1-jan2", "code" : { - "text": "Stratifier Feb1 Mar2 Jan2" + "text": "Stratifier Jan1 Jan2" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2 Jan2" + "expression": "Stratifier Jan1 Jan2" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json index 71a1f0458e..dbf1ecd5a5 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario4.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "id": "stratifier-may1-jun1-may1-jun1", "code" : { - "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + "text": "Stratifier May1 Jun1 + May1 Jun1" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-comp-feb1-mar2", + "id": "stratifier-comp-may1-jun1-1", "code" : { - "text": "Stratifier Feb1 Mar2" + "text": "Stratifier May1 Jun1 1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2" + "expression": "Stratifier May1 Jun1 1" } }, { - "id": "stratifier-comp-feb1-mar2-jan2", + "id": "stratifier-comp-may1-jun1-2", "code" : { - "text": "Stratifier Feb1 Mar2 Jan2" + "text": "Stratifier May1 Jun1 2" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2 Jan2" + "expression": "Stratifier May1 Jun1 1 2" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json index 6732c0cb09..9eddd5546f 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierDateBasisNoIntersectionScenario5.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-feb1-mar2-feb1-mar2-jan2", + "id": "stratifier-jul1-aug1-sep1-oct1", "code" : { - "text": "Stratifier Feb1 Mar2 + Feb1 Mar2 Jan2" + "text": "Stratifier Jul1 Aug1 + Sep1 Oct1" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-comp-feb1-mar2", + "id": "stratifier-comp-jul1-aug1", "code" : { - "text": "Stratifier Feb1 Mar2" + "text": "Stratifier Jul1 Aug1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2" + "expression": "Stratifier Jul1 Aug1" } }, { - "id": "stratifier-comp-feb1-mar2-jan2", + "id": "stratifier-comp-sep1-oct1", "code" : { - "text": "Stratifier Feb1 Mar2 Jan2" + "text": "Stratifier Sep1 Oct1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Feb1 Mar2 Jan2" + "expression": "Stratifier Sep1 Oct1" } } ] From 043a2f3acdcd2f2b52c31d601a011a2c69ebe937 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 16:09:51 -0400 Subject: [PATCH 39/48] Add more safety to execution. --- .../cr/measure/common/MeasureEvaluator.java | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index a427039a47..a23d84b9c9 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -22,6 +22,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -513,18 +514,35 @@ private void addStratifierComponentResult( } private void addStratifierNonComponentResult( - String subjectId, EvaluationResult evaluationResult, StratifierDef stratifierDef) { + String subjectId, EvaluationResult evaluationResult, StratifierDef stratifierDef) { var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); - Optional.ofNullable(expressionResult) - .ifPresentOrElse( - nonNullExpressionResult -> stratifierDef.putResult( - subjectId, // context of CQL expression ex: Patient based - nonNullExpressionResult.value(), - nonNullExpressionResult.evaluatedResources()), - () -> logger.warn( - "Could not find CQL expression result for stratifier expression: {}", - stratifierDef.expression())); + if (expressionResult != null) { + logger.info("expressionResult: stratifierDef.expression(): {}, value: {}, evaluatedResources: {}", stratifierDef.expression(), expressionResult.value(), expressionResult.evaluatedResources()); + } + + if (expressionResult == null) { + logger.warn( + "Could not find CQL expression result for stratifier expression: {}", + stratifierDef.expression()); + + return; + } + + final Object expressionResultValue = expressionResult.value(); + + if (expressionResultValue == null) { + logger.warn( + "CQL expression result is null for stratifier expression: {}", + stratifierDef.expression()); + + return; + } + + stratifierDef.putResult( + subjectId, + expressionResultValue, + expressionResult.evaluatedResources()); } /** @@ -664,9 +682,14 @@ private List nonComponentStratumPlural( return List.of(stratum); } - Map> subjectsByValue = subjectValues.keySet().stream() - .collect(Collectors.groupingBy( - x -> new StratumValueWrapper(subjectValues.get(x).rawValue()))); + final Map> subjectsByValue = subjectValues.entrySet() + .stream() + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().rawValue() != null) + .collect( + Collectors.groupingBy( + entry -> new StratumValueWrapper(entry.getValue().rawValue()), + Collectors.mapping(Entry::getKey, Collectors.toList()))); var stratumMultiple = new ArrayList(); From 41e9dc6c373cddfb26fe48d5b452c994089a7ab5 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 16:38:59 -0400 Subject: [PATCH 40/48] Setup more tests with correct assertions. --- .../r4/ComponentCriteriaStratifierTest.java | 20 ++++++------ .../input/cql/ComponentCriteriaStratifier.cql | 32 ++++++++++++++++--- ...erBooleanBasisNoIntersectionScenario2.json | 16 +++++----- ...erBooleanBasisNoIntersectionScenario3.json | 6 ++-- ...erBooleanBasisNoIntersectionScenario4.json | 16 +++++----- 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 29fc199692..33be4cbb48 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -224,10 +224,15 @@ void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -235,7 +240,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { .hasCount(2) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .stratifierById("stratifier-encounter-planned-triaged-arrived-cancelled-boolean") .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -246,9 +251,6 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test @@ -296,7 +298,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { .hasCount(2) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .stratifierById("stratifier-encounters-cancelled-triaged-cancelled-triaged-boolean") .firstStratum() .hasPopulationCount(1) .firstPopulation() diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql index 06c5de9fbc..1b13721b40 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -43,26 +43,48 @@ define "Stratifier Encounters Arrived In-Progress Resource": // Scenario 2: No overlap due to intersection only between initial-pop and strat2 -define "Stratifier Encounters Planned Triaged": +define "Stratifier Encounters Planned Triaged Boolean": + exists("Stratifier Encounters Planned Triaged Resource") + +define "Stratifier Encounters Planned Triaged Resource": [Encounter] E where E.status.value in { 'planned', 'triaged' } -define "Stratifier Encounters Arrived Cancelled": +define "Stratifier Encounters Arrived Cancelled Boolean": + exists("Stratifier Encounters Arrived Cancelled Resource") + +define "Stratifier Encounters Arrived Cancelled Resource": [Encounter] E where E.status.value in { 'arrived', 'cancelled' } // Scenario 3: No overlap despite total intersection between initial-pop and strat2 -define "Stratifier Encounters Cancelled Finished": +define "Stratifier Encounters Cancelled Finished Boolean": + exists("Stratifier Encounters Cancelled Finished Resource") + +define "Stratifier Encounters Cancelled Finished Resource": [Encounter] E where E.status.value in { 'cancelled', 'finished' } -define "Stratifier Encounters Arrived Planned": +define "Stratifier Encounters Arrived Planned Boolean": + exists("Stratifier Encounters Arrived Planned Resource") + +define "Stratifier Encounters Arrived Planned Resource": "Encounters Arrived Planned" // Scenario 4: No overlap despite total intersection between strat1 and strat2 -define "Stratifier Encounters Cancelled Triaged": +define "Stratifier Encounters Cancelled Triaged 1 Boolean": + exists("Stratifier Encounters Cancelled Triaged 1 Resource") + +define "Stratifier Encounters Cancelled Triaged 2 Boolean": + exists("Stratifier Encounters Cancelled Triaged 2 Resource") + +define "Stratifier Encounters Cancelled Triaged 1 Resource": + [Encounter] E + where E.status.value in { 'cancelled', 'triaged' } + +define "Stratifier Encounters Cancelled Triaged 2 Resource": [Encounter] E where E.status.value in { 'cancelled', 'triaged' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json index a2ebe7f5fa..cb2817bdc7 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-boolean", + "id": "stratifier-encounter-planned-triaged-arrived-cancelled-boolean", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + "text": "Stratifier Stratifier Encounters Planned Triaged Arrived Cancelled" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished-in-progress", + "id": "stratifier-encounter-planned-triaged", "code" : { - "text": "Stratifier Stratifier Encounter Finished" + "text": "Stratifier Stratifier Encounters Planned Triaged" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Boolean" + "expression": "Stratifier Encounters Planned Triaged Boolean" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounter-arrived-cancelled", "code" : { - "text": "Encounter In-Progress" + "text": "Stratifier Encounters Arrived Cancelled" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Boolean" + "expression": "Stratifier Encounters Arrived Cancelled Boolean" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json index 8c32802994..6776158c96 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json @@ -57,11 +57,11 @@ { "id": "stratifier-encounter-cancelled-finished", "code" : { - "text": "Stratifier Stratifier Encounter Cancelled Finished" + "text": "Stratifier Encounters Cancelled Finished" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Cancelled Finished Boolean" + "expression": "Stratifier Encounters Cancelled Finished Boolean" } }, { @@ -71,7 +71,7 @@ }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Arrived Planned Boolean" + "expression": "Stratifier Encounters Arrived Planned Boolean" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json index 9a711657df..54bc262e86 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-boolean", + "id": "stratifier-encounters-cancelled-triaged-cancelled-triaged-boolean", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + "text": "Stratifier Encounters Cancelled Triaged Cancelled Triaged Boolean" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished-in-progress", + "id": "stratifier-encounters-cancelled-triaged-1", "code" : { - "text": "Stratifier Stratifier Encounter Finished" + "text": "Stratifier Encounters Cancelled Triaged 1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Boolean" + "expression": "Stratifier Encounters Cancelled Triaged 1 Boolean" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-cancelled-triaged-2", "code" : { - "text": "Encounter In-Progress" + "text": "Stratifier Encounters Cancelled Triaged 2" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Boolean" + "expression": "Stratifier Encounters Cancelled Triaged 2 Boolean" } } ] From 17ef8fa4253531e2c11da37b5b562e629dd63c6f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 30 Oct 2025 16:58:38 -0400 Subject: [PATCH 41/48] More tweaks to tests. --- .../r4/ComponentCriteriaStratifierTest.java | 115 ++++++++++-------- .../input/cql/ComponentCriteriaStratifier.cql | 10 +- ...erBooleanBasisNoIntersectionScenario5.json | 16 +-- 3 files changed, 81 insertions(+), 60 deletions(-) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 33be4cbb48..718f9ae16e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -210,6 +210,7 @@ void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") + // LUKETODO: maybe this is correct? debug to be sure .hasCount(1) .up() .up() @@ -287,10 +288,15 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -309,18 +315,20 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -328,7 +336,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { .hasCount(2) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-boolean") + .stratifierById("stratifier-encounters-cancelled-in-progress-finished-triaged-boolean") .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -339,18 +347,20 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -369,18 +379,20 @@ void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -399,18 +411,20 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -429,18 +443,20 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -459,18 +475,20 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { - final MeasureReport report = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") - .evaluate() - .then() + final SelectedReport then = GIVEN.when() + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") + .evaluate() + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then .hasGroupCount(1) .firstGroup() .hasPopulationCount(1) @@ -489,9 +507,6 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } // LUKETODO: test that explicitly handles mismatches and asserts error handling: diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql index 1b13721b40..a1f8c0b99e 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/cql/ComponentCriteriaStratifier.cql @@ -90,11 +90,17 @@ define "Stratifier Encounters Cancelled Triaged 2 Resource": // Scenario 5: No overlap because init-pop, strat1, and strat2 have zero overlap with all -define "Stratifier Encounters Cancelled In-Progress": +define "Stratifier Encounters Cancelled In-Progress Boolean": + exists("Stratifier Encounters Cancelled In-Progress Resource") + +define "Stratifier Encounters Finished Triaged Boolean": + exists("Stratifier Encounters Finished Triaged Resource") + +define "Stratifier Encounters Cancelled In-Progress Resource": [Encounter] E where E.status.value in { 'cancelled', 'in-progress' } -define "Stratifier Encounters Finished Triaged": +define "Stratifier Encounters Finished Triaged Resource": [Encounter] E where E.status.value in { 'finished', 'triaged' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json index b14f16c435..8d6073c324 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-boolean", + "id": "stratifier-encounters-cancelled-in-progress-finished-triaged-boolean", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Boolean" + "text": "Stratifier Encounters Cancelled In-Progress Finished Triaged Boolean" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished-in-progress", + "id": "stratifier-encounters-cancelled-in-progress", "code" : { - "text": "Stratifier Stratifier Encounter Finished" + "text": "Stratifier Encounters Cancelled In-Progress" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Boolean" + "expression": "Stratifier Encounters Cancelled In-Progress Boolean" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-finished-triaged", "code" : { - "text": "Encounter In-Progress" + "text": "Stratifier Encounters Finished Triaged" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Boolean" + "expression": "Stratifier Encounters Finished Triaged Boolean" } } ] From 8857091507c80c4bbbecc53517a65f3a1ce1f40d Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 09:56:25 -0400 Subject: [PATCH 42/48] Get tests in a sufficient state that it's time to fix the production code. --- .../r4/ComponentCriteriaStratifierTest.java | 178 +++++++++--------- ...erBooleanBasisNoIntersectionScenario3.json | 2 +- ...EncounterBasisNoIntersectionScenario2.json | 16 +- ...EncounterBasisNoIntersectionScenario3.json | 16 +- ...EncounterBasisNoIntersectionScenario4.json | 16 +- ...EncounterBasisNoIntersectionScenario5.json | 16 +- 6 files changed, 126 insertions(+), 118 deletions(-) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java index 718f9ae16e..9275a78666 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifierTest.java @@ -1,7 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.r4; import ca.uhn.fhir.context.FhirContext; -import org.hl7.fhir.r4.model.MeasureReport; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; import org.opencds.cqf.fhir.cr.measure.r4.Measure.SelectedReport; @@ -39,6 +38,7 @@ void cohortDateComponentCriteriaStratWithIntersectionScenario1() { .up() .hasStratifierCount(1) .stratifierById("stratifier-feb1-jan2-feb3-jan2") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -80,6 +80,7 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario2() { .up() .hasStratifierCount(1) .stratifierById("stratifier-feb1-mar2-feb1-mar2-jan2") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -112,6 +113,7 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario3() { .up() .hasStratifierCount(1) .stratifierById("stratifier-mar1-apr1-jan1-jan2") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -127,8 +129,6 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario3() { @Test void cohortDateComponentCriteriaStratNoIntersectionScenario4() { - // LUKETODO: why is initial-population 0? - final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierDateBasisNoIntersectionScenario4") .evaluate() @@ -146,6 +146,7 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario4() { .up() .hasStratifierCount(1) .stratifierById("stratifier-may1-jun1-may1-jun1") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -178,6 +179,7 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario5() { .up() .hasStratifierCount(1) .stratifierById("stratifier-jul1-aug1-sep1-oct1") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -193,11 +195,15 @@ void cohortDateComponentCriteriaStratNoIntersectionScenario5() { @Test void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierBooleanBasisWithIntersectionScenario1") .evaluate() - .then() - .hasGroupCount(1) + .then(); + + System.out.println( + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -206,35 +212,32 @@ void cohortBooleanComponentCriteriaStratWithIntersectionScenario1() { .up() .hasStratifierCount(1) .stratifierById("stratifier-encounters-arrived-triaged-arrived-in-progress-boolean") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") - // LUKETODO: maybe this is correct? debug to be sure + // LUKETODO: maybe this is correct? debug to be sure .hasCount(1) .up() .up() .up() .up() .report(); - - System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); } @Test void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario2") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -242,6 +245,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario2() { .up() .hasStratifierCount(1) .stratifierById("stratifier-encounter-planned-triaged-arrived-cancelled-boolean") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -272,7 +276,8 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { .hasCount(2) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-cancelled-finished-arrived-planned-boolean") + .stratifierById("stratifier-encounters-cancelled-finished-arrived-planned-boolean") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -289,15 +294,14 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario3() { void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario4") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -305,6 +309,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { .up() .hasStratifierCount(1) .stratifierById("stratifier-encounters-cancelled-triaged-cancelled-triaged-boolean") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -321,15 +326,14 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario4() { void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario5") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -337,6 +341,7 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { .up() .hasStratifierCount(1) .stratifierById("stratifier-encounters-cancelled-in-progress-finished-triaged-boolean") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -353,15 +358,14 @@ void cohortBooleanComponentCriteriaStratNoIntersectionScenario5() { void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierEncounterBasisWithIntersectionScenario1") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() @@ -369,6 +373,7 @@ void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { .up() .hasStratifierCount(1) .stratifierById("stratifier-encounters-arrived-triaged-arrived-in-progress-resource") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() @@ -385,27 +390,27 @@ void cohortEncounterComponentCriteriaStratWithIntersectionScenario1() { void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .stratifierById("stratifier-encounters-planned-triaged-arrived-cancelled") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") - .hasCount(1) + .hasCount(0) .up() .up() .up() @@ -417,27 +422,27 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario2() { void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .stratifierById("stratifier-encounters-cancelled-finished-arrived-planned") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") - .hasCount(1) + .hasCount(0) .up() .up() .up() @@ -449,27 +454,27 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario3() { void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .stratifierById("stratifier-encounters-cancelled-triaged-cancelled-triaged") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") - .hasCount(1) + .hasCount(0) .up() .up() .up() @@ -481,27 +486,27 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario4() { void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { final SelectedReport then = GIVEN.when() - .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") - .evaluate() - .then(); + .measureId("ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5") + .evaluate() + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); - then - .hasGroupCount(1) + then.hasGroupCount(1) .firstGroup() .hasPopulationCount(1) .firstPopulation() .hasCount(4) .up() .hasStratifierCount(1) - .stratifierById("stratifier-encounter-finished-in-progress-encounter") + .stratifierById("stratifier-encounters-cancelled-in-progress-finished-triaged") + .hasStratumCount(1) .firstStratum() .hasPopulationCount(1) .firstPopulation() .hasName("initial-population") - .hasCount(1) + .hasCount(0) .up() .up() .up() @@ -517,45 +522,48 @@ void cohortEncounterComponentCriteriaStratNoIntersectionScenario5() { @Test void cohortBooleanComponentCriteriaStratPopulationStratExpressionMismatchEncounter() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierBooleanBasisMismatchEncounter") .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); } @Test void cohortEncounterComponentCriteriaStratPopulationStratExpressionMismatchDate() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierEncounterBasisMismatchDate") .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); } @Test void cohortDateComponentCriteriaStratPopulationStratExpressionMismatchBoolean() { - final MeasureReport report = GIVEN.when() + final SelectedReport then = GIVEN.when() .measureId("ComponentCriteriaStratifierDateBasisMismatchBoolean") .evaluate() - .then() - .hasContainedOperationOutcome() - .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") - .report(); + .then(); System.out.println( - FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(report)); + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(then.report())); + + then.hasContainedOperationOutcome() + .hasContainedOperationOutcomeMsg("Mismatch between population basis and stratifier criteria expression") + .report(); } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json index 6776158c96..b52c67e70a 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierBooleanBasisNoIntersectionScenario3.json @@ -43,7 +43,7 @@ ], "stratifier": [ { - "id": "stratifier-encounter-cancelled-finished-arrived-planned-boolean", + "id": "stratifier-encounters-cancelled-finished-arrived-planned-boolean", "code" : { "text": "Stratifier Stratifier Encounter Cancelled Finished Arrived Planned Boolean" }, diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json index 6053950de8..1680750a4a 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario2.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-encounter", + "id": "stratifier-encounters-planned-triaged-arrived-cancelled", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + "text": "Stratifier Encounters Planned Triaged Arrived Cancelled Resource" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished", + "id": "stratifier-encounters-planned-triaged", "code" : { - "text": "Stratifier Encounter Finished" + "text": "Stratifier Encounters Planned Triaged" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Resource" + "expression": "Stratifier Encounters Planned Triaged Resource" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-arrived-cancelled", "code" : { - "text": "Stratifier Encounter In-Progress" + "text": "Stratifier Encounters Arrived Cancelled" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Resource" + "expression": "Stratifier Encounter Arrived Cancelled Resource" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json index 31eb0ea25a..9495e94dbe 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario3.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-encounter", + "id": "stratifier-encounters-cancelled-finished-arrived-planned", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + "text": "Stratifier Encounters Cancelled Finished Arrived Planned Resource" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished", + "id": "stratifier-encounters-cancelled-finished", "code" : { - "text": "Stratifier Encounter Finished" + "text": "Stratifier Encounters Cancelled Finished" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Resource" + "expression": "Stratifier Encounters Cancelled Finished Resource" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-arrived-planned", "code" : { - "text": "Stratifier Encounter In-Progress" + "text": "Stratifier Encounters Arrived Planned" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Resource" + "expression": "Stratifier Encounter Arrived Planned Resource" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json index 6c38a832a7..fb3858f1a3 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario4.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-encounter", + "id": "stratifier-encounters-cancelled-triaged-cancelled-triaged", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + "text": "Stratifier Encounters Cancelled Triaged Cancelled Triaged" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished", + "id": "stratifier-encounters-cancelled-triaged-1", "code" : { - "text": "Stratifier Encounter Finished" + "text": "Stratifier Encounters Cancelled Triaged 1" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Resource" + "expression": "Stratifier Encounters Cancelled Triaged 1 Resource" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-cancelled-triaged-2", "code" : { - "text": "Stratifier Encounter In-Progress" + "text": "Stratifier Encounters Cancelled Triaged 2" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Resource" + "expression": "Stratifier Encounters Cancelled Triaged 2 Resource" } } ] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json index 6edb653f5f..ae230f2cf0 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/ComponentCriteriaStratifier/input/resources/measure/ComponentCriteriaStratifierEncounterBasisNoIntersectionScenario5.json @@ -43,9 +43,9 @@ ], "stratifier": [ { - "id": "stratifier-encounter-finished-in-progress-encounter", + "id": "stratifier-encounters-cancelled-in-progress-finished-triaged", "code" : { - "text": "Stratifier Stratifier Encounter Finished In-Progress Resource" + "text": "Stratifier Encounters Cancelled In-Progress Finished Triaged" }, "extension": [ { @@ -55,23 +55,23 @@ ], "component": [ { - "id": "stratifier-encounter-finished", + "id": "stratifier-encounters-cancelled-in-progress", "code" : { - "text": "Stratifier Encounter Finished" + "text": "Stratifier Encounters Cancelled In-Progress" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter Finished Resource" + "expression": "Stratifier Encounters Cancelled In-Progress Resource" } }, { - "id": "stratifier-encounter-in-progress", + "id": "stratifier-encounters-finished-triaged", "code" : { - "text": "Stratifier Encounter In-Progress" + "text": "Stratifier Encounters Finished Triaged" }, "criteria": { "language": "text/cql.identifier", - "expression": "Stratifier Encounter In-Progress Resource" + "expression": "Stratifier Encounters Finished Triaged Resource" } } ] From 36ec54319019a36fba4fabb632de430738261128 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 10:00:00 -0400 Subject: [PATCH 43/48] Spotless. --- .../cr/measure/common/MeasureEvaluator.java | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index a23d84b9c9..65c4344e69 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -514,17 +514,20 @@ private void addStratifierComponentResult( } private void addStratifierNonComponentResult( - String subjectId, EvaluationResult evaluationResult, StratifierDef stratifierDef) { + String subjectId, EvaluationResult evaluationResult, StratifierDef stratifierDef) { var expressionResult = evaluationResult.forExpression(stratifierDef.expression()); if (expressionResult != null) { - logger.info("expressionResult: stratifierDef.expression(): {}, value: {}, evaluatedResources: {}", stratifierDef.expression(), expressionResult.value(), expressionResult.evaluatedResources()); + logger.info( + "expressionResult: stratifierDef.expression(): {}, value: {}, evaluatedResources: {}", + stratifierDef.expression(), + expressionResult.value(), + expressionResult.evaluatedResources()); } if (expressionResult == null) { logger.warn( - "Could not find CQL expression result for stratifier expression: {}", - stratifierDef.expression()); + "Could not find CQL expression result for stratifier expression: {}", stratifierDef.expression()); return; } @@ -532,17 +535,12 @@ private void addStratifierNonComponentResult( final Object expressionResultValue = expressionResult.value(); if (expressionResultValue == null) { - logger.warn( - "CQL expression result is null for stratifier expression: {}", - stratifierDef.expression()); + logger.warn("CQL expression result is null for stratifier expression: {}", stratifierDef.expression()); return; } - stratifierDef.putResult( - subjectId, - expressionResultValue, - expressionResult.evaluatedResources()); + stratifierDef.putResult(subjectId, expressionResultValue, expressionResult.evaluatedResources()); } /** @@ -682,14 +680,12 @@ private List nonComponentStratumPlural( return List.of(stratum); } - final Map> subjectsByValue = subjectValues.entrySet() - .stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> entry.getValue().rawValue() != null) - .collect( - Collectors.groupingBy( - entry -> new StratumValueWrapper(entry.getValue().rawValue()), - Collectors.mapping(Entry::getKey, Collectors.toList()))); + final Map> subjectsByValue = subjectValues.entrySet().stream() + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().rawValue() != null) + .collect(Collectors.groupingBy( + entry -> new StratumValueWrapper(entry.getValue().rawValue()), + Collectors.mapping(Entry::getKey, Collectors.toList()))); var stratumMultiple = new ArrayList(); From 1279b369133d98453edfe7fd3c96d60d1ba597a8 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 10:19:29 -0400 Subject: [PATCH 44/48] Add toString() to Def classes. --- .../fhir/cr/measure/common/ConceptDef.java | 25 ++++++++----------- .../common/StratifierComponentDef.java | 11 ++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ConceptDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ConceptDef.java index 09514a1da4..7cbb9c1409 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ConceptDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/ConceptDef.java @@ -1,20 +1,10 @@ package org.opencds.cqf.fhir.cr.measure.common; +import jakarta.annotation.Nonnull; import java.util.List; +import java.util.StringJoiner; -public class ConceptDef { - - private final List codes; - private final String text; - - public ConceptDef(List codes, String text) { - this.codes = codes; - this.text = text; - } - - public List codes() { - return this.codes; - } +public record ConceptDef(List codes, String text) { public boolean isEmpty() { return this.codes.isEmpty(); @@ -28,7 +18,12 @@ public CodeDef first() { return this.codes.get(0); } - public String text() { - return this.text; + @Override + @Nonnull + public String toString() { + return new StringJoiner(", ", ConceptDef.class.getSimpleName() + "[", "]") + .add("codes=" + codes) + .add("text='" + text + "'") + .toString(); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierComponentDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierComponentDef.java index 53a7a01590..5f78312f59 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierComponentDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratifierComponentDef.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; public class StratifierComponentDef { private final String id; @@ -40,4 +41,14 @@ public Map getResults() { return this.results; } + + @Override + public String toString() { + return new StringJoiner(", ", StratifierComponentDef.class.getSimpleName() + "[", "]") + .add("id='" + id + "'") + .add("code=" + code) + .add("expression='" + expression + "'") + .add("results=" + results) + .toString(); + } } From cd4b491802b8cc1476290a86c26fcb3375a9686b Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 10:23:04 -0400 Subject: [PATCH 45/48] javadoc. --- .../fhir/cr/measure/common/StratumPopulationDef.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java index 8f5c547ba7..193171bd8f 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -24,12 +24,18 @@ public String getId() { return id; } - // LUKETODO: javadoc + /** + * @return The subjectIds as they are, whether they are qualified with a resource + * (ex: [Patient/pat1, Patient/pat2] or [pat1, pat2] + */ public Set getSubjectsQualifiedOrUnqualified() { return subjectsQualifiedOrUnqualified; } - // LUKETODO: javadoc + /** + * @return The subjectIds without a FHIR resource qualifier, whether they previously had a + * qualifier or not + */ public Set getSubjectsUnqualified() { return subjectsQualifiedOrUnqualified.stream() .filter(Objects::nonNull) From 5fd709fef45a2f2db9ec020d05af382d368ebb14 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 11:51:18 -0400 Subject: [PATCH 46/48] Start moving population/stratum population intersection code from R4StratifierBuilder to MeasureEvaluator. This change hasn't yet been activated. Also, move "post evaluation" multi-subject MeasureDef code to a new dedicated class. More TODOs. --- .../MeasureEvaluationResultHandler.java | 2 +- .../cr/measure/common/MeasureEvaluator.java | 246 ------------ .../common/MeasureMultiSubjectEvaluator.java | 376 ++++++++++++++++++ .../fhir/cr/measure/common/StratumDef.java | 2 + .../measure/common/StratumPopulationDef.java | 13 + .../cr/measure/r4/R4StratifierBuilder.java | 24 +- 6 files changed, 414 insertions(+), 249 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java index b6475c16ec..b35401429b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java @@ -69,7 +69,7 @@ public static void processResults( } } - evaluator.postEvaluation(measureDef); + MeasureMultiSubjectEvaluator.postEvaluationMultiSubject(measureDef); } /** diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 65c4344e69..395b3a3d9c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -12,28 +12,16 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.NUMERATOREXCLUSION; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Sets; -import com.google.common.collect.Table; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import org.hl7.fhir.r4.model.CodeableConcept; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; -import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureScoringTypePopulations; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -542,238 +530,4 @@ private void addStratifierNonComponentResult( stratifierDef.putResult(subjectId, expressionResultValue, expressionResult.evaluatedResources()); } - - /** - * Take the accumulated subject-by-subject evaluation results and use it to build StratumDefs - * and StratumPopulationDefs - * - * @param measureDef to mutate post-evaluation with results of initial stratifier - * subject-by-subject accumulations. - * - */ - public void postEvaluation(MeasureDef measureDef) { - - for (GroupDef groupDef : measureDef.groups()) { - for (StratifierDef stratifierDef : groupDef.stratifiers()) { - final List stratumDefs; - - if (!stratifierDef.components().isEmpty()) { - stratumDefs = componentStratumPlural(stratifierDef, groupDef.populations()); - } else { - stratumDefs = nonComponentStratumPlural(stratifierDef, groupDef.populations()); - } - - stratifierDef.addAllStratum(stratumDefs); - } - } - } - - private StratumDef buildStratumDef( - StratifierDef stratifierDef, - Set values, - List subjectIds, - List populationDefs) { - - boolean isComponent = values.size() > 1; - String stratumText = null; - - for (StratumValueDef valuePair : values) { - StratumValueWrapper value = valuePair.value(); - var componentDef = valuePair.def(); - // Set Stratum value to indicate which value is displaying results - // ex. for Gender stratifier, code 'Male' - if (value.getValueClass().equals(CodeableConcept.class)) { - if (isComponent) { - // component stratifier example: code: "gender", value: 'M' - // value being stratified: 'M' - stratumText = componentDef.code().text(); - } else { - // non-component stratifiers only set stratified value, code is set on stratifier object - // value being stratified: 'M' - if (value.getValue() instanceof CodeableConcept codeableConcept) { - stratumText = codeableConcept.getText(); - } - } - } else if (isComponent) { - stratumText = expressionResultToCodableConcept(value).getText(); - } else if (MeasureStratifierType.VALUE == stratifierDef.getStratifierType()) { - // non-component stratifiers only set stratified value, code is set on stratifier object - // value being stratified: 'M' - stratumText = expressionResultToCodableConcept(value).getText(); - } - } - - return new StratumDef( - stratumText, - populationDefs.stream() - .map(popDef -> buildStratumPopulationDef(popDef, subjectIds)) - .toList(), - values, - subjectIds); - } - - private static StratumPopulationDef buildStratumPopulationDef( - PopulationDef populationDef, List subjectIds) { - - var popSubjectIds = populationDef.getSubjects().stream() - .map(R4ResourceIdUtils::addPatientQualifier) - .collect(Collectors.toUnmodifiableSet()); - - var qualifiedSubjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); - - return new StratumPopulationDef(populationDef.id(), qualifiedSubjectIdsCommonToPopulation); - } - - private List componentStratumPlural(StratifierDef stratifierDef, List populationDefs) { - - final Table subjectResultTable = - buildSubjectResultsTable(stratifierDef.components()); - - // Stratifiers should be of the same basis as population - // Split subjects by result values - // ex. all Male Patients and all Female Patients - - var componentSubjects = groupSubjectsByValueDefSet(subjectResultTable); - - var stratumDefs = new ArrayList(); - - componentSubjects.forEach((valueSet, subjects) -> { - // converts table into component value combinations - // | Stratum | Set | List | - // | --------- | ----------------------- | ---------------------- | - // | Stratum-1 | <'M','White> | [subject-a] | - // | Stratum-2 | <'F','hispanic/latino'> | [subject-b] | - // | Stratum-3 | <'M','hispanic/latino'> | [subject-c] | - // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | - - var stratumDef = buildStratumDef(stratifierDef, valueSet, subjects, populationDefs); - - stratumDefs.add(stratumDef); - }); - - return stratumDefs; - } - - private List nonComponentStratumPlural( - StratifierDef stratifierDef, List populationDefs) { - // standard Stratifier - // one criteria expression defined, one set of criteria results - - // standard Stratifier - // one criteria expression defined, one set of criteria results - final Map subjectValues = stratifierDef.getResults(); - - // nonComponent stratifiers will have a single expression that can generate results, instead of grouping - // combinations of results - // example: 'gender' expression could produce values of 'M', 'F' - // subject1: 'gender'--> 'M' - // subject2: 'gender'--> 'F' - // stratifier criteria results are: 'M', 'F' - - if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { - // Seems to be irrelevant for criteria based stratifiers - var stratValues = Set.of(); - // Seems to be irrelevant for criteria based stratifiers - var patients = List.of(); - - var stratum = buildStratumDef(stratifierDef, stratValues, patients, populationDefs); - return List.of(stratum); - } - - final Map> subjectsByValue = subjectValues.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> entry.getValue().rawValue() != null) - .collect(Collectors.groupingBy( - entry -> new StratumValueWrapper(entry.getValue().rawValue()), - Collectors.mapping(Entry::getKey, Collectors.toList()))); - - var stratumMultiple = new ArrayList(); - - // Stratum 1 - // Value: 'M'--> subjects: subject1 - // Stratum 2 - // Value: 'F'--> subjects: subject2 - // loop through each value key - for (Map.Entry> stratValue : subjectsByValue.entrySet()) { - // patch Patient values with prefix of ResourceType to match with incoming population subjects for stratum - // TODO: should match context of CQL, not only Patient - var patientsSubjects = stratValue.getValue().stream() - .map(R4ResourceIdUtils::addPatientQualifier) - .toList(); - // build the stratum for each unique value - // non-component stratifiers will populate a 'null' for componentStratifierDef, since it doesn't have - // multiple criteria - // TODO: build out nonComponent stratum method - Set stratValues = Set.of(new StratumValueDef(stratValue.getKey(), null)); - var stratum = buildStratumDef(stratifierDef, stratValues, patientsSubjects, populationDefs); - stratumMultiple.add(stratum); - } - - return stratumMultiple; - } - - private Table buildSubjectResultsTable( - List componentDefs) { - - final Table subjectResultTable = HashBasedTable.create(); - - // Component Stratifier - // one or more criteria expression defined, one set of criteria results per component specified - // results of component stratifier are an intersection of membership to both component result sets - - componentDefs.forEach(componentDef -> componentDef.getResults().forEach((subject, result) -> { - StratumValueWrapper stratumValueWrapper = new StratumValueWrapper(result.rawValue()); - subjectResultTable.put(R4ResourceIdUtils.addPatientQualifier(subject), stratumValueWrapper, componentDef); - })); - - return subjectResultTable; - } - - private static Map, List> groupSubjectsByValueDefSet( - Table table) { - // input format - // | Subject (String) | CriteriaResult (ValueWrapper) | StratifierComponentDef | - // | ---------------- | ----------------------------- | ---------------------- | - // | subject-a | M | gender | - // | subject-b | F | gender | - // | subject-c | M | gender | - // | subject-d | F | gender | - // | subject-e | F | gender | - // | subject-a | white | race | - // | subject-b | hispanic/latino | race | - // | subject-c | hispanic/latino | race | - // | subject-d | black | race | - // | subject-e | black | race | - - // Step 1: Build Map> - final Map> subjectToValueDefs = new HashMap<>(); - - for (Table.Cell cell : table.cellSet()) { - subjectToValueDefs - .computeIfAbsent(cell.getRowKey(), k -> new HashSet<>()) - .add(new StratumValueDef(cell.getColumnKey(), cell.getValue())); - } - // output format: - // | Set | List | - // | ----------------------- | ---------------------- | - // | <'M','White> | [subject-a] | - // | <'F','hispanic/latino'> | [subject-b] | - // | <'M','hispanic/latino'> | [subject-c] | - // | <'F','black'> | [subject-d, subject-e] | - - // Step 2: Invert to Map, List> - return subjectToValueDefs.entrySet().stream() - .collect(Collectors.groupingBy( - Map.Entry::getValue, - Collector.of(ArrayList::new, (list, e) -> list.add(e.getKey()), (l1, l2) -> { - l1.addAll(l2); - return l1; - }))); - } - - // This is weird pattern where we have multiple qualifying values within a single stratum, - // which was previously unsupported. So for now, comma-delim the first five values. - private static CodeableConcept expressionResultToCodableConcept(StratumValueWrapper value) { - return new CodeableConcept().setText(value.getValueAsString()); - } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java new file mode 100644 index 0000000000..1ec4392d20 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java @@ -0,0 +1,376 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; +import com.google.common.collect.Table; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.ResourceType; +import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +// LUKETODO: javadoc +public class MeasureMultiSubjectEvaluator { + + /** + * Take the accumulated subject-by-subject evaluation results and use it to build StratumDefs + * and StratumPopulationDefs + * + * @param measureDef to mutate post-evaluation with results of initial stratifier + * subject-by-subject accumulations. + * + */ + public static void postEvaluationMultiSubject(MeasureDef measureDef) { + + for (GroupDef groupDef : measureDef.groups()) { + for (StratifierDef stratifierDef : groupDef.stratifiers()) { + final List stratumDefs; + + if (!stratifierDef.components().isEmpty()) { + stratumDefs = componentStratumPlural( + stratifierDef, + groupDef.getPopulationBasis(), + groupDef.populations()); + } else { + stratumDefs = nonComponentStratumPlural( + stratifierDef, + groupDef.getPopulationBasis(), + groupDef.populations()); + } + + stratifierDef.addAllStratum(stratumDefs); + } + } + } + + private static StratumDef buildStratumDef( + StratifierDef stratifierDef, + Set values, + List subjectIds, + CodeDef populationBasis, + List populationDefs) { + + boolean isComponent = values.size() > 1; + String stratumText = null; + + for (StratumValueDef valuePair : values) { + StratumValueWrapper value = valuePair.value(); + var componentDef = valuePair.def(); + // Set Stratum value to indicate which value is displaying results + // ex. for Gender stratifier, code 'Male' + if (value.getValueClass().equals(CodeableConcept.class)) { + if (isComponent) { + // component stratifier example: code: "gender", value: 'M' + // value being stratified: 'M' + stratumText = componentDef.code().text(); + } else { + // non-component stratifiers only set stratified value, code is set on stratifier object + // value being stratified: 'M' + if (value.getValue() instanceof CodeableConcept codeableConcept) { + stratumText = codeableConcept.getText(); + } + } + } else if (isComponent) { + stratumText = expressionResultToCodableConcept(value).getText(); + } else if (MeasureStratifierType.VALUE == stratifierDef.getStratifierType()) { + // non-component stratifiers only set stratified value, code is set on stratifier object + // value being stratified: 'M' + stratumText = expressionResultToCodableConcept(value).getText(); + } + } + + return new StratumDef( + stratumText, + populationDefs.stream() + .map(popDef -> buildStratumPopulationDef( + stratifierDef.getStratifierType(), + stratifierDef.getAllCriteriaResultValues(), + populationBasis, + popDef, + subjectIds)) + .toList(), + values, + subjectIds); + } + + private static StratumPopulationDef buildStratumPopulationDef( + MeasureStratifierType measureStratifierType, + Set evaluationResultsForStratifier, + CodeDef groupPopulationBasis, + PopulationDef populationDef, + List subjectIds) { + + var popSubjectIds = populationDef.getSubjects().stream() + .map(R4ResourceIdUtils::addPatientQualifier) + .collect(Collectors.toUnmodifiableSet()); + + var qualifiedSubjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); + + final StratumPopulationDef stratumPopulationDef = + new StratumPopulationDef( + populationDef.id(), + qualifiedSubjectIdsCommonToPopulation); + + final int stratumCount = getStratumCountUpper( + measureStratifierType, + evaluationResultsForStratifier, + populationDef, + getResourceIds(subjectIds, groupPopulationBasis, populationDef)); + + stratumPopulationDef.setCount(stratumCount); + + return stratumPopulationDef; + } + + private static List componentStratumPlural(StratifierDef stratifierDef, + CodeDef populationBasis, List populationDefs) { + + final Table subjectResultTable = + buildSubjectResultsTable(stratifierDef.components()); + + // Stratifiers should be of the same basis as population + // Split subjects by result values + // ex. all Male Patients and all Female Patients + + var componentSubjects = groupSubjectsByValueDefSet(subjectResultTable); + + var stratumDefs = new ArrayList(); + + componentSubjects.forEach((valueSet, subjects) -> { + // converts table into component value combinations + // | Stratum | Set | List | + // | --------- | ----------------------- | ---------------------- | + // | Stratum-1 | <'M','White> | [subject-a] | + // | Stratum-2 | <'F','hispanic/latino'> | [subject-b] | + // | Stratum-3 | <'M','hispanic/latino'> | [subject-c] | + // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | + + var stratumDef = buildStratumDef(stratifierDef, valueSet, subjects, populationBasis, + populationDefs); + + stratumDefs.add(stratumDef); + }); + + return stratumDefs; + } + + private static List nonComponentStratumPlural( + StratifierDef stratifierDef, CodeDef populationBasis, List populationDefs) { + // standard Stratifier + // one criteria expression defined, one set of criteria results + + // standard Stratifier + // one criteria expression defined, one set of criteria results + final Map subjectValues = stratifierDef.getResults(); + + // nonComponent stratifiers will have a single expression that can generate results, instead of grouping + // combinations of results + // example: 'gender' expression could produce values of 'M', 'F' + // subject1: 'gender'--> 'M' + // subject2: 'gender'--> 'F' + // stratifier criteria results are: 'M', 'F' + + if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { + // Seems to be irrelevant for criteria based stratifiers + var stratValues = Set.of(); + // Seems to be irrelevant for criteria based stratifiers + var patients = List.of(); + + var stratum = buildStratumDef(stratifierDef, stratValues, patients, populationBasis, + populationDefs); + return List.of(stratum); + } + + final Map> subjectsByValue = subjectValues.entrySet().stream() + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().rawValue() != null) + .collect(Collectors.groupingBy( + entry -> new StratumValueWrapper(entry.getValue().rawValue()), + Collectors.mapping(Entry::getKey, Collectors.toList()))); + + var stratumMultiple = new ArrayList(); + + // Stratum 1 + // Value: 'M'--> subjects: subject1 + // Stratum 2 + // Value: 'F'--> subjects: subject2 + // loop through each value key + for (Map.Entry> stratValue : subjectsByValue.entrySet()) { + // patch Patient values with prefix of ResourceType to match with incoming population subjects for stratum + // TODO: should match context of CQL, not only Patient + var patientsSubjects = stratValue.getValue().stream() + .map(R4ResourceIdUtils::addPatientQualifier) + .toList(); + // build the stratum for each unique value + // non-component stratifiers will populate a 'null' for componentStratifierDef, since it doesn't have + // multiple criteria + // TODO: build out nonComponent stratum method + Set stratValues = Set.of(new StratumValueDef(stratValue.getKey(), null)); + var stratum = buildStratumDef( + stratifierDef, + stratValues, + patientsSubjects, + populationBasis, + populationDefs); + stratumMultiple.add(stratum); + } + + return stratumMultiple; + } + + private static Table buildSubjectResultsTable( + List componentDefs) { + + final Table subjectResultTable = HashBasedTable.create(); + + // Component Stratifier + // one or more criteria expression defined, one set of criteria results per component specified + // results of component stratifier are an intersection of membership to both component result sets + + componentDefs.forEach(componentDef -> componentDef.getResults().forEach((subject, result) -> { + StratumValueWrapper stratumValueWrapper = new StratumValueWrapper(result.rawValue()); + subjectResultTable.put(R4ResourceIdUtils.addPatientQualifier(subject), stratumValueWrapper, componentDef); + })); + + return subjectResultTable; + } + + private static Map, List> groupSubjectsByValueDefSet( + Table table) { + // input format + // | Subject (String) | CriteriaResult (ValueWrapper) | StratifierComponentDef | + // | ---------------- | ----------------------------- | ---------------------- | + // | subject-a | M | gender | + // | subject-b | F | gender | + // | subject-c | M | gender | + // | subject-d | F | gender | + // | subject-e | F | gender | + // | subject-a | white | race | + // | subject-b | hispanic/latino | race | + // | subject-c | hispanic/latino | race | + // | subject-d | black | race | + // | subject-e | black | race | + + // Step 1: Build Map> + final Map> subjectToValueDefs = new HashMap<>(); + + for (Table.Cell cell : table.cellSet()) { + subjectToValueDefs + .computeIfAbsent(cell.getRowKey(), k -> new HashSet<>()) + .add(new StratumValueDef(cell.getColumnKey(), cell.getValue())); + } + // output format: + // | Set | List | + // | ----------------------- | ---------------------- | + // | <'M','White> | [subject-a] | + // | <'F','hispanic/latino'> | [subject-b] | + // | <'M','hispanic/latino'> | [subject-c] | + // | <'F','black'> | [subject-d, subject-e] | + + // Step 2: Invert to Map, List> + return subjectToValueDefs.entrySet().stream() + .collect(Collectors.groupingBy( + Map.Entry::getValue, + Collector.of(ArrayList::new, (list, e) -> list.add(e.getKey()), (l1, l2) -> { + l1.addAll(l2); + return l1; + }))); + } + + // This is weird pattern where we have multiple qualifying values within a single stratum, + // which was previously unsupported. So for now, comma-delim the first five values. + private static CodeableConcept expressionResultToCodableConcept(StratumValueWrapper value) { + return new CodeableConcept().setText(value.getValueAsString()); + } + + private static int getStratumCountUpper( + MeasureStratifierType measureStratifierType, + Set evaluationResults, + PopulationDef populationDef, + List resourceIds) { + + if (MeasureStratifierType.CRITERIA == measureStratifierType) { + final Set resources = populationDef.getResources(); + // LUKETODO: for the component criteria scenario, we don't add the results directly to the stratifierDef, + // but to each of the component defs, which is why this is empty + if (resources.isEmpty() || evaluationResults.isEmpty()) { + // There's no intersection, so no point in going further. + return 0; + } + + final Class resourcesClassFirst = resources.iterator().next().getClass(); + final Class resultClassFirst = evaluationResults.iterator().next().getClass(); + + // Sanity check: isCriteriaBasedStratifier() should have filtered this out + if (resourcesClassFirst != resultClassFirst) { + // Different classes, so no point in going further. + return 0; + } + + final SetView intersection = Sets.intersection(resources, evaluationResults); + return intersection.size(); + } + + if (resourceIds.isEmpty()) { + return 0; + } + + return resourceIds.size(); + } + + @Nonnull + private static List getResourceIds( + List subjectIds, CodeDef populationBasis, PopulationDef populationDef) { + String resourceType; + try { + // when this method is checked with a primitive value and not ResourceType it returns an error + // this try/catch is to prevent the exception thrown from setting the correct value + resourceType = + ResourceType.fromCode(populationBasis.code()).toString(); + } catch (FHIRException e) { + resourceType = null; + } + + // only ResourceType fhirType should return true here + boolean isResourceType = resourceType != null; + List resourceIds = new ArrayList<>(); + assert populationDef != null; + if (populationDef.getSubjectResources() != null) { + for (String subjectId : subjectIds) { + // retrieve criteria results by subject Key + var resources = + populationDef.getSubjectResources().get(R4ResourceIdUtils.stripPatientQualifier(subjectId)); + if (resources != null) { + if (isResourceType) { + resourceIds.addAll(resources.stream() + .map(MeasureMultiSubjectEvaluator::getPopulationResourceIds) // get resource id + .toList()); + } else { + resourceIds.addAll( + resources.stream().map(Object::toString).toList()); + } + } + } + } + return resourceIds; + } + + private static String getPopulationResourceIds(Object resourceObject) { + if (resourceObject instanceof IBaseResource resource) { + return resource.getIdElement().toVersionless().getValueAsString(); + } + return null; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java index e49be71dac..376083be20 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumDef.java @@ -43,4 +43,6 @@ public Set getValueDefs() { public List getSubjectIds() { return subjectIds; } + + // LUKETODO: toString } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java index 193171bd8f..bb9c4dc19a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -14,6 +14,8 @@ public class StratumPopulationDef { private final String id; private final Set subjectsQualifiedOrUnqualified; + // Temporary: this needs to be captured as number of intersected resources + private int count = 0; public StratumPopulationDef(String id, Set subjectsQualifiedOrUnqualified) { this.id = id; @@ -42,4 +44,15 @@ public Set getSubjectsUnqualified() { .map(R4ResourceIdUtils::stripAnyResourceQualifier) .collect(Collectors.toUnmodifiableSet()); } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + + // LUKETODO: toString } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index f039863414..83f987c092 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -303,15 +303,28 @@ private static void buildStratumPopulation( final Set subjectsQualifiedOrUnqualified = stratumPopulationDef.getSubjectsQualifiedOrUnqualified(); if (groupDef.isBooleanBasis()) { - buildBooleanBasisStratumPopulation(bc, sgpc, populationDef, subjectsQualifiedOrUnqualified); + buildBooleanBasisStratumPopulation( + bc, + sgpc, + stratumPopulationDef, + populationDef, + subjectsQualifiedOrUnqualified); } else { - buildResourceBasisStratumPopulation(bc, stratifierDef, sgpc, subjectIds, populationDef, groupDef); + buildResourceBasisStratumPopulation( + bc, + stratifierDef, + stratumPopulationDef, + sgpc, + subjectIds, + populationDef, + groupDef); } } private static void buildBooleanBasisStratumPopulation( BuilderContext bc, StratifierGroupPopulationComponent sgpc, + StratumPopulationDef stratumPopulationDef, PopulationDef populationDef, Set subjectIdsCommonToPopulation) { @@ -338,6 +351,7 @@ private static void buildBooleanBasisStratumPopulation( private static void buildResourceBasisStratumPopulation( BuilderContext bc, StratifierDef stratifierDef, + StratumPopulationDef stratumPopulationDef, StratifierGroupPopulationComponent sgpc, List subjectIds, PopulationDef populationDef, @@ -345,6 +359,12 @@ private static void buildResourceBasisStratumPopulation( final List resourceIds = getResourceIds(subjectIds, groupDef, populationDef); + // LUKETODO: this is wrong for our purposes: + // 1) we are getting non-distinct Date values, one duplicate for each of the 2 dates resolved by the population + // 2) we are doing the computation in the Builder, when we ought to do it in the MeasureEvaluator + // 3) We're conflating the intersection code with the counting, but this needs to be done separately + // 4) So we need to capture the intersection of resources in the MeasureEvaluator, then count them separately + // 5) As a first step, move this code to the MeasureEvaluator and ensure all existing tests pass final int stratumCount = getStratumCountUpper(stratifierDef, populationDef, resourceIds); sgpc.setCount(stratumCount); From 8d3ad63655403a06423b0e951245410abb271c14 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 15:07:32 -0400 Subject: [PATCH 47/48] Flip switch on stratum population count. Start migrating resource IDs logic. --- .../common/MeasureMultiSubjectEvaluator.java | 134 ++++++++---------- .../measure/common/StratumPopulationDef.java | 11 ++ .../cr/measure/r4/R4StratifierBuilder.java | 85 +++++------ 3 files changed, 110 insertions(+), 120 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java index 1ec4392d20..197732c299 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureMultiSubjectEvaluator.java @@ -4,13 +4,6 @@ import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import com.google.common.collect.Table; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.ResourceType; -import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; -import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -20,6 +13,13 @@ import java.util.Set; import java.util.stream.Collector; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.ResourceType; +import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; // LUKETODO: javadoc public class MeasureMultiSubjectEvaluator { @@ -40,14 +40,10 @@ public static void postEvaluationMultiSubject(MeasureDef measureDef) { if (!stratifierDef.components().isEmpty()) { stratumDefs = componentStratumPlural( - stratifierDef, - groupDef.getPopulationBasis(), - groupDef.populations()); + stratifierDef, groupDef.getPopulationBasis(), groupDef.populations()); } else { stratumDefs = nonComponentStratumPlural( - stratifierDef, - groupDef.getPopulationBasis(), - groupDef.populations()); + stratifierDef, groupDef.getPopulationBasis(), groupDef.populations()); } stratifierDef.addAllStratum(stratumDefs); @@ -92,17 +88,17 @@ private static StratumDef buildStratumDef( } return new StratumDef( - stratumText, - populationDefs.stream() - .map(popDef -> buildStratumPopulationDef( - stratifierDef.getStratifierType(), - stratifierDef.getAllCriteriaResultValues(), - populationBasis, - popDef, - subjectIds)) - .toList(), - values, - subjectIds); + stratumText, + populationDefs.stream() + .map(popDef -> buildStratumPopulationDef( + stratifierDef.getStratifierType(), + stratifierDef.getAllCriteriaResultValues(), + populationBasis, + popDef, + subjectIds)) + .toList(), + values, + subjectIds); } private static StratumPopulationDef buildStratumPopulationDef( @@ -113,32 +109,30 @@ private static StratumPopulationDef buildStratumPopulationDef( List subjectIds) { var popSubjectIds = populationDef.getSubjects().stream() - .map(R4ResourceIdUtils::addPatientQualifier) - .collect(Collectors.toUnmodifiableSet()); + .map(R4ResourceIdUtils::addPatientQualifier) + .collect(Collectors.toUnmodifiableSet()); var qualifiedSubjectIdsCommonToPopulation = Sets.intersection(new HashSet<>(subjectIds), popSubjectIds); final StratumPopulationDef stratumPopulationDef = - new StratumPopulationDef( - populationDef.id(), - qualifiedSubjectIdsCommonToPopulation); + new StratumPopulationDef(populationDef.id(), qualifiedSubjectIdsCommonToPopulation); - final int stratumCount = getStratumCountUpper( - measureStratifierType, - evaluationResultsForStratifier, - populationDef, - getResourceIds(subjectIds, groupPopulationBasis, populationDef)); + final List resourceIds = getResourceIds(subjectIds, groupPopulationBasis, populationDef); + + final int stratumCount = + getStratumCountUpper(measureStratifierType, evaluationResultsForStratifier, populationDef, resourceIds); stratumPopulationDef.setCount(stratumCount); + stratumPopulationDef.addAllResourceIds(resourceIds); return stratumPopulationDef; } - private static List componentStratumPlural(StratifierDef stratifierDef, - CodeDef populationBasis, List populationDefs) { + private static List componentStratumPlural( + StratifierDef stratifierDef, CodeDef populationBasis, List populationDefs) { final Table subjectResultTable = - buildSubjectResultsTable(stratifierDef.components()); + buildSubjectResultsTable(stratifierDef.components()); // Stratifiers should be of the same basis as population // Split subjects by result values @@ -157,8 +151,7 @@ private static List componentStratumPlural(StratifierDef stratifierD // | Stratum-3 | <'M','hispanic/latino'> | [subject-c] | // | Stratum-4 | <'F','black'> | [subject-d, subject-e] | - var stratumDef = buildStratumDef(stratifierDef, valueSet, subjects, populationBasis, - populationDefs); + var stratumDef = buildStratumDef(stratifierDef, valueSet, subjects, populationBasis, populationDefs); stratumDefs.add(stratumDef); }); @@ -167,7 +160,7 @@ private static List componentStratumPlural(StratifierDef stratifierD } private static List nonComponentStratumPlural( - StratifierDef stratifierDef, CodeDef populationBasis, List populationDefs) { + StratifierDef stratifierDef, CodeDef populationBasis, List populationDefs) { // standard Stratifier // one criteria expression defined, one set of criteria results @@ -188,17 +181,16 @@ private static List nonComponentStratumPlural( // Seems to be irrelevant for criteria based stratifiers var patients = List.of(); - var stratum = buildStratumDef(stratifierDef, stratValues, patients, populationBasis, - populationDefs); + var stratum = buildStratumDef(stratifierDef, stratValues, patients, populationBasis, populationDefs); return List.of(stratum); } final Map> subjectsByValue = subjectValues.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> entry.getValue().rawValue() != null) - .collect(Collectors.groupingBy( - entry -> new StratumValueWrapper(entry.getValue().rawValue()), - Collectors.mapping(Entry::getKey, Collectors.toList()))); + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().rawValue() != null) + .collect(Collectors.groupingBy( + entry -> new StratumValueWrapper(entry.getValue().rawValue()), + Collectors.mapping(Entry::getKey, Collectors.toList()))); var stratumMultiple = new ArrayList(); @@ -211,19 +203,15 @@ private static List nonComponentStratumPlural( // patch Patient values with prefix of ResourceType to match with incoming population subjects for stratum // TODO: should match context of CQL, not only Patient var patientsSubjects = stratValue.getValue().stream() - .map(R4ResourceIdUtils::addPatientQualifier) - .toList(); + .map(R4ResourceIdUtils::addPatientQualifier) + .toList(); // build the stratum for each unique value // non-component stratifiers will populate a 'null' for componentStratifierDef, since it doesn't have // multiple criteria // TODO: build out nonComponent stratum method Set stratValues = Set.of(new StratumValueDef(stratValue.getKey(), null)); - var stratum = buildStratumDef( - stratifierDef, - stratValues, - patientsSubjects, - populationBasis, - populationDefs); + var stratum = + buildStratumDef(stratifierDef, stratValues, patientsSubjects, populationBasis, populationDefs); stratumMultiple.add(stratum); } @@ -231,7 +219,7 @@ private static List nonComponentStratumPlural( } private static Table buildSubjectResultsTable( - List componentDefs) { + List componentDefs) { final Table subjectResultTable = HashBasedTable.create(); @@ -248,7 +236,7 @@ private static Table buildS } private static Map, List> groupSubjectsByValueDefSet( - Table table) { + Table table) { // input format // | Subject (String) | CriteriaResult (ValueWrapper) | StratifierComponentDef | // | ---------------- | ----------------------------- | ---------------------- | @@ -268,8 +256,8 @@ private static Map, List> groupSubjectsByValueDefSe for (Table.Cell cell : table.cellSet()) { subjectToValueDefs - .computeIfAbsent(cell.getRowKey(), k -> new HashSet<>()) - .add(new StratumValueDef(cell.getColumnKey(), cell.getValue())); + .computeIfAbsent(cell.getRowKey(), k -> new HashSet<>()) + .add(new StratumValueDef(cell.getColumnKey(), cell.getValue())); } // output format: // | Set | List | @@ -281,12 +269,12 @@ private static Map, List> groupSubjectsByValueDefSe // Step 2: Invert to Map, List> return subjectToValueDefs.entrySet().stream() - .collect(Collectors.groupingBy( - Map.Entry::getValue, - Collector.of(ArrayList::new, (list, e) -> list.add(e.getKey()), (l1, l2) -> { - l1.addAll(l2); - return l1; - }))); + .collect(Collectors.groupingBy( + Map.Entry::getValue, + Collector.of(ArrayList::new, (list, e) -> list.add(e.getKey()), (l1, l2) -> { + l1.addAll(l2); + return l1; + }))); } // This is weird pattern where we have multiple qualifying values within a single stratum, @@ -311,7 +299,8 @@ private static int getStratumCountUpper( } final Class resourcesClassFirst = resources.iterator().next().getClass(); - final Class resultClassFirst = evaluationResults.iterator().next().getClass(); + final Class resultClassFirst = + evaluationResults.iterator().next().getClass(); // Sanity check: isCriteriaBasedStratifier() should have filtered this out if (resourcesClassFirst != resultClassFirst) { @@ -332,13 +321,12 @@ private static int getStratumCountUpper( @Nonnull private static List getResourceIds( - List subjectIds, CodeDef populationBasis, PopulationDef populationDef) { + List subjectIds, CodeDef populationBasis, PopulationDef populationDef) { String resourceType; try { // when this method is checked with a primitive value and not ResourceType it returns an error // this try/catch is to prevent the exception thrown from setting the correct value - resourceType = - ResourceType.fromCode(populationBasis.code()).toString(); + resourceType = ResourceType.fromCode(populationBasis.code()).toString(); } catch (FHIRException e) { resourceType = null; } @@ -351,15 +339,15 @@ private static List getResourceIds( for (String subjectId : subjectIds) { // retrieve criteria results by subject Key var resources = - populationDef.getSubjectResources().get(R4ResourceIdUtils.stripPatientQualifier(subjectId)); + populationDef.getSubjectResources().get(R4ResourceIdUtils.stripPatientQualifier(subjectId)); if (resources != null) { if (isResourceType) { resourceIds.addAll(resources.stream() - .map(MeasureMultiSubjectEvaluator::getPopulationResourceIds) // get resource id - .toList()); + .map(MeasureMultiSubjectEvaluator::getPopulationResourceIds) // get resource id + .toList()); } else { resourceIds.addAll( - resources.stream().map(Object::toString).toList()); + resources.stream().map(Object::toString).toList()); } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java index bb9c4dc19a..1a7ea523ff 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/StratumPopulationDef.java @@ -1,5 +1,7 @@ package org.opencds.cqf.fhir.cr.measure.common; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -16,6 +18,8 @@ public class StratumPopulationDef { private final Set subjectsQualifiedOrUnqualified; // Temporary: this needs to be captured as number of intersected resources private int count = 0; + // Temporary: figure out what to do with this + private List resourceIds = new ArrayList<>(); public StratumPopulationDef(String id, Set subjectsQualifiedOrUnqualified) { this.id = id; @@ -53,6 +57,13 @@ public void setCount(int count) { this.count = count; } + public List getResourceIds() { + return resourceIds; + } + + public void addAllResourceIds(List resourceIds) { + this.resourceIds.addAll(resourceIds); + } // LUKETODO: toString } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index 83f987c092..f3e9214c97 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -2,12 +2,12 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import com.google.common.collect.Sets; -import com.google.common.collect.Sets.SetView; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -304,20 +304,10 @@ private static void buildStratumPopulation( if (groupDef.isBooleanBasis()) { buildBooleanBasisStratumPopulation( - bc, - sgpc, - stratumPopulationDef, - populationDef, - subjectsQualifiedOrUnqualified); + bc, sgpc, stratumPopulationDef, populationDef, subjectsQualifiedOrUnqualified); } else { buildResourceBasisStratumPopulation( - bc, - stratifierDef, - stratumPopulationDef, - sgpc, - subjectIds, - populationDef, - groupDef); + bc, stratifierDef, stratumPopulationDef, sgpc, subjectIds, populationDef, groupDef); } } @@ -359,22 +349,21 @@ private static void buildResourceBasisStratumPopulation( final List resourceIds = getResourceIds(subjectIds, groupDef, populationDef); + if (! collectionsEqualIgnoringOrder(resourceIds, stratumPopulationDef.getResourceIds())) { + throw new IllegalStateException("resource IDs don't match: old: %s, new: %s".formatted(resourceIds, stratumPopulationDef.getResourceIds())); + } + // LUKETODO: this is wrong for our purposes: // 1) we are getting non-distinct Date values, one duplicate for each of the 2 dates resolved by the population // 2) we are doing the computation in the Builder, when we ought to do it in the MeasureEvaluator // 3) We're conflating the intersection code with the counting, but this needs to be done separately // 4) So we need to capture the intersection of resources in the MeasureEvaluator, then count them separately // 5) As a first step, move this code to the MeasureEvaluator and ensure all existing tests pass - final int stratumCount = getStratumCountUpper(stratifierDef, populationDef, resourceIds); - - sgpc.setCount(stratumCount); - - if (resourceIds.isEmpty()) { - return; - } + sgpc.setCount(stratumPopulationDef.getCount()); // subject-list ListResource to match intersection of results - if (bc.report().getType() == org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUBJECTLIST) { + if ((!resourceIds.isEmpty()) + && bc.report().getType() == org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUBJECTLIST) { ListResource popSubjectList = R4StratifierBuilder.createIdList(UUID.randomUUID().toString(), resourceIds); bc.addContained(popSubjectList); @@ -382,38 +371,40 @@ private static void buildResourceBasisStratumPopulation( } } - private static int getStratumCountUpper( - StratifierDef stratifierDef, PopulationDef populationDef, List resourceIds) { - - if (MeasureStratifierType.CRITERIA == stratifierDef.getStratifierType()) { - final Set resources = populationDef.getResources(); - // LUKETODO: for the component criteria scenario, we don't add the results directly to the stratifierDef, - // but to each of the component defs, which is why this is empty - final Set results = stratifierDef.getAllCriteriaResultValues(); - - if (resources.isEmpty() || results.isEmpty()) { - // There's no intersection, so no point in going further. - return 0; - } + public static boolean collectionsEqualIgnoringOrder(Collection coll1, Collection coll2) { + if (coll1 == null || coll2 == null) { + return coll1 == coll2; + } - final Class resourcesClassFirst = resources.iterator().next().getClass(); - final Class resultClassFirst = results.iterator().next().getClass(); + if (coll1.size() != coll2.size()) { + return false; + } - // Sanity check: isCriteriaBasedStratifier() should have filtered this out - if (resourcesClassFirst != resultClassFirst) { - // Different classes, so no point in going further. - return 0; - } + Map frequencies = new HashMap<>(); - final SetView intersection = Sets.intersection(resources, results); - return intersection.size(); + // Build frequency map for the first collection + for (T item : coll1) { + frequencies.put(item, frequencies.getOrDefault(item, 0) + 1); } - if (resourceIds.isEmpty()) { - return 0; + // A simple 'Map.equals()' won't work if coll2 has different elements + // that aren't in coll1. We must check coll2 against the map. + for (T item : coll2) { + Integer count = frequencies.get(item); + + // If the item is not in the map or the count is zero, it's a mismatch + if (count == null || count == 0) { + return false; + } + + // Decrement the count for the item + frequencies.put(item, count - 1); } - return resourceIds.size(); + // All counts should be zero, but the size check at the beginning + // combined with the decrement loop already guarantees this. + // If we reached here, the collections are equal. + return true; } @Nonnull From 4aac3f7920e86b431de8f98f5f57ac41a5ffce46 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 31 Oct 2025 17:07:28 -0400 Subject: [PATCH 48/48] Migrate more logic to the 2nd step measure evaluator. --- .../fhir/cr/measure/common/PopulationDef.java | 7 ++ .../cr/measure/r4/R4StratifierBuilder.java | 66 +++++-------------- 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java index 4f6f56ce16..c4f710c2f8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/PopulationDef.java @@ -7,6 +7,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4ResourceIdUtils; public class PopulationDef { @@ -62,6 +63,12 @@ public Set getSubjects() { return this.getSubjectResources().keySet(); } + public Set getSubjectsWithPatientQualifier() { + return getSubjects().stream() + .map(R4ResourceIdUtils::addPatientQualifier) + .collect(Collectors.toUnmodifiableSet()); + } + public void retainAllResources(Set resourcesToRetain) { getSubjectResources().forEach((key, value) -> value.retainAll(resourcesToRetain)); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java index f3e9214c97..1aca04a73b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4StratifierBuilder.java @@ -97,14 +97,7 @@ private static void componentStratifier( var reportStratum = reportStratifier.addStratum(); buildStratum( - bc, - stratifierDef, - stratumDef, - reportStratum, - stratumDef.getValueDefs(), - stratumDef.getSubjectIds(), - populations, - groupDef); + bc, stratifierDef, stratumDef, reportStratum, stratumDef.getValueDefs(), populations, groupDef); }); } @@ -135,7 +128,6 @@ private static void nonComponentStratifier( getOnlyStratumDef(stratifierDef), reportStratum, stratValues, - patients, populations, groupDef); return; // short-circuit so we don't process non-criteria logic @@ -161,15 +153,7 @@ private static void buildStratumOuter( var reportStratum = reportStratifier.addStratum(); - buildStratum( - bc, - stratifierDef, - stratumDef, - reportStratum, - stratumDef.getValueDefs(), - stratumDef.getSubjectIds(), - populations, - groupDef); + buildStratum(bc, stratifierDef, stratumDef, reportStratum, stratumDef.getValueDefs(), populations, groupDef); } private static void buildStratum( @@ -178,7 +162,6 @@ private static void buildStratum( StratumDef stratumDef, StratifierGroupComponent stratum, Set values, - List subjectIds, List populations, GroupDef groupDef) { boolean isComponent = values.size() > 1; @@ -242,8 +225,7 @@ private static void buildStratum( throw new InternalErrorException("could not find MeasureGroupPopulationComponent"); } var stratumPopulation = stratum.addPopulation(); - buildStratumPopulation( - bc, stratifierDef, stratumPopulationDef, stratumPopulation, subjectIds, optMgpc.get(), groupDef); + buildStratumPopulation(bc, stratumPopulationDef, stratumPopulation, optMgpc.get(), groupDef); } } @@ -269,10 +251,8 @@ private static CodeableConcept expressionResultToCodableConcept(StratumValueWrap // the provided list of subjectIds private static void buildStratumPopulation( BuilderContext bc, - StratifierDef stratifierDef, StratumPopulationDef stratumPopulationDef, StratifierGroupPopulationComponent sgpc, - List subjectIds, MeasureGroupPopulationComponent population, GroupDef groupDef) { @@ -300,14 +280,10 @@ private static void buildStratumPopulation( population.getCode().getCodingFirstRep().getCode())); } - final Set subjectsQualifiedOrUnqualified = stratumPopulationDef.getSubjectsQualifiedOrUnqualified(); - if (groupDef.isBooleanBasis()) { - buildBooleanBasisStratumPopulation( - bc, sgpc, stratumPopulationDef, populationDef, subjectsQualifiedOrUnqualified); + buildBooleanBasisStratumPopulation(bc, sgpc, stratumPopulationDef, populationDef); } else { - buildResourceBasisStratumPopulation( - bc, stratifierDef, stratumPopulationDef, sgpc, subjectIds, populationDef, groupDef); + buildResourceBasisStratumPopulation(bc, stratumPopulationDef, sgpc); } } @@ -315,43 +291,31 @@ private static void buildBooleanBasisStratumPopulation( BuilderContext bc, StratifierGroupPopulationComponent sgpc, StratumPopulationDef stratumPopulationDef, - PopulationDef populationDef, - Set subjectIdsCommonToPopulation) { + PopulationDef populationDef) { + + final Set subjectsCommonToPopulation = stratumPopulationDef.getSubjectsQualifiedOrUnqualified(); + + var popSubjectIds = populationDef.getSubjectsWithPatientQualifier(); - var popSubjectIds = populationDef.getSubjects().stream() - .map(R4ResourceIdUtils::addPatientQualifier) - .toList(); if (popSubjectIds.isEmpty()) { sgpc.setCount(0); return; } - sgpc.setCount(subjectIdsCommonToPopulation.size()); + sgpc.setCount(subjectsCommonToPopulation.size()); // subject-list ListResource to match intersection of results - if (!subjectIdsCommonToPopulation.isEmpty() + if (!subjectsCommonToPopulation.isEmpty() && bc.report().getType() == org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUBJECTLIST) { ListResource popSubjectList = - R4StratifierBuilder.createIdList(UUID.randomUUID().toString(), subjectIdsCommonToPopulation); + R4StratifierBuilder.createIdList(UUID.randomUUID().toString(), subjectsCommonToPopulation); bc.addContained(popSubjectList); sgpc.setSubjectResults(new Reference("#" + popSubjectList.getId())); } } private static void buildResourceBasisStratumPopulation( - BuilderContext bc, - StratifierDef stratifierDef, - StratumPopulationDef stratumPopulationDef, - StratifierGroupPopulationComponent sgpc, - List subjectIds, - PopulationDef populationDef, - GroupDef groupDef) { - - final List resourceIds = getResourceIds(subjectIds, groupDef, populationDef); - - if (! collectionsEqualIgnoringOrder(resourceIds, stratumPopulationDef.getResourceIds())) { - throw new IllegalStateException("resource IDs don't match: old: %s, new: %s".formatted(resourceIds, stratumPopulationDef.getResourceIds())); - } + BuilderContext bc, StratumPopulationDef stratumPopulationDef, StratifierGroupPopulationComponent sgpc) { // LUKETODO: this is wrong for our purposes: // 1) we are getting non-distinct Date values, one duplicate for each of the 2 dates resolved by the population @@ -361,6 +325,8 @@ private static void buildResourceBasisStratumPopulation( // 5) As a first step, move this code to the MeasureEvaluator and ensure all existing tests pass sgpc.setCount(stratumPopulationDef.getCount()); + final List resourceIds = stratumPopulationDef.getResourceIds(); + // subject-list ListResource to match intersection of results if ((!resourceIds.isEmpty()) && bc.report().getType() == org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUBJECTLIST) {