Skip to content

Commit 092b771

Browse files
committed
Merge branch 'dev' into users/jbritton/LEGLINK-921
2 parents c4586cb + ba1e746 commit 092b771

5 files changed

Lines changed: 270 additions & 77 deletions

File tree

DotNet/Automation/Generation/FhirGenerationPipeline.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
402402
// measure's MeasureReport does not contain the patient's resources, so its SDE
403403
// semantics do not contribute to the intersection of exclusions that determines
404404
// whether a resource reaches ABS.
405+
HashSet<string>? cqlFilteredKeys = null;
405406
var cqlInput = CqlFilterInputExtractor.ExtractFromEntries(patientId, entries);
406407
var effectiveProfile = profile;
407408

@@ -419,7 +420,7 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
419420
var qualifyingMeasures = measures.Where(effectiveProfile.QualifiesFor).ToList();
420421
if (qualifyingMeasures.Count > 0)
421422
{
422-
var cqlFilteredKeys = CqlFilterSimulator.ComputeFilteredKeys(qualifyingMeasures, cqlInput);
423+
cqlFilteredKeys = CqlFilterSimulator.ComputeFilteredKeys(qualifyingMeasures, cqlInput);
423424
manifestBuilder.SetCqlFilteredKeys(patientId, cqlFilteredKeys);
424425
}
425426
}
@@ -464,7 +465,8 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
464465
acquiredKeys,
465466
patientSimEntries,
466467
sharedSimEntries,
467-
acquisitionSimulation.OrganizationLocationConditionFhirPaths);
468+
acquisitionSimulation.OrganizationLocationConditionFhirPaths,
469+
cqlFilteredKeys);
468470
manifestBuilder.SetSimulatedAcquiredKeys(patientId, acquiredKeys);
469471
// patientSimEntries (JsonElement clones) are now eligible for GC
470472
}
@@ -562,6 +564,7 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
562564
var profile = new PatientProfile(eligibilities, ClinicalScenarioId: imported.DetectedClinicalScenarioId);
563565

564566
// 3. CQL filter simulation + period-aware eligibility prediction
567+
HashSet<string>? cqlFilteredKeys = null;
565568
var cqlInput = CqlFilterInputExtractor.ExtractFromEntries(patientId, entries);
566569
var effectiveProfile = profile;
567570
if (cqlInput != null)
@@ -578,7 +581,7 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
578581
var qualifyingMeasures = measures.Where(effectiveProfile.QualifiesFor).ToList();
579582
if (qualifyingMeasures.Count > 0)
580583
{
581-
var cqlFilteredKeys = CqlFilterSimulator.ComputeFilteredKeys(qualifyingMeasures, cqlInput);
584+
cqlFilteredKeys = CqlFilterSimulator.ComputeFilteredKeys(qualifyingMeasures, cqlInput);
582585
manifestBuilder.SetCqlFilteredKeys(patientId, cqlFilteredKeys);
583586
}
584587
}
@@ -617,7 +620,8 @@ public static async Task<PipelineResult> GenerateAndUploadAsync(
617620
acquiredKeys,
618621
patientSimEntries,
619622
sharedSimEntries,
620-
acquisitionSimulation.OrganizationLocationConditionFhirPaths);
623+
acquisitionSimulation.OrganizationLocationConditionFhirPaths,
624+
cqlFilteredKeys);
621625
manifestBuilder.SetSimulatedAcquiredKeys(patientId, acquiredKeys);
622626
}
623627

DotNet/Automation/Generation/GenerationManifest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ public HashSet<string> GetExpectedAbsKeysForPatient(string patientId)
284284

285285
HashSet<string>? sourceKeys = null;
286286

287-
if (SimulatedAcquiredResourceKeysByPatient.TryGetValue(patientId, out var simulated) && simulated.Count > 0)
287+
if (SimulatedAcquiredResourceKeysByPatient.TryGetValue(patientId, out var simulated))
288288
sourceKeys = simulated;
289289
else if (ResourceKeysByPatient.TryGetValue(patientId, out var generated) && generated.Count > 0)
290290
sourceKeys = generated;

DotNet/Automation/Generation/OrgResourceMapPredictionFilter.cs

Lines changed: 40 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using System.Text.Json;
2-
using System.Text.RegularExpressions;
1+
using Hl7.Fhir.ElementModel;
2+
using Hl7.Fhir.Model;
3+
using Hl7.FhirPath;
4+
using System.Text.Json;
35

