Create and Encrypt Medical Data
The example in this section demonstrates how to register a medical examination, where some medical exams are performed and a diagnosis is elaborated.
Initiating a Medical Examination​
In Cardinal, the concept of a medical examination is represented through the Contact
entity. Generally, a Contact
represents a moment when medical data are produced and involves a patient and, usually, one or more healthcare actors.
For more details, check the Contact explanation.
As the first step, the user can choose to use an existing Patient or create a new one:
- Kotlin
- Python
- Typescript
print("Insert the id of a Patient (blank to create a new one): ")
val patientId = readlnOrNull()
val patient = if (patientId.isNullOrBlank()) {
sdk.patient.createPatient(
DecryptedPatient(
id = UUID.randomUUID().toString(),
firstName = "Annabelle",
lastName = "Hall",
).let { sdk.patient.withEncryptionMetadata(it) }
)
} else {
sdk.patient.getPatient(patientId)
}
patient_id = input("Insert the id of a Patient (blank to create a new one): ")
if len(patient_id) == 0:
patient = sdk.patient.create_patient_blocking(
sdk.patient.with_encryption_metadata_blocking(
DecryptedPatient(
id=str(uuid.uuid4()),
first_name="Annabelle",
last_name="Hall"
)
)
)
else:
patient = sdk.patient.get_patient_blocking(patient_id)
const patientId = await readLn("Insert the id of a Patient (blank to create a new one): ")
let patient: DecryptedPatient
if(patientId.length === 0) {
patient = await sdk.patient.createPatient(
await sdk.patient.withEncryptionMetadata(
new DecryptedPatient({
id: uuid(),
firstName: "Annabelle",
lastName: "Hall",
})
)
)
} else {
patient = await sdk.patient.getPatient(patientId)
}
Next, a new Contact
is instantiated with a custom description provided by the user:
- Kotlin
- Python
- Typescript
val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
print("Examination description: ")
val description = readln().trim()
val contact = DecryptedContact(
id = UUID.randomUUID().toString(),
descr = description,
openingDate = LocalDateTime.now().format(formatter).toLong()
)
description = input("Examination description: ")
contact = DecryptedContact(
id=str(uuid.uuid4()),
descr=description,
opening_date=int(datetime.now().strftime("%Y%m%d%H%M%S"))
)
const description = await readLn("Examination description: ")
const contact = new DecryptedContact({
id: uuid(),
descr: description,
openingDate: currentFuzzyDate()
})
A Contact
is an encryptable entity, so a DecryptedContact
is used when instantiating it. Besides the description,
both the id
and openingDate
must be set. The openingDate
represents the moment when the medical examination
starts. Generally, it marks the beginning of the event during which medical data are created.
Being an encryptable entity, the encryption metadata need to be initialized before creating the Contact
, just as with the Patient
:
- Kotlin
- Python
- Typescript
val contactWithMetadata = sdk.contact.withEncryptionMetadata(contact, patient)
contact_with_metadata = sdk.contact.with_encryption_metadata_blocking(contact, patient)
const contactWithMetadata = await sdk.contact.withEncryptionMetadata(contact, patient)
In this case, the initialization step differs slightly from that of the patient. The function takes two parameters as input: the contact itself and a patient. This is because the metadata will also include a link to the Patient who is the subject of this examination. To prevent data leaks, this link is encrypted, and only users with access to the contact will be able to decipher it.
Finally, the Contact
can be encrypted and stored in the cloud:
- Kotlin
- Python
- Typescript
val createdContact = sdk.contact.createContact(contactWithMetadata)
created_contact = sdk.contact.create_contact_blocking(contact_with_metadata)
const createdContact = await sdk.contact.createContact(contactWithMetadata)
Registering Medical Data​
After the Contact
is created, you can add medical data to it. Medical information is registered using a
Service
encryptable entity nested within a Contact
. In the following examples, data in different formats will be
added to the contact that was just created.
Creating Scalar Medical Data (Blood Pressure)​
The first piece of information added to the contact is a blood pressure measurement. In the following snippet, a
Service
(using its DecryptedService
variation, since Service
is an encryptable entity) is instantiated with the
result of the exam:
- Kotlin
- Python
- Typescript
val bloodPressureService = DecryptedService(
id = UUID.randomUUID().toString(),
label = "Blood pressure",
identifier = listOf(Identifier(system = "cardinal", value = "bloodPressure")),
content = mapOf(
"en" to DecryptedContent(
measureValue = Measure(
value = Random.nextInt(80, 120).toDouble(),
unit = "mmHg"
)
)
)
)
blood_pressure_service = DecryptedService(
id=str(uuid.uuid4()),
label="Blood pressure",
identifier=[Identifier(system="cardinal", value="bloodPressure")],
content={
"en": DecryptedContent(
measure_value=Measure(
value=float(random.randint(80, 120)),
unit="mmHg"
)
)
}
)
const bloodPressureService = new DecryptedService({
id: uuid(),
label: "Blood pressure",
identifier: [new Identifier({system: "cardinal", value: "bloodPressure"})],
content: {
"en": new DecryptedContent({
measureValue: new Measure({
value: random(80, 120),
unit: "mmHg"
})
})
}
})
In this case, a free-text label
provides a description for the Service
, and an identifier
allows for a
more structured labeling.
When adding sensitive information to an encryptable entity, always remember that not all fields are encrypted. You can customize the encrypted fields as explained in this how to.
The actual measurement is stored in the content
of the Service
. This field is a map that associates an
ISO language code with Content
. In this case,
the content contains a measure value that holds the blood pressure result and its unit.
The Service
can now be added to the existing Contact
:
- Kotlin
- Python
- Typescript
val contactWithBloodPressure = sdk.contact.modifyContact(
createdContact.copy(
services = setOf(bloodPressureService)
)
)
created_contact.services = [blood_pressure_service]
contact_with_blood_pressure = sdk.contact.modify_contact_blocking(
created_contact
)
const contactWithBloodPressure = await sdk.contact.modifyContact(
new DecryptedContact({
...createdContact,
services: [...createdContact.services, bloodPressureService]
})
)
It is worth noting that even though Service
is an encryptable entity, there is no need to call the
withEncryptionMetadata
method because the entity is nested within another encryptable entity and will inherit
its encryption metadata. This means that if the enclosing entity was shared with another user,
the nested entity will automatically be shared as well.
Creating Signal-like Medical Data (Electrocardiography)​
A Service
can also hold time-series data, signals, and, in general, vector-like data. In the following example,
the resulting signal from an ECG (Electrocardiography) exam is
added to the Contact
through a Service
:
- Kotlin
- Python
- Typescript
val ecgSignal = List(10) { Random.nextInt(0, 100) / 100.0 }
val heartRateService = DecryptedService(
id = UUID.randomUUID().toString(),
identifier = listOf(Identifier(system = "cardinal", value = "ecg")),
label = "Heart rate",
content = mapOf(
"en" to DecryptedContent(
timeSeries = TimeSeries(
samples = listOf(ecgSignal)
)
)
)
)
val contactWithECG = sdk.contact.modifyContact(
contactWithBloodPressure.copy(
services = contactWithBloodPressure.services + heartRateService
)
)
ecg_signal = [round(float(random.uniform(0, 1)), 2) for _ in range(10)]
heart_rate_service = DecryptedService(
id=str(uuid.uuid4()),
identifier=[Identifier(system="cardinal", value="ecg")],
label="Heart rate",
content={
"en": DecryptedContent(
time_series=TimeSeries(
samples=[ecg_signal]
)
)
}
)
contact_with_blood_pressure.services = contact_with_blood_pressure.services + [heart_rate_service]
contact_with_ecg = sdk.contact.modify_contact_blocking(contact_with_blood_pressure)
const ecgSignal = Array.from({ length: 10 }, () => random(0, 100)/100.0 )
const heartRateService = new DecryptedService({
id: uuid(),
identifier: [new Identifier({system: "cardinal", value: "ecg"})],
label: "Heart rate",
content: {
"en": new DecryptedContent({
timeSeries: new TimeSeries({
samples: [ecgSignal]
})
})
}
})
const contactWithECG = await sdk.contact.modifyContact(
new DecryptedContact({
...contactWithBloodPressure,
services: [...contactWithBloodPressure.services, heartRateService]
})
)
The structure of this Service
is almost identical to that of the previous example, with the only difference being
that the medical data are stored as timeSeries
in the Content instead of measureValue
. A TimeSeries
entity
can contain both 1-dimensional and 2-dimensional signals, as well as aggregated data such as minimum, maximum, and
average values.
Creating Medical Image Data​
Due to their larger size, the process of uploading medical images (such as those from X-Ray or CT exams, as well as simple photos) differs from uploading single measurements or signals. This difference is intended to avoid performance loss when querying and retrieving entities that contain large files.
A Content
has a binaryData
field that can be used to store binary data, but for the aforementioned reasons,
it should not be used to store large amounts of data.
The first step in uploading a medical image (or another large file) is to create a new Document
entity.
A Document
is an encryptable entity that represents medical documents (e.g., reports, certificates, images) in any format.
- Kotlin
- Python
- Typescript
val document = DecryptedDocument(
id = UUID.randomUUID().toString(),
documentType = DocumentType.Labresult
)
document = DecryptedDocument(
id=str(uuid.uuid4()),
document_type=DocumentType.Labresult
)
const document = new DecryptedDocument({
id: uuid(),
documentType: DocumentType.Labresult
})
In this example, a new DecryptedDocument
is instantiated with the type set to a laboratory result. Since a Document
is encryptable, the encryption metadata must be initialized before it is created on the cloud.
- Kotlin
- Python
- Typescript
val createdDocument = sdk.document.createDocument(
sdk.document.withEncryptionMetadata(document, null)
)
created_document = sdk.document.create_document_blocking(
sdk.document.with_encryption_metadata_blocking(document, None)
)
const createdDocument = await sdk.document.createDocument(
await sdk.document.withEncryptionMetadata(document, null)
)
Note that in this case, the withEncryptionMetadata
method takes a null second parameter. This is because there is no
need to link the Document
directly to the Patient
, as the document will be linked to a Service
that, in turn,
will be linked to the Patient
.
Next, you can load the image as an attachment to the Document
. A Document
can have a single main attachment and
multiple secondary attachments. In this case, an "image" is loaded as the main attachment to the document.
- Kotlin
- Python
- Typescript
val xRayImage = Random.nextBytes(100)
val documentWithAttachment = sdk.document.encryptAndSetMainAttachment(
document = createdDocument,
utis = listOf("public.tiff"),
attachment = xRayImage
)
x_ray_image = bytearray(secrets.token_bytes(100))
document_with_attachment = sdk.document.encrypt_and_set_main_attachment_blocking(
document=created_document,
utis=["public.tiff"],
attachment=x_ray_image
)
const xRayImage = new Int8Array(100)
for (let i = 0; i < 100; i++) {
xRayImage[i] = random(-127, 128)
}
const documentWithAttachment = await sdk.document.encryptAndSetMainAttachment(
createdDocument,
["public.tiff"],
xRayImage
)
The bytes composing the image are encrypted and set as the attachment of the Document
. Information about the
UTI of the attachment is also set.
Finally, it is possible to link this Document
with a new Service
representing the X-Ray image and add it to the Contact
.
- Kotlin
- Python
- Typescript
val xRayService = DecryptedService(
id = UUID.randomUUID().toString(),
label = "X-Ray image",
identifier = listOf(Identifier(system = "cardinal", value = "xRay")),
content = mapOf(
"en" to DecryptedContent(
documentId = documentWithAttachment.id
)
)
)
val contactWithImage = sdk.contact.modifyContact(
contactWithECG.copy(
services = contactWithECG.services + xRayService
)
)
x_ray_service = DecryptedService(
id=str(uuid.uuid4()),
label="X-Ray image",
identifier=[Identifier(system="cardinal", value="xRay")],
content={
"en": DecryptedContent(
document_id=document_with_attachment.id
)
}
)
contact_with_ecg.services = contact_with_ecg.services + [x_ray_service]
contact_with_image = sdk.contact.modify_contact_blocking(contact_with_ecg)
const xRayService = new DecryptedService({
id: uuid(),
label: "X-Ray image",
identifier: [new Identifier({system: "cardinal", value: "xRay"})],
content: {
"en": new DecryptedContent({
documentId: documentWithAttachment.id
})
}
})
const contactWithImage = await sdk.contact.modifyContact(
new DecryptedContact({
...contactWithECG,
services: [...contactWithECG.services, xRayService]
})
)
Adding a Diagnosis​
Diagnoses and other medical contexts that define the health condition of a patient are represented by a
HealthElement
encryptable entity. In this example, the user will create a HealthElement
containing the
diagnosis elaborated after the examination.
- Kotlin
- Python
- Typescript
print("What is the diagnosis?: ")
val diagnosis = readln().trim()
val healthElement = DecryptedHealthElement(
id = UUID.randomUUID().toString(),
descr = diagnosis
)
val createdDiagnosis = sdk.healthElement.createHealthElement(
sdk.healthElement.withEncryptionMetadata(healthElement, patient)
)
diagnosis = input("What is the diagnosis?: ")
health_element = DecryptedHealthElement(
id=str(uuid.uuid4()),
descr=diagnosis
)
created_diagnosis = sdk.health_element.create_health_element_blocking(
sdk.health_element.with_encryption_metadata_blocking(health_element, patient)
)
const diagnosis = await readLn("What is the diagnosis?: ")
const healthElement = new DecryptedHealthElement({
id: uuid(),
descr: diagnosis
})
const createdDiagnosis = await sdk.healthElement.createHealthElement(
await sdk.healthElement.withEncryptionMetadata(healthElement, patient)
)
A HealthElement
is an encryptable entity, so like a Contact
, first a DecryptedHealthElement
is instantiated with the desired information. Then, the encryption metadata are initialized, linking the
HealthElement
to a Patient and sharing the entity with the current user. Finally, the entity is created.
It is possible to associate the HealthElement
with a Contact
by linking it to a SubContact
:
- Kotlin
- Python
- Typescript
val contactWithDiagnosis = sdk.contact.modifyContact(
contactWithImage.copy(
subContacts = setOf(DecryptedSubContact(
descr = "Diagnosis",
healthElementId = createdDiagnosis.id
))
)
)
contact_with_image.sub_contacts = [
DecryptedSubContact(
descr="Diagnosis",
health_element_id=created_diagnosis.id
)
]
contact_with_diagnosis = sdk.contact.modify_contact_blocking(contact_with_image)
const contactWithDiagnosis = await sdk.contact.modifyContact(
new DecryptedContact({
...contactWithImage,
subContacts: [
new DecryptedSubContact({
descr: "Diagnosis",
healthElementId: createdDiagnosis.id
})
]
})
)
Closing the Examination​
To indicate that the medical examination has concluded, you can set the closingDate
on the corresponding Contact
.
This action signifies that the data collection session is finished and, ideally, that the Contact
will not be
modified further.
- Kotlin
- Python
- Typescript
val finalContact = sdk.contact.modifyContact(
contactWithDiagnosis.copy(
closingDate = LocalDateTime.now().format(formatter).toLong()
)
)
contact_with_diagnosis.closing_date = int(datetime.now().strftime("%Y%m%d%H%M%S"))
final_contact = sdk.contact.modify_contact_blocking(contact_with_diagnosis)
const finalContact = await sdk.contact.modifyContact(
new DecryptedContact({
...contactWithDiagnosis,
closingDate: currentFuzzyDate()
})
)
Like openingDate
, the closingDate
is an instant, precise to the second, represented in the YYYYMMDDhhmmss
format.