Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Is-a filter in local ValueSet expansion #2733

Merged
merged 21 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b9ab5c6
add filter extensions
mmsmits Mar 11, 2024
5c6f307
freature: added is-a filter to expand valuesets
mmsmits Mar 11, 2024
78aab37
added tests
mmsmits Mar 11, 2024
9390e07
improved Tests
mmsmits Mar 11, 2024
2264ce1
document and clean up
mmsmits Mar 12, 2024
3fa0eef
remove some unused code
mmsmits Mar 12, 2024
021143a
throw an error when trying to filter codes from LOINC or SNOMED CT
mmsmits Mar 12, 2024
560da4a
(this commit shows better than review words what I meant)
ewoutkramer Mar 13, 2024
49f2dfb
moved valueset expansion extensions to shims.base
mmsmits Mar 15, 2024
800f044
minor improvements
mmsmits Mar 15, 2024
97933f2
added test to check for a positive result
mmsmits Mar 15, 2024
5db1f19
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits Mar 15, 2024
2ad9b62
improvement: first flatten codes and then use a lookup to find all de…
mmsmits Mar 15, 2024
f19783a
improvement of checking for correct filter is moved to switch statement
mmsmits Mar 15, 2024
931d41e
also copies the properties of CodeSystem concepts to ValueSet contains
mmsmits Mar 15, 2024
5e3621d
make list of CodeSystems to complex to filter a setting
mmsmits Mar 15, 2024
3ceabd7
handles filtering on incomplete codesystems
mmsmits Mar 18, 2024
b82ee16
fix test
mmsmits Mar 18, 2024
9f2c78d
remove specific check for certain codesystems. Now check if the CodeS…
mmsmits Mar 19, 2024
fbae182
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits Mar 19, 2024
9ec0acd
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
ewoutkramer Mar 19, 2024
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
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Conformance/Model/ValueSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public bool CodeInExpansion(String code, string? system = null)
return null;
}

public ValueSet.ContainsComponent FindInExpansion(String code, string? system = null)
public ValueSet.ContainsComponent? FindInExpansion(String code, string? system = null)
{
ensureExpansion();

Expand Down
64 changes: 42 additions & 22 deletions src/Hl7.Fhir.Conformance/Model/ValueSetExpansionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
using Hl7.Fhir.Model;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

#nullable enable

namespace Hl7.Fhir.Model
{
public static class ValueSetExpansionExtensions
{
public static ValueSet.ContainsComponent FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string system=null)

public static ValueSet.ContainsComponent? FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string? system = null)
{
foreach (var contains in cnt)
{
Expand All @@ -22,7 +21,7 @@ public static ValueSet.ContainsComponent FindCode(this IEnumerable<ValueSet.Cont
}


public static ValueSet.ContainsComponent FindCode(this ValueSet.ContainsComponent contains, string code, string system=null)
public static ValueSet.ContainsComponent? FindCode(this ValueSet.ContainsComponent contains, string code, string? system = null)
{
// Direct hit
if (code == contains.Code && (system == null || system == contains.System))
Expand All @@ -34,20 +33,41 @@ public static ValueSet.ContainsComponent FindCode(this ValueSet.ContainsComponen
else
return null;
}

internal static CodeSystem.ConceptDefinitionComponent? FindCode(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, string code)
{
foreach (var concept in concepts)
{
var predicate = () => concept.Code == code;
var result = concept.findCodeByPredicate(predicate);
if (result != null) return result;
}
return null;
}

private static CodeSystem.ConceptDefinitionComponent? findCodeByPredicate(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, Func<bool> predicate)
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var concept in concepts)
{
var result = concept.findCodeByPredicate(predicate);
if (result != null) return result;
}
return null;
}

private static CodeSystem.ConceptDefinitionComponent? findCodeByPredicate(this CodeSystem.ConceptDefinitionComponent concept, Func<bool> predicate)
{
// Direct hit
if (predicate.Invoke())
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
return concept;

// Not in this node, but this node may have child nodes to check
if (concept.Concept != null && concept.Concept.Any())
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
return concept.Concept.findCodeByPredicate(predicate);
else
return null;
}
}

//public static class ValueSetExtensionExtensions
//{
// public const string EXT_DEPRECATED = "http://hl7.org/fhir/StructureDefinition/valueset-deprecated";

// public static bool? GetDeprecated(this ValueSet.ConceptDefinitionComponent def)
// {
// return def.GetBoolExtension(EXT_DEPRECATED);
// }

// public static void SetDeprecated(this ValueSet.ConceptDefinitionComponent def, bool value)
// {
// def.SetBoolExtension(EXT_DEPRECATED, value);
// }
//}
}

#nullable restore
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.STU3/Model/ValueSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public bool CodeInExpansion(String code, string? system = null)
return null;
}