46
namespace LantanaGroup.Automation.Generation;
57

@@ -19,7 +21,8 @@ public static HashSet<string> Apply(
1921
HashSet<string> acquiredKeys,
2022
IReadOnlyList<(string ResourceType, string ResourceId, string Key, JsonElement Resource)> patientResourceEntries,
2123
IReadOnlyList<(string ResourceType, string ResourceId, string Key, JsonElement Resource)>? sharedResourceEntries,
22-
IReadOnlyList<string>? organizationLocationConditionFhirPaths)
24+
IReadOnlyList<string>? organizationLocationConditionFhirPaths,
25+
IReadOnlySet<string>? cqlFilteredKeys = null)
2326
{
2427
if (acquiredKeys.Count == 0
2528
|| organizationLocationConditionFhirPaths == null
@@ -115,12 +118,13 @@ public static HashSet<string> Apply(
115118
filtered.Add(key);
116119
}
117120

118-
return PruneUnreferencedReferenceResources(filtered, entriesByKey);
121+
return PruneUnreferencedReferenceResources(filtered, entriesByKey, cqlFilteredKeys);
119122
}
120123

121124
private static HashSet<string> PruneUnreferencedReferenceResources(
122125
HashSet<string> keptKeys,
123-
Dictionary<string, ResourceEntry> entriesByKey)
126+
Dictionary<string, ResourceEntry> entriesByKey,
127+
IReadOnlySet<string>? cqlFilteredKeys)
124128
{
125129
if (keptKeys.Count == 0)
126130
return keptKeys;
@@ -129,6 +133,7 @@ private static HashSet<string> PruneUnreferencedReferenceResources(
129133
// prediction if still referenced by kept resources after org-scope filtering.
130134
var pruneTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
131135
{
136+
"Location",
132137
"Medication",
133138
"Specimen",
134139
"Device"
@@ -142,6 +147,9 @@ private static HashSet<string> PruneUnreferencedReferenceResources(
142147
var referencedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
143148
foreach (var key in keptKeys)
144149
{
150+
if (cqlFilteredKeys?.Contains(key) == true)
151+
continue;
152+
145153
if (!entriesByKey.TryGetValue(key, out var entry))
146154
continue;
147155

@@ -196,86 +204,46 @@ private static bool LocationMatchesCondition(JsonElement locationResource, strin
196204
if (string.IsNullOrWhiteSpace(fhirPath))
197205
return false;
198206

199-
var path = fhirPath.Trim();
200-
if (path.StartsWith("Location.", StringComparison.OrdinalIgnoreCase))
201-
path = path["Location.".Length..];
202-
203-
var system = ExtractQuotedValue(path, "system");
204-
var value = ExtractQuotedValue(path, "value");
205-
var code = ExtractQuotedValue(path, "code");
206-
207-
if (path.Contains("identifier", StringComparison.OrdinalIgnoreCase)
208-
&& !string.IsNullOrWhiteSpace(system)
209-
&& locationResource.TryGetProperty("identifier", out var identifiers)
210-
&& identifiers.ValueKind == JsonValueKind.Array)
207+
try
211208
{
212-
foreach (var identifier in identifiers.EnumerateArray())
213-
{
214-
if (!identifier.TryGetProperty("system", out var sysProp) || sysProp.ValueKind != JsonValueKind.String)
215-
continue;
209+
var path = fhirPath.Trim();
210+
if (path.StartsWith("Location.", StringComparison.OrdinalIgnoreCase))
211+
path = path["Location.".Length..];
216212

217-
var identifierSystem = sysProp.GetString();
218-
if (!string.Equals(identifierSystem, system, StringComparison.OrdinalIgnoreCase))
219-
continue;
213+
var location = JsonSerializer.Deserialize<Location>(
214+
locationResource.GetRawText(),
215+
FhirSerializerOptions.ForFhirWithoutValidation());
220216

221-
if (string.IsNullOrWhiteSpace(value))
222-
return true;
217+
if (location is null)
218+
return false;
223219

224-
if (identifier.TryGetProperty("value", out var valueProp)
225-
&& valueProp.ValueKind == JsonValueKind.String
226-
&& string.Equals(valueProp.GetString(), value, StringComparison.OrdinalIgnoreCase))
227-
{
228-
return true;
229-
}
230-
}
231-
}
220+
var element = location.ToTypedElement();
221+
var compiled = new FhirPathCompiler().Compile(path);
222+
var results = compiled(element, new EvaluationContext()).ToList();
232223

233-
if (path.Contains("type.coding", StringComparison.OrdinalIgnoreCase)
234-
&& !string.IsNullOrWhiteSpace(system)
235-
&& locationResource.TryGetProperty("type", out var types)
236-
&& types.ValueKind == JsonValueKind.Array)
237-
{
238-
foreach (var type in types.EnumerateArray())
239-
{
240-
if (!type.TryGetProperty("coding", out var codingArray) || codingArray.ValueKind != JsonValueKind.Array)
241-
continue;
224+
if (results.Count == 0)
225+
return false;
242226

243-
foreach (var coding in codingArray.EnumerateArray())
244-
{
245-
if (!coding.TryGetProperty("system", out var sysProp) || sysProp.ValueKind != JsonValueKind.String)
246-
continue;
227+
if (results.Count == 1 && results[0].Value is bool isMatch)
228+
return isMatch;
247229

248-
if (!string.Equals(sysProp.GetString(), system, StringComparison.OrdinalIgnoreCase))
249-
continue;
250-
251-
if (string.IsNullOrWhiteSpace(code))
252-
return true;
253-
254-
if (coding.TryGetProperty("code", out var codeProp)
255-
&& codeProp.ValueKind == JsonValueKind.String
256-
&& string.Equals(codeProp.GetString(), code, StringComparison.OrdinalIgnoreCase))
257-
{
258-
return true;
259-
}
260-
}
261-
}
230+
return true;
231+
}
232+
catch
233+
{
234+
return false;
262235
}
263-
264-
return false;
265-
}
266-
267-
private static string? ExtractQuotedValue(string source, string key)
268-
{
269-
var match = Regex.Match(source, $@"\b{Regex.Escape(key)}\s*=\s*'([^']+)'", RegexOptions.IgnoreCase);
270-
return match.Success ? match.Groups[1].Value : null;
271236
}
272237

273238
private static bool TryGetReferencedResourceIds(JsonElement resource, string resourceType, out List<string> ids)
274239
{
275240
ids = EnumerateReferences(resource)
276-
.Where(reference => reference.StartsWith(resourceType + "/", StringComparison.OrdinalIgnoreCase))
277-
.Select(reference => reference[(resourceType.Length + 1)..])
278-
.Where(id => !string.IsNullOrWhiteSpace(id))
241+
.Select(reference => TryParseReference(reference, out var type, out var id)
242+
? (Type: type, Id: id)
243+
: (Type: string.Empty, Id: string.Empty))
244+
.Where(parsed => string.Equals(parsed.Type, resourceType, StringComparison.OrdinalIgnoreCase)
245+
&& !string.IsNullOrWhiteSpace(parsed.Id))
246+
.Select(parsed => parsed.Id)
279247
.Distinct(StringComparer.OrdinalIgnoreCase)
280248
.ToList();
281249

DotNet/ServiceTests/UnitTests/Automation/GenerationManifestExpectedAbsTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,29 @@ public void Simulated_acquired_keys_take_precedence_over_generated_keys_for_qual
167167
keys.Should().BeEquivalentTo(["Patient/p-q", "Encounter/Esim"]);
168168
}
169169

170+
[Fact]
171+
public void Empty_simulated_acquired_set_does_not_fallback_to_generated_keys()
172+
{
173+
var manifest = new GenerationManifest
174+
{
175+
PatientIds = ["p-q"],
176+
Profiles = [QualifyingProfile()],
177+
SelectedMeasures = [Ach],
178+
ResourceKeysByPatient = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal)
179+
{
180+
["p-q"] = new(StringComparer.OrdinalIgnoreCase) { "Encounter/Egen", "Condition/Cgen" },
181+
},
182+
SimulatedAcquiredResourceKeysByPatient = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal)
183+
{
184+
["p-q"] = new(StringComparer.OrdinalIgnoreCase)
185+
},
186+
};
187+
188+
var keys = manifest.GetExpectedAbsKeysForPatient("p-q");
189+
190+
keys.Should().BeEquivalentTo(["Patient/p-q"]);
191+
}
192+
170193
[Fact]
171194
public void Expected_abs_counts_do_not_include_organization_when_include_setting_is_false()
172195
{

0 commit comments

Comments
 (0)