Skip to content

Commit 95f97b4

Browse files
author
alex-omophub
committed
Release version 1.6.0 with FHIR-to-OMOP concept resolution features
- Introduced `fhir` resource for resolving FHIR coded values to OMOP standard concepts, including methods `resolve()`, `resolve_batch()`, and `resolve_codeable_concept()`. - Updated DESCRIPTION and README to reflect new features and improved functionality. - Enhanced test coverage for FHIR resolution methods and added relevant examples in the documentation.
1 parent 322ecd5 commit 95f97b4

16 files changed

Lines changed: 1175 additions & 5 deletions

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
Package: omophub
22
Title: R Client for the 'OMOPHub' Medical Vocabulary API
3-
Version: 1.5.0
3+
Version: 1.6.0
44
Authors@R: c(
55
person("Alex", "Chen", email = "alex@omophub.com", role = c("aut", "cre", "cph")),
66
person("Observational Health Data Science and Informatics", role = c("cph"))
77
)
88
Description: Provides an R interface to the 'OMOPHub' API for accessing
99
'OHDSI ATHENA' standardized medical vocabularies. Supports concept search,
1010
semantic search using neural embeddings, concept similarity, vocabulary
11-
exploration, hierarchy navigation, relationship queries, and concept
12-
mappings with automatic pagination and rate limiting.
11+
exploration, hierarchy navigation, relationship queries, concept
12+
mappings, and FHIR-to-OMOP concept resolution with automatic pagination.
1313
License: MIT + file LICENSE
1414
URL: https://github.com/omopHub/omophub-R,
1515
https://docs.omophub.com,

NEWS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
# omophub 1.6.0
2+
3+
## New Features
4+
5+
* **FHIR-to-OMOP Concept Resolver** (`client$fhir`): Translate FHIR coded
6+
values into OMOP standard concepts, CDM target tables, and optional Phoebe
7+
recommendations in a single API call.
8+
9+
- `resolve()`: Resolve a single FHIR `Coding` (system URI + code) or
10+
text-only input via semantic search fallback. Returns the standard
11+
concept, target CDM table, domain alignment check, and optional mapping
12+
quality signal.
13+
14+
- `resolve_batch()`: Batch-resolve up to 100 FHIR codings per request with
15+
inline per-item error reporting.
16+
17+
- `resolve_codeable_concept()`: Resolve a FHIR `CodeableConcept` with
18+
multiple codings. Automatically picks the best match per OHDSI vocabulary
19+
preference (SNOMED > RxNorm > LOINC > CVX > ICD-10). Falls back to the
20+
`text` field via semantic search when no coding resolves.
21+
22+
## Tests
23+
24+
* Improved test coverage
25+
126
# omophub 1.5.0
227

328
## New Features

R/client.R

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ OMOPHubClient <- R6::R6Class(
142142
private$.mappings <- MappingsResource$new(private$.base_req)
143143
}
144144
private$.mappings
145+
},
146+
147+
#' @field fhir Access to FHIR-to-OMOP Concept Resolver operations.
148+
fhir = function() {
149+
if (is.null(private$.fhir)) {
150+
private$.fhir <- FhirResource$new(private$.base_req)
151+
}
152+
private$.fhir
145153
}
146154
),
147155
private = list(
@@ -159,6 +167,7 @@ OMOPHubClient <- R6::R6Class(
159167
.domains = NULL,
160168
.hierarchy = NULL,
161169
.relationships = NULL,
162-
.mappings = NULL
170+
.mappings = NULL,
171+
.fhir = NULL
163172
)
164173
)

R/fhir.R

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#' FHIR-to-OMOP Concept Resolver
2+
#'
3+
#' @description
4+
#' R6 class providing access to the FHIR-to-OMOP Concept Resolver endpoints.
5+
#' Translates FHIR coded values (system URI + code) into OMOP standard
6+
#' concepts, CDM target tables, and optional Phoebe recommendations.
7+
#'
8+
#' @details
9+
#' Access via the `fhir` active binding on an `OMOPHubClient`:
10+
#' ```r
11+
#' client <- OMOPHubClient$new(api_key = "oh_xxx")
12+
#' result <- client$fhir$resolve(
13+
#' system = "http://snomed.info/sct",
14+
#' code = "44054006",
15+
#' resource_type = "Condition"
16+
#' )
17+
#' result$data$resolution$target_table
18+
#' # "condition_occurrence"
19+
#' ```
20+
#'
21+
#' @keywords internal
22+
FhirResource <- R6::R6Class(
23+
"FhirResource",
24+
public = list(
25+
#' @description
26+
#' Create a new FhirResource.
27+
#' @param base_req Base httr2 request object.
28+
initialize = function(base_req) {
29+
private$.base_req <- base_req
30+
},
31+
32+
#' @description
33+
#' Resolve a single FHIR Coding to an OMOP standard concept.
34+
#'
35+
#' Provide at least one of (`system` + `code`), (`vocabulary_id` + `code`),
36+
#' or `display`.
37+
#'
38+
#' @param system FHIR code system URI (e.g. `"http://snomed.info/sct"`).
39+
#' @param code Code value from the FHIR Coding.
40+
#' @param display Human-readable text (semantic search fallback).
41+
#' @param vocabulary_id Direct OMOP vocabulary_id, bypasses URI resolution.
42+
#' @param resource_type FHIR resource type (e.g. `"Condition"`, `"Observation"`).
43+
#' @param include_recommendations Logical. Include Phoebe recommendations. Default `FALSE`.
44+
#' @param recommendations_limit Integer. Max recommendations (1-20). Default `5L`.
45+
#' @param include_quality Logical. Include mapping quality signal. Default `FALSE`.
46+
#'
47+
#' @returns A list with `input` and `resolution` containing source/standard
48+
#' concepts, target CDM table, and optional enrichments.
49+
resolve = function(system = NULL,
50+
code = NULL,
51+
display = NULL,
52+
vocabulary_id = NULL,
53+
resource_type = NULL,
54+
include_recommendations = FALSE,
55+
recommendations_limit = 5L,
56+
include_quality = FALSE) {
57+
body <- compact_list(
58+
system = system,
59+
code = code,
60+
display = display,
61+
vocabulary_id = vocabulary_id,
62+
resource_type = resource_type
63+
)
64+
if (isTRUE(include_recommendations)) {
65+
body$include_recommendations <- TRUE
66+
body$recommendations_limit <- as.integer(recommendations_limit)
67+
}
68+
if (isTRUE(include_quality)) {
69+
body$include_quality <- TRUE
70+
}
71+
72+
perform_post(private$.base_req, "fhir/resolve", body = body)
73+
},
74+
75+
#' @description
76+
#' Batch-resolve up to 100 FHIR Codings.
77+
#'
78+
#' Failed items are reported inline without failing the batch.
79+
#'
80+
#' @param codings A list of coding lists, each with optional elements
81+
#' `system`, `code`, `display`, `vocabulary_id`.
82+
#' @param resource_type FHIR resource type applied to all codings.
83+
#' @param include_recommendations Logical. Default `FALSE`.
84+
#' @param recommendations_limit Integer. Default `5L`.
85+
#' @param include_quality Logical. Default `FALSE`.
86+
#'
87+
#' @returns A list with `results` (per-item) and `summary`
88+
#' (total/resolved/failed).
89+
resolve_batch = function(codings,
90+
resource_type = NULL,
91+
include_recommendations = FALSE,
92+
recommendations_limit = 5L,
93+
include_quality = FALSE) {
94+
stopifnot(is.list(codings), length(codings) >= 1, length(codings) <= 100)
95+
96+
body <- list(codings = codings)
97+
if (!is.null(resource_type)) body$resource_type <- resource_type
98+
if (isTRUE(include_recommendations)) {
99+
body$include_recommendations <- TRUE
100+
body$recommendations_limit <- as.integer(recommendations_limit)
101+
}
102+
if (isTRUE(include_quality)) body$include_quality <- TRUE
103+
104+
perform_post(private$.base_req, "fhir/resolve/batch", body = body)
105+
},
106+
107+
#' @description
108+
#' Resolve a FHIR CodeableConcept with vocabulary preference.
109+
#'
110+
#' Picks the best match per OHDSI preference order
111+
#' (SNOMED > RxNorm > LOINC > CVX > ICD-10). Falls back to `text`
112+
#' via semantic search if no coding resolves.
113+
#'
114+
#' @param coding A list of coding lists, each with `system`, `code`,
115+
#' and optional `display`.
116+
#' @param text Optional CodeableConcept.text for semantic fallback.
117+
#' @param resource_type FHIR resource type.
118+
#' @param include_recommendations Logical. Default `FALSE`.
119+
#' @param recommendations_limit Integer. Default `5L`.
120+
#' @param include_quality Logical. Default `FALSE`.
121+
#'
122+
#' @returns A list with `best_match`, `alternatives`, and `unresolved`.
123+
resolve_codeable_concept = function(coding,
124+
text = NULL,
125+
resource_type = NULL,
126+
include_recommendations = FALSE,
127+
recommendations_limit = 5L,
128+
include_quality = FALSE) {
129+
stopifnot(is.list(coding), length(coding) >= 1, length(coding) <= 20)
130+
131+
body <- list(coding = coding)
132+
if (!is.null(text)) body$text <- text
133+
if (!is.null(resource_type)) body$resource_type <- resource_type
134+
if (isTRUE(include_recommendations)) {
135+
body$include_recommendations <- TRUE
136+
body$recommendations_limit <- as.integer(recommendations_limit)
137+
}
138+
if (isTRUE(include_quality)) body$include_quality <- TRUE
139+
140+
perform_post(private$.base_req, "fhir/resolve/codeable-concept", body = body)
141+
}
142+
),
143+
private = list(
144+
.base_req = NULL
145+
)
146+
)
147+
148+
149+
#' Helper to remove NULL entries from a named list.
150+
#' @param ... Named arguments.
151+
#' @returns A list with NULL entries removed.
152+
#' @keywords internal
153+
compact_list <- function(...) {
154+
args <- list(...)
155+
args[!vapply(args, is.null, logical(1))]
156+
}

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,48 @@ results <- client$search$bulk_semantic(list(
134134
), defaults = list(threshold = 0.5, page_size = 10))
135135
```
136136

137+
## FHIR-to-OMOP Resolution
138+
139+
Resolve FHIR coded values to OMOP standard concepts in one call:
140+
141+
```r
142+
# Single FHIR Coding -> OMOP concept + CDM target table
143+
result <- client$fhir$resolve(
144+
system = "http://snomed.info/sct",
145+
code = "44054006",
146+
resource_type = "Condition"
147+
)
148+
result$resolution$target_table
149+
# [1] "condition_occurrence"
150+
151+
# ICD-10-CM -> traverses 'Maps to' automatically
152+
result <- client$fhir$resolve(
153+
system = "http://hl7.org/fhir/sid/icd-10-cm",
154+
code = "E11.9"
155+
)
156+
result$resolution$standard_concept$vocabulary_id
157+
# [1] "SNOMED"
158+
159+
# Batch resolve up to 100 codings
160+
batch <- client$fhir$resolve_batch(list(
161+
list(system = "http://snomed.info/sct", code = "44054006"),
162+
list(system = "http://loinc.org", code = "2339-0"),
163+
list(system = "http://www.nlm.nih.gov/research/umls/rxnorm", code = "197696")
164+
))
165+
cat(sprintf("Resolved %d/%d\n", batch$summary$resolved, batch$summary$total))
166+
167+
# CodeableConcept with vocabulary preference (SNOMED wins over ICD-10)
168+
result <- client$fhir$resolve_codeable_concept(
169+
coding = list(
170+
list(system = "http://snomed.info/sct", code = "44054006"),
171+
list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9")
172+
),
173+
resource_type = "Condition"
174+
)
175+
result$best_match$resolution$source_concept$vocabulary_id
176+
# [1] "SNOMED"
177+
```
178+
137179
## Use Cases
138180

139181
### ETL & Data Pipelines
@@ -226,6 +268,7 @@ concepts_df %>%
226268
| `mappings` | Cross-vocabulary mappings | `get()`, `map()` |
227269
| `vocabularies` | Vocabulary metadata | `list()`, `get()`, `stats()` |
228270
| `domains` | Domain information | `list()`, `get()`, `concepts()` |
271+
| `fhir` | FHIR-to-OMOP resolution | `resolve()`, `resolve_batch()`, `resolve_codeable_concept()` |
229272

230273
## Pagination
231274

0 commit comments

Comments
 (0)