public ValueSet.ContainsComponent FindInExpansion(String code, string? system = null)
public ValueSet.ContainsComponent? FindInExpansion(String code, string? system = null)
{
ensureExpansion();

Expand Down
91 changes: 69 additions & 22 deletions src/Hl7.Fhir.STU3/Model/ValueSetExpansionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
using Hl7.Fhir.Model;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

#nullable enable

namespace Hl7.Fhir.Model
{
public static class ValueSetExpansionExtensions
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
{
public static ValueSet.ContainsComponent FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string system=null)

public static ValueSet.ContainsComponent? FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string? system = null)
{
foreach (var contains in cnt)
{
Expand All @@ -22,7 +21,7 @@ public static ValueSet.ContainsComponent FindCode(this IEnumerable<ValueSet.Cont
}


public static ValueSet.ContainsComponent FindCode(this ValueSet.ContainsComponent contains, string code, string system=null)
public static ValueSet.ContainsComponent? FindCode(this ValueSet.ContainsComponent contains, string code, string? system = null)
{
// Direct hit
if (code == contains.Code && (system == null || system == contains.System))
Expand All @@ -34,20 +33,68 @@ public static ValueSet.ContainsComponent FindCode(this ValueSet.ContainsComponen
else
return null;
}

internal static CodeSystem.ConceptDefinitionComponent? FindCode(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, string code)
{
foreach (var concept in concepts)
{
var predicate = () => concept.Code == code;
var result = concept.findCodeByPredicate(predicate);
if (result != null) return result;
}
return null;
}

private static CodeSystem.ConceptDefinitionComponent? findCodeByPredicate(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, Func<bool> predicate)
{
foreach (var concept in concepts)
{
var result = concept.findCodeByPredicate(predicate);
if (result != null) return result;
}
return null;
}

private static CodeSystem.ConceptDefinitionComponent? findCodeByPredicate(this CodeSystem.ConceptDefinitionComponent concept, Func<bool> predicate)
{
// Direct hit
if (predicate.Invoke())
return concept;

// Not in this node, but this node may have child nodes to check
if (concept.Concept != null && concept.Concept.Any())
return concept.Concept.findCodeByPredicate(predicate);
else
return null;
}


internal static List<CodeSystem.ConceptDefinitionComponent> FilterCodesByProperty(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, string property, DataType value)
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
{
Func<CodeSystem.ConceptDefinitionComponent, bool> predicate = concept => concept.Property.Any(p => p.Code == property && p.Value == value);
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
return concepts.filterCodesByPredicate(predicate);
}


private static List<CodeSystem.ConceptDefinitionComponent> filterCodesByPredicate(this IEnumerable<CodeSystem.ConceptDefinitionComponent> concepts, Func<CodeSystem.ConceptDefinitionComponent, bool> predicate)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();

foreach (var concept in concepts)
{
if (predicate(concept))
{
result.Add(concept);
}

if (concept.Concept != null)
{
result.AddRange(concept.Concept.filterCodesByPredicate(predicate));
}
}
return result;
}


}

//public static class ValueSetExtensionExtensions
//{
// public const string EXT_DEPRECATED = "http://hl7.org/fhir/StructureDefinition/valueset-deprecated";

// public static bool? GetDeprecated(this ValueSet.ConceptDefinitionComponent def)
// {
// return def.GetBoolExtension(EXT_DEPRECATED);
// }

// public static void SetDeprecated(this ValueSet.ConceptDefinitionComponent def, bool value)
// {
// def.SetBoolExtension(EXT_DEPRECATED, value);
// }
//}
}
1 change: 1 addition & 0 deletions src/Hl7.Fhir.Shims.Base/Hl7.Fhir.Shims.Base.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Specification\Source\SnapshotSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\CodeSystemFilterProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\ExpandParameters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\ExternalTerminologyService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\FallbackTerminologyService.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification.Source;
using Hl7.Fhir.Utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using T = System.Threading.Tasks;

#nullable enable

namespace Hl7.Fhir.Specification.Terminology
{

internal static class CodeSystemFilterProcessor
{
private static readonly ReadOnlyCollection<FilterOperator?> SUPPORTEDFILTERS = new(new List<FilterOperator?> { FilterOperator.IsA });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just private static readonly FilterOperator[] SUPPORTEDFILTERS = [ FilterOperator.IsA ]; ?

private const string SUBSUMEDBYCODE = "subsumedBy";

/// <summary>
/// Retrieve codes from a CodeSystem resource bases on one or multiple filters.
/// </summary>
/// <param name="codeSystemUri">Uri of the CodeSystem</param>
/// <param name="filters">Filters to be applied</param>
/// <param name="settings">ValueSetExpanderSettings </param>
/// <returns></returns>
/// <exception cref="ValueSetExpansionTooComplexException">Thrown when a filter is applied that is not supported (yet)</exception>
/// <exception cref="CodeSystemUnknownException">Thrown when no resource resolver was set in ValueSetExpanderSettings.ValueSetSource</exception>
/// <exception cref="CodeSystemUnknownException">Thrown when the requested CodeSystem can not be found by the resource resolver in ValueSetExpanderSettings</exception>
internal async static T.Task<IEnumerable<ValueSet.ContainsComponent>> FilterConceptsFromCodeSystem(string codeSystemUri, List<ValueSet.FilterComponent> filters, ValueSetExpanderSettings settings)
{
if (codeSystemUri == "http://snomed.info/sct" || codeSystemUri == "http://loinc.org")
mmsmits marked this conversation as resolved.
Show resolved Hide resolved
{
throw new ValueSetExpansionTooComplexException($"Locally filter codes from Codesystem {codeSystemUri} is not supported");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't use the term "locally", since if this is run on a remote server, it's not local for the caller. Just mentiond that these (complex) valuesets are not supported, I'd say.

}

if (filters.Any(f => !SUPPORTEDFILTERS.Contains(f.Op)))
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
{
string supportedFiltersString = string.Join(", ", SUPPORTEDFILTERS.Select(f => $"'{f.GetLiteral()}'"));
throw new ValueSetExpansionTooComplexException($"ConceptSets with a filter other than {SUPPORTEDFILTERS} are not yet supported.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you wanted to use {supportedFiltersString} here, not {SUPPORTEDFILTERS}!

}

if (settings.ValueSetSource == null)
throw Error.InvalidOperation($"No valueset resolver available to resolve codesystem '{codeSystemUri}', so the expansion cannot be completed.");

var codeSystem = await settings.ValueSetSource.AsAsync().FindCodeSystemAsync(codeSystemUri).ConfigureAwait(false)
?? throw new CodeSystemUnknownException($"Cannot find codesystem '{codeSystemUri}', so the defined filter(s) cannot be applied.");

var result = applyFilters(filters, codeSystem);

return result.Select(c => c.ToContainsComponent(codeSystem, settings));
}

private static List<CodeSystem.ConceptDefinitionComponent> applyFilters(List<ValueSet.FilterComponent> filters, CodeSystem codeSystem)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();
var properties = codeSystem.Property;
bool first = true;

ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
foreach (var filter in filters)
{
//If multiple filters are specified within the include, they SHALL all be true. So the second filter, can just filter the result of the first etc.
var newResult = first
? applyFilter(codeSystem.Concept, properties, filter)
: applyFilter(result, properties, filter);

if (first) first = false;
result = newResult.ToList();
}

return result;
}

private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter)
{
return filter.Op switch
{
FilterOperator.IsA => applyIsAFilter(concepts, properties, filter),
_ => throw new InvalidOperationException("no filter was selected")
};
}

private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyIsAFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();

//find descendants based on subsumedBy
if (properties.Any(p => p.Code == SUBSUMEDBYCODE))
{
//first find the parent itself (if it's in the CodeSystem)
if (concepts.FindCode(filter.Value) is { } concept)
result.Add(concept);

//Create a dictionary which lists children by parent.
var dict = new Dictionary<string, List<CodeSystem.ConceptDefinitionComponent>>();
addConceptsToSubsumedByDict(concepts, dict);

//find descendants based on that dictionary
var descendants = applySubsumedBy(dict, filter);
result.AddRange(descendants);
}
else
{
//SubsumedBy is not used, we should only check for a nested hierarchy, and include the code and it's descendants
if (concepts.FindCode(filter.Value) is { } concept)
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
result.Add(concept);
}
return result;
}

private static void addConceptsToSubsumedByDict(List<CodeSystem.ConceptDefinitionComponent> concepts, Dictionary<string, List<CodeSystem.ConceptDefinitionComponent>> dict)
{
foreach (var concept in concepts)
{
//find all properties that are parents.
var parents = concept.Property
.Where(p => p.Code == SUBSUMEDBYCODE && p.Value is Code && ((Code)p.Value).Value is not null)
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
.Select(p => ((Code)p.Value).Value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do most of the code in this function with the LINQ GroupBy function, which returns an ILookup, just what you need here, and probably more efficient.


//Find all their children
foreach (var parent in parents)
{
if (dict.ContainsKey(parent))
{
dict[parent].Add(concept);
}
else
{
dict.Add(parent, new List<CodeSystem.ConceptDefinitionComponent> { concept });
}
}

if (concept.Concept is not null && concept.Concept.Any())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple places in the code where we need the whole list of the code + its children. Maybe you can make a function "Flatten" that returns an enumerable (with our without the parent itself, I don't know whats best). Then you can write both the filtering and the grouping stuff in this function using that Flatten.

{
addConceptsToSubsumedByDict(concept.Concept, dict);
}
}
}

private static List<CodeSystem.ConceptDefinitionComponent> applySubsumedBy(Dictionary<string, List<CodeSystem.ConceptDefinitionComponent>> dict, ValueSet.FilterComponent filter)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();
var root = filter.Value;
if (root != null)
{
addDescendants(dict, root, result);
}
return result;
}

//recursively loop through all the children to eventually find all descendants.
private static void addDescendants(Dictionary<string, List<CodeSystem.ConceptDefinitionComponent>> dict, string parent, List<CodeSystem.ConceptDefinitionComponent> result)
{
if (dict.TryGetValue(parent, out var descendants))
{
foreach (var descendant in descendants)
{
result.Add(descendant);
parent = descendant.Code;
addDescendants(dict, parent, result);
}
}
}
}
}

#nullable restore
Loading