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
- Dart
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)
}
String patientId = // This is provided by the user through the UI
final patient = patientId.trim().isEmpty
? await sdk.patient.createPatient(
await sdk.patient.withEncryptionMetadata(
DecryptedPatient(
generateUuid(),
firstName: "Annabelle",
lastName: "Hall",
)
)
) : await sdk.patient.getPatient(patientId);
Next, a new Contact
is instantiated with a custom description provided by the user:
- Kotlin
- Python
- Typescript
- Dart
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()
})
String description = // This is provided by the user through the UI
final contact = DecryptedContact(
generateUuid(),
descr: description,
openingDate: currentDateAsYYYYMMddHHmmSS()
);
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
- Dart
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)
final 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
- Dart
val createdContact = sdk.contact.createContact(contactWithMetadata)
created_contact = sdk.contact.create_contact_blocking(contact_with_metadata)
const createdContact = await sdk.contact.createContact(contactWithMetadata)
final 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
- Dart
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"
})
})
}
})
final bloodPressureService = DecryptedService(
generateUuid(),
label: "Blood pressure",
identifier: [Identifier(system: "cardinal", value: "bloodPressure")],
content: {
"en": DecryptedContent(
measureValue: Measure(
value: (80 + Random().nextInt(41)).toDouble(),
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
- Dart
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]
})
)
createdContact.services = { bloodPressureService };
final contactWithBloodPressure = await sdk.contact.modifyContact(createdContact);
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
- Dart
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]
})
)
final ecgSignal = List.generate(10, (_) => Random().nextInt(100) / 100.0);
final heartRateService = DecryptedService(
generateUuid(),
identifier: [Identifier(system: "cardinal", value: "ecg")],
label: "Heart rate",
content: {
"en": DecryptedContent(
timeSeries: TimeSeries(
samples: [ecgSignal]
)
)
}
);
contactWithBloodPressure.services.add(heartRateService);
final contactWithECG = await sdk.contact.modifyContact(contactWithBloodPressure);
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
- Dart
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
})
final document = DecryptedDocument(
generateUuid(),
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
- Dart
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)
)
final 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
- Dart
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
)
Uint8List xRayImage = Uint8List(100);
for (int i = 0; i < xRayImage.length; i++) {
xRayImage[i] = Random().nextInt(256);
}
final 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
- Dart
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]
})
)
final xRayService = DecryptedService(
generateUuid(),
label: "X-Ray image",
identifier: [Identifier(system: "cardinal", value: "xRay")],
content: {
"en": DecryptedContent(
documentId: documentWithAttachment.id
)
}
);
contactWithECG.services.add(xRayService);
final contactWithImage = await sdk.contact.modifyContact(contactWithECG);
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
- Dart
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)
)
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
- Dart
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
})
]
})
)
String diagnosis = // This is provided by the user through the UI
final healthElement = DecryptedHealthElement(
generateUuid(),
descr: diagnosis
);
final createdDiagnosis = await sdk.healthElement.createHealthElement(
await sdk.healthElement.withEncryptionMetadata(healthElement, patient)
);
contactWithImage.subContacts = {
DecryptedSubContact(
descr: "Diagnosis",
healthElementId: createdDiagnosis.id
)
};
final contactWithDiagnosis = await sdk.contact.modifyContact(contactWithImage);
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
- Dart
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()
})
)
contactWithDiagnosis.closingDate = currentDateAsYYYYMMddHHmmSS();
final finalContact = await sdk.contact.modifyContact(contactWithDiagnosis);
Like openingDate
, the closingDate
is an instant, precise to the second, represented in the YYYYMMDDhhmmss
format.