|
1 | 1 | package org.jetbrains.kotlinx.dataframe.examples.multik
|
2 | 2 |
|
| 3 | +import kotlinx.datetime.LocalDate |
3 | 4 | import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
|
| 5 | +import org.jetbrains.kotlinx.dataframe.api.append |
| 6 | +import org.jetbrains.kotlinx.dataframe.api.cast |
| 7 | +import org.jetbrains.kotlinx.dataframe.api.mapToFrame |
4 | 8 | import org.jetbrains.kotlinx.dataframe.api.print
|
| 9 | +import org.jetbrains.kotlinx.dataframe.api.single |
5 | 10 | import org.jetbrains.kotlinx.dataframe.api.toDataFrame
|
6 |
| -import org.jetbrains.kotlinx.dataframe.io.toStandaloneHtml |
7 |
| -import org.jetbrains.kotlinx.multik.api.identity |
8 | 11 | import org.jetbrains.kotlinx.multik.api.mk
|
9 |
| -import org.jetbrains.kotlinx.multik.ndarray.data.D2Array |
10 |
| -import org.jetbrains.kotlinx.multik.ndarray.data.set |
11 |
| -import kotlin.math.cos |
12 |
| -import kotlin.math.sin |
13 |
| -import kotlin.math.tan |
14 |
| - |
15 |
| -@DataSchema |
16 |
| -data class Transformation( |
17 |
| - val type: TransformationType, |
18 |
| - val parameters: Map<String, Double>, |
19 |
| - val note: String, |
20 |
| - val matrix: D2Array<Double>, |
21 |
| -) |
22 |
| - |
23 |
| -enum class TransformationType { |
24 |
| - IDENTITY, |
25 |
| - TRANSLATION, |
26 |
| - SCALING, |
27 |
| - ROTATION, |
28 |
| - SHEARING, |
29 |
| - REFLECTION_ABOUT_ORIGIN, |
30 |
| - REFLECTION_ABOUT_X_AXIS, |
31 |
| - REFLECTION_ABOUT_Y_AXIS, |
32 |
| -} |
| 12 | +import org.jetbrains.kotlinx.multik.api.rand |
| 13 | +import org.jetbrains.kotlinx.multik.ndarray.data.D3Array |
| 14 | +import org.jetbrains.kotlinx.multik.ndarray.data.D4Array |
| 15 | +import java.time.Month.JULY |
33 | 16 |
|
34 | 17 | /**
|
35 |
| - * IDK yet about this one... TODO |
| 18 | + * DataFrames can store anything inside, including Multik ndarrays. |
| 19 | + * This can be useful for storing matrices for easier access later or to simply organize data read from other files. |
| 20 | + * For example, MRI data is often stored as 3D arrays and sometimes even 4D arrays. |
36 | 21 | */
|
37 | 22 | fun main() {
|
38 |
| - // DataFrames can store anything inside, including Multik nd arrays. |
39 |
| - // This can be useful for storing matrices for easier access later, |
40 |
| - // such as affine transformations when making 2D graphics! |
41 |
| - // (https://en.wikipedia.org/wiki/Affine_transformation) |
42 |
| - |
43 |
| - // let's make a transformation sequence that rotates and scales an image in place. |
44 |
| - // It's currently 100x50, positioned with its left bottom corner at (x=10, y=0) |
45 |
| - val transformations = listOf( |
46 |
| - Transformation( |
47 |
| - type = TransformationType.TRANSLATION, |
48 |
| - parameters = mapOf("x" to -10.0, "y" to 0.0), |
49 |
| - note = "Translate so left-bottom touches origin", |
50 |
| - matrix = translationMatrixOf(x = -10.0, y = 0.0), |
51 |
| - ), |
52 |
| - Transformation( |
53 |
| - type = TransformationType.SCALING, |
54 |
| - parameters = mapOf("w" to 2.0, "h" to 2.0), |
55 |
| - note = "Scale by x2", |
56 |
| - matrix = scaleMatrixOf(w = 2.0, h = 2.0), |
57 |
| - ), |
58 |
| - Transformation( |
59 |
| - type = TransformationType.TRANSLATION, |
60 |
| - parameters = mapOf("x" to -100.0, "y" to -50.0), |
61 |
| - note = "Translate so the new image center is at the origin", |
62 |
| - matrix = translationMatrixOf(x = -100.0, y = -50.0), |
63 |
| - ), |
64 |
| - Transformation( |
65 |
| - type = TransformationType.ROTATION, |
66 |
| - parameters = mapOf("angle" to 45.0), |
67 |
| - note = "Rotate by 45 degrees", |
68 |
| - matrix = rotationMatrixOf(angle = 45.0), |
69 |
| - ), |
70 |
| - Transformation( |
71 |
| - type = TransformationType.TRANSLATION, |
72 |
| - parameters = mapOf("x" to 10.0 + 50.0, "y" to 0.0 + 25.0), |
73 |
| - note = "Translate back so the center is at the same original position", |
74 |
| - matrix = translationMatrixOf(x = 10.0 + 50.0, y = 0.0 + 25.0), |
75 |
| - ), |
| 23 | + // imaginary list of patient data |
| 24 | + @Suppress("ktlint:standard:argument-list-wrapping") |
| 25 | + val metadata = listOf( |
| 26 | + MriMetadata(10012L, 25, "Healthy", LocalDate(2023, 1, 1)), |
| 27 | + MriMetadata(10013L, 45, "Tuberculosis", LocalDate(2023, 2, 15)), |
| 28 | + MriMetadata(10014L, 32, "Healthy", LocalDate(2023, 3, 22)), |
| 29 | + MriMetadata(10015L, 58, "Pneumonia", LocalDate(2023, 4, 8)), |
| 30 | + MriMetadata(10016L, 29, "Tuberculosis", LocalDate(2023, 5, 30)), |
| 31 | + MriMetadata(10017L, 42, "Healthy", LocalDate(2023, 6, 15)), |
| 32 | + MriMetadata(10018L, 37, "Healthy", LocalDate(2023, 7, 1)), |
| 33 | + MriMetadata(10019L, 55, "Healthy", LocalDate(2023, 8, 15)), |
| 34 | + MriMetadata(10020L, 28, "Healthy", LocalDate(2023, 9, 1)), |
| 35 | + MriMetadata(10021L, 44, "Healthy", LocalDate(2023, 10, 15)), |
| 36 | + MriMetadata(10022L, 31, "Healthy", LocalDate(2023, 11, 1)), |
76 | 37 | ).toDataFrame()
|
77 | 38 |
|
78 |
| - transformations.print(borders = true) |
79 |
| - transformations.toStandaloneHtml().openInBrowser() |
80 |
| -} |
81 |
| - |
82 |
| -fun identityMatrix(): D2Array<Double> = mk.identity(3) |
| 39 | + // "reading" the results from "files" |
| 40 | + val results = metadata.mapToFrame { |
| 41 | + +patientId |
| 42 | + +age |
| 43 | + +diagnosis |
| 44 | + +scanDate |
| 45 | + "t1WeightedMri" from { readT1WeightedMri(patientId) } |
| 46 | + "fMriBoldSeries" from { readFMRiBoldSeries(patientId) } |
| 47 | + }.cast<MriResults>(verify = true) |
| 48 | + .append() |
83 | 49 |
|
84 |
| -/** Returns a 3x3 affine transformation matrix that translates by (x, y) */ |
85 |
| -fun translationMatrixOf(x: Double = 0.0, y: Double = 0.0): D2Array<Double> = |
86 |
| - identityMatrix().apply { |
87 |
| - this[0, 2] = x |
88 |
| - this[1, 2] = y |
89 |
| - } |
| 50 | + results.print(borders = true) |
90 | 51 |
|
91 |
| -/** Returns a 3x3 affine transformation matrix that scales by (w, h) about the origin */ |
92 |
| -fun scaleMatrixOf(w: Double = 1.0, h: Double = 1.0): D2Array<Double> = |
93 |
| - identityMatrix().apply { |
94 |
| - this[0, 0] = w |
95 |
| - this[1, 1] = h |
96 |
| - } |
| 52 | + // now when we want to check and visualize the T1-weighted MRI scan |
| 53 | + // for that one healthy patient in July, we can do: |
| 54 | + val scan = results |
| 55 | + .single { scanDate.month == JULY && diagnosis == "Healthy" } |
| 56 | + .t1WeightedMri |
97 | 57 |
|
98 |
| -/** Returns a 3x3 affine transformation matrix that rotates by [angle] degrees about the origin */ |
99 |
| -fun rotationMatrixOf(angle: Double): D2Array<Double> { |
100 |
| - val cos = cos(angle) |
101 |
| - val sin = sin(angle) |
102 |
| - return identityMatrix().apply { |
103 |
| - this[0, 0] = cos |
104 |
| - this[0, 1] = -sin |
105 |
| - this[1, 0] = sin |
106 |
| - this[1, 1] = cos |
107 |
| - } |
| 58 | + // easy :) |
| 59 | + visualize(scan) |
108 | 60 | }
|
109 | 61 |
|
110 |
| -/** Returns a 3x3 affine transformation matrix that shears by [x] and [y] */ |
111 |
| -fun shearingMatrixOf(x: Double = 0.0, y: Double = 0.0): D2Array<Double> = |
112 |
| - identityMatrix().apply { |
113 |
| - this[0, 1] = tan(x) |
114 |
| - this[1, 0] = tan(y) |
115 |
| - } |
| 62 | +@DataSchema |
| 63 | +data class MriMetadata( |
| 64 | + /** Unique patient ID. */ |
| 65 | + val patientId: Long, |
| 66 | + /** Patient age. */ |
| 67 | + val age: Int, |
| 68 | + /** Clinical diagnosis (e.g. "Healthy", "Tuberculosis") */ |
| 69 | + val diagnosis: String, |
| 70 | + /** Date of the scan */ |
| 71 | + val scanDate: LocalDate, |
| 72 | +) |
| 73 | + |
| 74 | +@DataSchema |
| 75 | +data class MriResults( |
| 76 | + /** Unique patient ID. */ |
| 77 | + val patientId: Long, |
| 78 | + /** Patient age. */ |
| 79 | + val age: Int, |
| 80 | + /** Clinical diagnosis (e.g. "Healthy", "Tuberculosis") */ |
| 81 | + val diagnosis: String, |
| 82 | + /** Date of the scan */ |
| 83 | + val scanDate: LocalDate, |
| 84 | + /** |
| 85 | + * T1-weighted anatomical MRI scan. |
| 86 | + * |
| 87 | + * Dimensions: (256 x 256 x 180) |
| 88 | + * - 256 width x 256 height |
| 89 | + * - 180 slices |
| 90 | + */ |
| 91 | + val t1WeightedMri: D3Array<Float>, |
| 92 | + /** |
| 93 | + * Blood oxygenation level-dependent (BOLD) time series from an fMRI scan. |
| 94 | + * |
| 95 | + * Dimensions: (64 x 64 x 30 x 200) |
| 96 | + * - 64 width x 64 height |
| 97 | + * - 30 slices |
| 98 | + * - 200 timepoints |
| 99 | + */ |
| 100 | + val fMriBoldSeries: D4Array<Float>, |
| 101 | +) |
116 | 102 |
|
117 |
| -/** Returns a 3x3 affine transformation matrix that reflects about the origin */ |
118 |
| -fun reflectionAboutOriginMatrix(): D2Array<Double> = |
119 |
| - identityMatrix().apply { |
120 |
| - this[0, 0] = -1.0 |
121 |
| - this[1, 1] = -1.0 |
122 |
| - } |
| 103 | +fun readT1WeightedMri(id: Long): D3Array<Float> { |
| 104 | + // This should in practice, of course, read the actual data, but for this example we just return a dummy array |
| 105 | + return mk.rand(256, 256, 180) |
| 106 | +} |
123 | 107 |
|
124 |
| -/** Returns a 3x3 affine transformation matrix that reflects about the x-axis */ |
125 |
| -fun reflectionAboutXAxisMatrix(): D2Array<Double> = |
126 |
| - identityMatrix().apply { |
127 |
| - this[1, 1] = -1.0 |
128 |
| - } |
| 108 | +fun readFMRiBoldSeries(id: Long): D4Array<Float> { |
| 109 | + // This should in practice, of course, read the actual data, but for this example we just return a dummy array |
| 110 | + return mk.rand(64, 64, 30, 200) |
| 111 | +} |
129 | 112 |
|
130 |
| -/** Returns a 3x3 affine transformation matrix that reflects about the y-axis */ |
131 |
| -fun reflectionAboutYAxisMatrix(): D2Array<Double> = |
132 |
| - identityMatrix().apply { |
133 |
| - this[0, 0] = -1.0 |
134 |
| - } |
| 113 | +fun visualize(scan: D3Array<Float>) { |
| 114 | + // This would then actually visualize the scan |
| 115 | +} |
0 commit comments