Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4240647
Introduce changes to continuous variable observations from another br…
lukedegruchy Oct 15, 2025
b5c6e49
Add TODOs about error handling.
lukedegruchy Oct 15, 2025
3d9ec5e
Merge with changes in R4MeasureDefBuilder. Tweak a couple of tests. …
lukedegruchy Oct 16, 2025
9dd224c
Merge changes from R4MeasureReportBuilder. More tests are fixed, but…
lukedegruchy Oct 16, 2025
17d0dc6
Fix all tests in MeasureStratifierTest by fixing at least of bad merg…
lukedegruchy Oct 16, 2025
b634702
Fix last set of test due to missing test files. Add more TODOs.
lukedegruchy Oct 16, 2025
753a49d
Sonar.
lukedegruchy Oct 16, 2025
3766a1e
Finish refactoring R4MeasureDefBuilder.
lukedegruchy Oct 16, 2025
f39510b
More refactoring for ContinuousVariableObservationHandler, including …
lukedegruchy Oct 16, 2025
d13a2ba
Refactor tests and clean up more cruft.
lukedegruchy Oct 16, 2025
9019842
More refactoring: Make it easier to reason about the workflow in ques…
lukedegruchy Oct 16, 2025
57b338e
Add a new record and resolve LibraryInitHandler TODOs.
lukedegruchy Oct 16, 2025
f387a04
Final round of refactoring to make code easier to reason about. Also…
lukedegruchy Oct 16, 2025
861d566
Merge remote-tracking branch 'origin/master' into ld-20251015-continu…
lukedegruchy Oct 16, 2025
dfcc0f1
Optimize algorithm for initializing the library for a given measure o…
lukedegruchy Oct 16, 2025
e068a0f
Add a bunch of TODOs with ideas of how to generalize FHIR resources i…
lukedegruchy Oct 17, 2025
30062f8
Add more ideas, prototype classes, and todos.
lukedegruchy Oct 20, 2025
c5dc296
More spitballing.
lukedegruchy Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@
@SuppressWarnings("UnstableApiUsage")
public class Engines {

// LUKETODO: this class makes heavy of Repository, instead of acting directly on Libraries
// it uses Adapters to deal with FHIR libraries in version-agnostic ways, but the main thing
// seems to be to retrieve related libraries from a database, which stores the FHIR resource
// LUKETODO: The only thing we could potentially do is have a wrapper around the Repository that
// adapts a given Resource to a version-agnostic one for CQL
// LUKETODO: how do we handle ValueSets/Terminology/etc?
// LUKETODO: ideas:
/*
1. A read-only interface on top of IRepository, to only read and search for resources (check with Brenin)
2. A new interface that will retrieve given FHIR resources
3. A new interface that will retrieve the specific Def classes, such as MeasureDef/LibraryDef/etc
with the implementation handling the nitty gritty details of conversion between FHIR resources
and defs
*/

private static Logger logger = LoggerFactory.getLogger(Engines.class);

private Engines() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.opencds.cqf.fhir.utility.iterable.BundleIterable;
import org.opencds.cqf.fhir.utility.search.Searches;

// LUKETODO: Do we need this to return an IBaseResource, or just a LibraryDef, which will have the Base64 CQL library?
public class RepositoryFhirLibrarySourceProvider extends BaseFhirLibrarySourceProvider {

private final IRepository repository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -21,40 +20,35 @@
*/
public class CompositeEvaluationResultsPerMeasure {
// The same measure may have successful results AND errors, so account for both
private final Map<IIdType, Map<String, EvaluationResult>> resultsPerMeasure;
private final Map<MeasureDef, Map<String, EvaluationResult>> resultsPerMeasure;
// We may get several errors for a given measure
private final Map<IIdType, List<String>> errorsPerMeasure;
private final Map<MeasureDef, List<String>> errorsPerMeasure;

private CompositeEvaluationResultsPerMeasure(Builder builder) {

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

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

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

errorsPerMeasure.getOrDefault(unqualifiedMeasureId, List.of()).forEach(measureDef::addError);
public Map<String, EvaluationResult> 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());
}

/**
Expand All @@ -63,7 +57,7 @@ public Map<String, EvaluationResult> processMeasureForSuccessOrFailure(IIdType m
* and associated EvaluationResult produced from CQL expression evaluation
* @return {@code Map<IIdType, Map<String, EvaluationResult>>}
*/
public Map<IIdType, Map<String, EvaluationResult>> getResultsPerMeasure() {
public Map<MeasureDef, Map<String, EvaluationResult>> getResultsPerMeasure() {
return this.resultsPerMeasure;
}

Expand All @@ -72,7 +66,7 @@ public Map<IIdType, Map<String, EvaluationResult>> 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<IIdType, List<String>>}
*/
public Map<IIdType, List<String>> getErrorsPerMeasure() {
public Map<MeasureDef, List<String>> getErrorsPerMeasure() {
return this.errorsPerMeasure;
}

Expand All @@ -81,49 +75,57 @@ public static Builder builder() {
}

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

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

public void addResults(List<IIdType> measureIds, String subjectId, EvaluationResult evaluationResult) {
for (IIdType measureId : measureIds) {
addResult(measureId, subjectId, evaluationResult);
public void addResults(
List<MeasureDef> measureDefs,
String subjectId,
EvaluationResult evaluationResult,
MeasureObservationResults 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,
MeasureObservationResults 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<>());
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<? extends IIdType> measureIds, String error) {
public void addErrors(List<MeasureDef> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading