ELM Tutorial¶
A step-by-step guide to working with ELM (Expression Logical Model).
What You'll Learn¶
- Loading ELM files
- Evaluating definitions
- Working with parameters
- Using patient data
- Converting CQL to ELM
- Building an evaluation pipeline
Part 1: Your First ELM File¶
What is ELM?¶
ELM (Expression Logical Model) is the compiled representation of CQL. When CQL source code is compiled, it produces ELM JSON that can be:
- Executed by any ELM-compatible engine
- Shared between systems without recompilation
- Stored for efficient repeated execution
Creating a Simple ELM File¶
Create a file called hello.elm.json:
{
"library": {
"identifier": {
"id": "HelloWorld",
"version": "1.0"
},
"statements": {
"def": [
{
"name": "Greeting",
"expression": {
"type": "Literal",
"valueType": "{urn:hl7-org:elm-types:r1}String",
"value": "Hello, ELM!"
}
},
{
"name": "Answer",
"expression": {
"type": "Literal",
"valueType": "{urn:hl7-org:elm-types:r1}Integer",
"value": "42"
}
}
]
}
}
}
Loading and Evaluating¶
Using the CLI:
# Load and show info
fhir elm load hello.elm.json
# Evaluate a definition
fhir elm eval hello.elm.json Greeting
# Output: Hello, ELM!
fhir elm eval hello.elm.json Answer
# Output: 42
# Evaluate all definitions
fhir elm run hello.elm.json
Using Python:
from fhirkit.engine.elm import ELMEvaluator
evaluator = ELMEvaluator()
# Load the ELM file
library = evaluator.load("hello.elm.json")
# Evaluate definitions
greeting = evaluator.evaluate_definition("Greeting")
print(greeting) # Hello, ELM!
answer = evaluator.evaluate_definition("Answer")
print(answer) # 42
Part 2: Arithmetic Operations¶
Adding Numbers¶
Create math.elm.json:
{
"library": {
"identifier": {"id": "MathExamples", "version": "1.0"},
"statements": {
"def": [
{
"name": "Sum",
"expression": {
"type": "Add",
"operand": [
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "10"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "20"}
]
}
},
{
"name": "Product",
"expression": {
"type": "Multiply",
"operand": [
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "6"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "7"}
]
}
},
{
"name": "Complex",
"expression": {
"type": "Add",
"operand": [
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "1"},
{
"type": "Multiply",
"operand": [
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "2"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "3"}
]
}
]
}
}
]
}
}
}
Try it:
evaluator = ELMEvaluator()
evaluator.load("math.elm.json")
print(evaluator.evaluate_definition("Sum")) # 30
print(evaluator.evaluate_definition("Product")) # 42
print(evaluator.evaluate_definition("Complex")) # 7 (1 + 2*3)
Exercise 1¶
Create an ELM file that calculates:
- Difference: 100 - 37
- Quotient: 144 / 12
- Power: 2^10
Part 3: Working with Parameters¶
Defining Parameters¶
Parameters allow runtime configuration. Create params.elm.json:
{
"library": {
"identifier": {"id": "ParameterExample", "version": "1.0"},
"parameters": [
{
"name": "Multiplier",
"parameterType": "{urn:hl7-org:elm-types:r1}Integer",
"default": {"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "10"}
},
{
"name": "BaseValue",
"parameterType": "{urn:hl7-org:elm-types:r1}Integer"
}
],
"statements": {
"def": [
{
"name": "Result",
"expression": {
"type": "Multiply",
"operand": [
{"type": "ParameterRef", "name": "BaseValue"},
{"type": "ParameterRef", "name": "Multiplier"}
]
}
}
]
}
}
}
Using Parameters¶
CLI:
fhir elm eval params.elm.json Result --param BaseValue=5
# Output: 50 (5 * 10, using default Multiplier)
fhir elm eval params.elm.json Result --param BaseValue=5 --param Multiplier=3
# Output: 15
Python:
evaluator = ELMEvaluator()
evaluator.load("params.elm.json")
# Use default multiplier
result = evaluator.evaluate_definition("Result", parameters={"BaseValue": 5})
print(result) # 50
# Override multiplier
result = evaluator.evaluate_definition("Result", parameters={
"BaseValue": 5,
"Multiplier": 3
})
print(result) # 15
Part 4: Referencing Definitions¶
Definitions can reference each other. Create references.elm.json:
{
"library": {
"identifier": {"id": "ReferenceExample", "version": "1.0"},
"statements": {
"def": [
{
"name": "A",
"expression": {
"type": "Literal",
"valueType": "{urn:hl7-org:elm-types:r1}Integer",
"value": "10"
}
},
{
"name": "B",
"expression": {
"type": "Literal",
"valueType": "{urn:hl7-org:elm-types:r1}Integer",
"value": "20"
}
},
{
"name": "Sum",
"expression": {
"type": "Add",
"operand": [
{"type": "ExpressionRef", "name": "A"},
{"type": "ExpressionRef", "name": "B"}
]
}
},
{
"name": "Product",
"expression": {
"type": "Multiply",
"operand": [
{"type": "ExpressionRef", "name": "A"},
{"type": "ExpressionRef", "name": "B"}
]
}
},
{
"name": "Average",
"expression": {
"type": "Divide",
"operand": [
{"type": "ExpressionRef", "name": "Sum"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Decimal", "value": "2.0"}
]
}
}
]
}
}
}
Evaluate all:
evaluator = ELMEvaluator()
evaluator.load("references.elm.json")
results = evaluator.evaluate_all_definitions()
for name, value in results.items():
print(f"{name}: {value}")
# Output:
# A: 10
# B: 20
# Sum: 30
# Product: 200
# Average: 15.0
Part 5: Conditionals¶
If-Then-Else¶
Create conditionals.elm.json:
{
"library": {
"identifier": {"id": "ConditionalExample", "version": "1.0"},
"parameters": [
{
"name": "Age",
"parameterType": "{urn:hl7-org:elm-types:r1}Integer"
}
],
"statements": {
"def": [
{
"name": "IsAdult",
"expression": {
"type": "GreaterOrEqual",
"operand": [
{"type": "ParameterRef", "name": "Age"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "18"}
]
}
},
{
"name": "Category",
"expression": {
"type": "If",
"condition": {
"type": "Less",
"operand": [
{"type": "ParameterRef", "name": "Age"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "18"}
]
},
"then": {"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}String", "value": "Minor"},
"else": {
"type": "If",
"condition": {
"type": "Less",
"operand": [
{"type": "ParameterRef", "name": "Age"},
{"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}Integer", "value": "65"}
]
},
"then": {"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}String", "value": "Adult"},
"else": {"type": "Literal", "valueType": "{urn:hl7-org:elm-types:r1}String", "value": "Senior"}
}
}
}
]
}
}
}
Try different ages:
evaluator = ELMEvaluator()
evaluator.load("conditionals.elm.json")
for age in [15, 30, 70]:
is_adult = evaluator.evaluate_definition("IsAdult", parameters={"Age": age})
category = evaluator.evaluate_definition("Category", parameters={"Age": age})
print(f"Age {age}: IsAdult={is_adult}, Category={category}")
# Output:
# Age 15: IsAdult=False, Category=Minor
# Age 30: IsAdult=True, Category=Adult
# Age 70: IsAdult=True, Category=Senior
Part 6: Patient Data¶
Using Patient Context¶
Create patient.elm.json:
{
"library": {
"identifier": {"id": "PatientExample", "version": "1.0"},
"usings": [
{
"localIdentifier": "FHIR",
"uri": "http://hl7.org/fhir",
"version": "4.0.1"
}
],
"contexts": [
{"name": "Patient"}
],
"statements": {
"def": [
{
"name": "PatientGender",
"context": "Patient",
"expression": {
"type": "Property",
"path": "gender",
"source": {"type": "ExpressionRef", "name": "Patient"}
}
},
{
"name": "PatientBirthDate",
"context": "Patient",
"expression": {
"type": "Property",
"path": "birthDate",
"source": {"type": "ExpressionRef", "name": "Patient"}
}
}
]
}
}
}
Evaluate with patient:
evaluator = ELMEvaluator()
evaluator.load("patient.elm.json")
patient = {
"resourceType": "Patient",
"id": "example",
"gender": "male",
"birthDate": "1990-05-15",
"name": [{"family": "Smith", "given": ["John"]}]
}
gender = evaluator.evaluate_definition("PatientGender", resource=patient)
birthdate = evaluator.evaluate_definition("PatientBirthDate", resource=patient)
print(f"Gender: {gender}") # male
print(f"Birth Date: {birthdate}") # 1990-05-15
Part 7: Converting CQL to ELM¶
The easiest way to create ELM is to write CQL and convert it.
Using the CLI¶
# Convert CQL to ELM
fhir elm convert library.cql -o library.elm.json
# Or use the CQL export command
fhir cql export library.cql -o library.elm.json
Using Python¶
from fhirkit.engine.elm import ELMSerializer
serializer = ELMSerializer()
cql_source = """
library MyLibrary version '1.0'
using FHIR version '4.0.1'
context Patient
define PatientAge: AgeInYears()
define IsAdult: PatientAge >= 18
define AgeCategory:
case
when PatientAge < 18 then 'Pediatric'
when PatientAge < 65 then 'Adult'
else 'Senior'
end
"""
# Convert to ELM JSON
elm_json = serializer.serialize_library_json(cql_source, indent=2)
print(elm_json)
# Save to file
with open("mylibrary.elm.json", "w") as f:
f.write(elm_json)
Using CQLEvaluator¶
from fhirkit.engine.cql import CQLEvaluator
evaluator = CQLEvaluator()
# Compile CQL
library = evaluator.compile("""
library Example version '1.0'
define Sum: 1 + 2 + 3
define Greeting: 'Hello!'
""")
# Export to ELM
elm_json = evaluator.to_elm_json(indent=2)
elm_dict = evaluator.to_elm_dict()
elm_model = evaluator.to_elm()
Part 8: Building an Evaluation Pipeline¶
Complete Example¶
from fhirkit.engine.elm import ELMEvaluator, ELMSerializer
from fhirkit.engine.cql import InMemoryDataSource
import json
# Step 1: Define CQL logic
cql_source = """
library RiskAssessment version '1.0'
using FHIR version '4.0.1'
parameter "Risk Threshold" Integer default 3
context Patient
define PatientAge: AgeInYears()
define HasDiabetes:
exists([Condition] C where C.code.coding.code = '44054006')
define HasHypertension:
exists([Condition] C where C.code.coding.code = '38341003')
define RiskScore:
(if PatientAge >= 50 then 1 else 0) +
(if HasDiabetes then 2 else 0) +
(if HasHypertension then 1 else 0)
define IsHighRisk:
RiskScore >= "Risk Threshold"
"""
# Step 2: Convert to ELM
serializer = ELMSerializer()
elm_json = serializer.serialize_library_json(cql_source)
# Step 3: Create data source with patient data
data_source = InMemoryDataSource()
# Add patient
patient = {
"resourceType": "Patient",
"id": "patient-1",
"birthDate": "1965-03-15",
"gender": "male"
}
data_source.add_resource(patient)
# Add conditions
data_source.add_resource({
"resourceType": "Condition",
"id": "cond-1",
"subject": {"reference": "Patient/patient-1"},
"code": {"coding": [{"code": "44054006", "system": "http://snomed.info/sct"}]}
})
# Step 4: Evaluate
evaluator = ELMEvaluator(data_source=data_source)
evaluator.load(elm_json)
results = evaluator.evaluate_all_definitions(resource=patient)
print("Risk Assessment Results:")
print(f" Patient Age: {results.get('PatientAge')}")
print(f" Has Diabetes: {results.get('HasDiabetes')}")
print(f" Has Hypertension: {results.get('HasHypertension')}")
print(f" Risk Score: {results.get('RiskScore')}")
print(f" Is High Risk: {results.get('IsHighRisk')}")
Output:
Risk Assessment Results:
Patient Age: 59
Has Diabetes: True
Has Hypertension: False
Risk Score: 3
Is High Risk: True
Part 9: Exercises¶
Exercise 1: Temperature Converter¶
Create an ELM file that:
1. Takes a Celsius parameter
2. Calculates Fahrenheit using the formula: F = C * 9/5 + 32
3. Determines if it's Freezing (below 0C), Cold (0-15C), Warm (15-25C), or Hot (above 25C)
Exercise 2: BMI Calculator¶
Create CQL that:
1. Takes WeightKg and HeightCm parameters
2. Calculates BMI: weight / (height/100)^2
3. Returns a category: Underweight (<18.5), Normal (18.5-25), Overweight (25-30), Obese (>30)
Convert it to ELM and evaluate with different values.
Exercise 3: Patient Risk Assessment¶
Using the pipeline from Part 8: 1. Add more risk factors (smoking, family history) 2. Add weighted scoring 3. Evaluate across multiple patients
Summary¶
You've learned how to:
- Load ELM from files and JSON strings
- Evaluate definitions with and without parameters
- Use patient data for clinical logic
- Convert CQL to ELM using CLI and Python
- Build pipelines for real-world applications
Next Steps¶
- ELM Guide - Conceptual overview
- ELM API - Complete Python API
- ELM Reference - All expression types
- CQL Tutorial - Learn CQL syntax