OpenREM 1.0.0b2__py3-none-any.whl → 1.0.0b3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. openrem/locale/de/LC_MESSAGES/django.po +1060 -1059
  2. openrem/locale/django.pot +973 -972
  3. openrem/locale/es_MX/LC_MESSAGES/django.po +1049 -1048
  4. openrem/locale/it/LC_MESSAGES/django.po +1044 -1043
  5. openrem/locale/lt/LC_MESSAGES/django.po +989 -988
  6. openrem/locale/nb_NO/LC_MESSAGES/django.po +985 -984
  7. openrem/locale/pt_BR/LC_MESSAGES/django.po +1003 -1002
  8. openrem/manage.py +10 -10
  9. openrem/openremproject/__init__.py +1 -1
  10. openrem/openremproject/local_settings.py.linux +128 -128
  11. openrem/openremproject/local_settings.py.windows +144 -144
  12. openrem/openremproject/local_settings.py.windows-sqlite3 +129 -129
  13. openrem/openremproject/settings.py +278 -278
  14. openrem/openremproject/urls.py +32 -32
  15. openrem/openremproject/wsgi.py.example +28 -28
  16. openrem/remapp/__init__.py +2 -2
  17. openrem/remapp/admin.py +31 -31
  18. openrem/remapp/exports/ct_export.py +780 -753
  19. openrem/remapp/exports/dx_export.py +817 -805
  20. openrem/remapp/exports/export_common.py +931 -951
  21. openrem/remapp/exports/export_common_pandas.py +2422 -0
  22. openrem/remapp/exports/exportviews.py +815 -860
  23. openrem/remapp/exports/mg_csv_nhsbsp.py +292 -292
  24. openrem/remapp/exports/mg_export.py +673 -510
  25. openrem/remapp/exports/nm_export.py +796 -575
  26. openrem/remapp/exports/rf_export.py +1418 -1431
  27. openrem/remapp/extractors/ct_philips.py +424 -414
  28. openrem/remapp/extractors/ct_toshiba.py +2116 -2108
  29. openrem/remapp/extractors/dx.py +1033 -952
  30. openrem/remapp/extractors/extract_common.py +817 -817
  31. openrem/remapp/extractors/import_views.py +426 -426
  32. openrem/remapp/extractors/mam.py +685 -672
  33. openrem/remapp/extractors/nm_image.py +439 -431
  34. openrem/remapp/extractors/ptsizecsv2db.py +368 -368
  35. openrem/remapp/extractors/rdsr.py +667 -654
  36. openrem/remapp/extractors/rdsr_methods.py +1771 -1768
  37. openrem/remapp/extractors/rrdsr_methods.py +630 -622
  38. openrem/remapp/fixtures/openskin_safelist.json +11 -11
  39. openrem/remapp/forms.py +2286 -2277
  40. openrem/remapp/interface/chart_functions.py +2412 -2393
  41. openrem/remapp/interface/mod_filters.py +1241 -1243
  42. openrem/remapp/migrations/0001_initial.py.1-0-upgrade +1043 -1043
  43. openrem/remapp/models.py +3418 -3407
  44. openrem/remapp/netdicom/dicomviews.py +681 -683
  45. openrem/remapp/netdicom/qrscu.py +2646 -2646
  46. openrem/remapp/netdicom/tools.py +134 -134
  47. openrem/remapp/static/css/bootstrap-theme.css +587 -587
  48. openrem/remapp/static/css/bootstrap-theme.min.css +4 -4
  49. openrem/remapp/static/css/bootstrap.css +6800 -6800
  50. openrem/remapp/static/css/bootstrap.min.css +4 -4
  51. openrem/remapp/static/css/datepicker3.css +790 -790
  52. openrem/remapp/static/css/jquery.qtip.min.css +2 -2
  53. openrem/remapp/static/css/openrem-extra.css +442 -442
  54. openrem/remapp/static/css/openrem.css +96 -96
  55. openrem/remapp/static/css/registration.css +34 -34
  56. openrem/remapp/static/fonts/glyphicons-halflings-regular.svg +287 -287
  57. openrem/remapp/static/js/bootstrap-datepicker.js +1671 -1671
  58. openrem/remapp/static/js/bootstrap.js +2363 -2363
  59. openrem/remapp/static/js/bootstrap.min.js +6 -6
  60. openrem/remapp/static/js/charts/chartCommonFunctions.js +75 -75
  61. openrem/remapp/static/js/charts/chartFullScreen.js +41 -41
  62. openrem/remapp/static/js/charts/ctChartAjax.js +331 -331
  63. openrem/remapp/static/js/charts/dxChartAjax.js +290 -290
  64. openrem/remapp/static/js/charts/mgChartAjax.js +144 -144
  65. openrem/remapp/static/js/charts/nmChartAjax.js +64 -64
  66. openrem/remapp/static/js/charts/plotly-2.35.2.min.js +8 -0
  67. openrem/remapp/static/js/charts/rfChartAjax.js +128 -128
  68. openrem/remapp/static/js/chroma.min.js +32 -32
  69. openrem/remapp/static/js/datepicker.js +5 -5
  70. openrem/remapp/static/js/dicom.js +115 -115
  71. openrem/remapp/static/js/django_reverse/reverse.js +13 -13
  72. openrem/remapp/static/js/formatDate.js +7 -7
  73. openrem/remapp/static/js/html5shiv.min.js +8 -8
  74. openrem/remapp/static/js/jquery-1.11.0.min.js +4 -4
  75. openrem/remapp/static/js/npm.js +12 -12
  76. openrem/remapp/static/js/respond.min.js +4 -4
  77. openrem/remapp/static/js/skin-dose-maps/jquery.qtip.min.js +4 -4
  78. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dHUDObject.js +112 -112
  79. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dObject.js +367 -367
  80. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dPersonObject.js +158 -158
  81. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapColourScaleObject.js +153 -153
  82. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapObject.js +367 -367
  83. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping.js +584 -584
  84. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping3d.js +255 -255
  85. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMappingAjax.js +267 -212
  86. openrem/remapp/static/js/skin-dose-maps/three.min.js +835 -835
  87. openrem/remapp/static/js/sorttable.js +495 -495
  88. openrem/remapp/templates/base.html +253 -253
  89. openrem/remapp/templates/registration/changepassword.html +25 -25
  90. openrem/remapp/templates/registration/changepassworddone.html +12 -12
  91. openrem/remapp/templates/registration/login.html +42 -42
  92. openrem/remapp/templates/remapp/backgroundtaskmaximumrows_form.html +29 -29
  93. openrem/remapp/templates/remapp/base.html +1 -1
  94. openrem/remapp/templates/remapp/ctdetail.html +235 -235
  95. openrem/remapp/templates/remapp/ctfiltered.html +310 -310
  96. openrem/remapp/templates/remapp/dicomdeletesettings_form.html +31 -31
  97. openrem/remapp/templates/remapp/dicomqr.html +147 -147
  98. openrem/remapp/templates/remapp/dicomquerydetails.html +83 -83
  99. openrem/remapp/templates/remapp/dicomqueryimages.html +49 -49
  100. openrem/remapp/templates/remapp/dicomqueryseries.html +109 -109
  101. openrem/remapp/templates/remapp/dicomquerysummary.html +48 -48
  102. openrem/remapp/templates/remapp/dicomremoteqr_confirm_delete.html +60 -60
  103. openrem/remapp/templates/remapp/dicomremoteqr_form.html +32 -32
  104. openrem/remapp/templates/remapp/dicomstorescp_confirm_delete.html +53 -53
  105. openrem/remapp/templates/remapp/dicomstorescp_form.html +48 -48
  106. openrem/remapp/templates/remapp/dicomsummary.html +257 -257
  107. openrem/remapp/templates/remapp/displaychartoptions.html +184 -184
  108. openrem/remapp/templates/remapp/displayhomepageoptions.html +57 -57
  109. openrem/remapp/templates/remapp/displayname-count.html +6 -6
  110. openrem/remapp/templates/remapp/displayname-last-date.html +3 -3
  111. openrem/remapp/templates/remapp/displayname-modality.html +86 -105
  112. openrem/remapp/templates/remapp/displayname-skinmap.html +18 -18
  113. openrem/remapp/templates/remapp/displaynameupdate.html +100 -100
  114. openrem/remapp/templates/remapp/displaynameview.html +222 -219
  115. openrem/remapp/templates/remapp/dxdetail.html +176 -176
  116. openrem/remapp/templates/remapp/dxfiltered.html +324 -324
  117. openrem/remapp/templates/remapp/exports-active.html +25 -25
  118. openrem/remapp/templates/remapp/exports-complete.html +35 -35
  119. openrem/remapp/templates/remapp/exports-error.html +26 -26
  120. openrem/remapp/templates/remapp/exports-queue.html +18 -18
  121. openrem/remapp/templates/remapp/exports.html +191 -191
  122. openrem/remapp/templates/remapp/failed_summary_list.html +27 -27
  123. openrem/remapp/templates/remapp/filteredbase.html +162 -162
  124. openrem/remapp/templates/remapp/highdosemetricalertsettings_form.html +76 -76
  125. openrem/remapp/templates/remapp/home-list-modalities.html +94 -94
  126. openrem/remapp/templates/remapp/home.html +202 -202
  127. openrem/remapp/templates/remapp/list_filters.html +24 -24
  128. openrem/remapp/templates/remapp/mgdetail.html +160 -138
  129. openrem/remapp/templates/remapp/mgfiltered.html +311 -311
  130. openrem/remapp/templates/remapp/nmdetail.html +300 -300
  131. openrem/remapp/templates/remapp/nmfiltered.html +255 -255
  132. openrem/remapp/templates/remapp/notpatient.html +190 -190
  133. openrem/remapp/templates/remapp/notpatientindicators_form_base.html +81 -81
  134. openrem/remapp/templates/remapp/notpatientindicatorsid_confirm_delete.html +54 -54
  135. openrem/remapp/templates/remapp/notpatientindicatorsid_form.html +23 -23
  136. openrem/remapp/templates/remapp/notpatientindicatorsname_confirm_delete.html +54 -54
  137. openrem/remapp/templates/remapp/notpatientindicatorsname_form.html +23 -23
  138. openrem/remapp/templates/remapp/notpatientindicatorsname_form_base.html +85 -85
  139. openrem/remapp/templates/remapp/openskinsafelist_add.html +130 -130
  140. openrem/remapp/templates/remapp/openskinsafelist_confirm_delete.html +100 -100
  141. openrem/remapp/templates/remapp/openskinsafelist_form.html +207 -207
  142. openrem/remapp/templates/remapp/patientidsettings_form.html +83 -83
  143. openrem/remapp/templates/remapp/populate_summary_progress.html +83 -83
  144. openrem/remapp/templates/remapp/populate_summary_progress_error.html +36 -36
  145. openrem/remapp/templates/remapp/review_failed_imports.html +157 -157
  146. openrem/remapp/templates/remapp/review_failed_study.html +41 -41
  147. openrem/remapp/templates/remapp/review_studies_delete_button.html +20 -20
  148. openrem/remapp/templates/remapp/review_study.html +19 -19
  149. openrem/remapp/templates/remapp/review_summary_list.html +245 -245
  150. openrem/remapp/templates/remapp/rf_dose_alert_email_template.html +14 -1
  151. openrem/remapp/templates/remapp/rfalertnotificationsview.html +59 -59
  152. openrem/remapp/templates/remapp/rfdetail.html +547 -543
  153. openrem/remapp/templates/remapp/rfdetailbase.html +18 -18
  154. openrem/remapp/templates/remapp/rffiltered.html +404 -404
  155. openrem/remapp/templates/remapp/sizeimports.html +119 -119
  156. openrem/remapp/templates/remapp/sizeprocess.html +96 -96
  157. openrem/remapp/templates/remapp/sizeupload.html +110 -110
  158. openrem/remapp/templates/remapp/skindosemapcalcsettings_form.html +28 -28
  159. openrem/remapp/templates/remapp/standardname-modality.html +69 -69
  160. openrem/remapp/templates/remapp/standardnames_confirm_delete.html +71 -71
  161. openrem/remapp/templates/remapp/standardnames_form.html +87 -87
  162. openrem/remapp/templates/remapp/standardnamesettings_form.html +41 -41
  163. openrem/remapp/templates/remapp/standardnamesrefreshall.html +92 -92
  164. openrem/remapp/templates/remapp/standardnameview.html +103 -103
  165. openrem/remapp/templates/remapp/study_confirm_delete.html +147 -147
  166. openrem/remapp/templates/remapp/task_admin.html +265 -265
  167. openrem/remapp/templates/remapp/tasks.html +76 -76
  168. openrem/remapp/templatetags/formfilters.py +13 -13
  169. openrem/remapp/templatetags/proper_paginate.py +38 -38
  170. openrem/remapp/templatetags/remappduration.py +36 -36
  171. openrem/remapp/templatetags/sigdig.py +38 -38
  172. openrem/remapp/templatetags/sort_class_property_value.py +15 -15
  173. openrem/remapp/templatetags/update_variable.py +20 -20
  174. openrem/remapp/templatetags/url_replace.py +25 -25
  175. openrem/remapp/tests/test_charts_common.py +202 -202
  176. openrem/remapp/tests/test_charts_ct.py +7111 -7111
  177. openrem/remapp/tests/test_charts_dx.py +3513 -3513
  178. openrem/remapp/tests/test_charts_mg.py +1116 -1115
  179. openrem/remapp/tests/test_dcmdatetime.py +189 -189
  180. openrem/remapp/tests/test_dicom_qr.py +2580 -2580
  181. openrem/remapp/tests/test_display_name.py +274 -274
  182. openrem/remapp/tests/test_export_ct_xlsx.py +272 -248
  183. openrem/remapp/tests/test_export_dx_xlsx.py +137 -134
  184. openrem/remapp/tests/test_export_mammo_csv.py +242 -242
  185. openrem/remapp/tests/test_export_rf_xlsx.py +246 -246
  186. openrem/remapp/tests/test_files/DX-Im-DRGEM.dcm +0 -0
  187. openrem/remapp/tests/test_files/MG-RDSR-GEPristina-2D.dcm +0 -0
  188. openrem/remapp/tests/test_files/MG-RDSR-GEPristina-DBT.dcm +0 -0
  189. openrem/remapp/tests/test_files/MG-RDSR-Giotto-DBT.dcm +0 -0
  190. openrem/remapp/tests/test_files/skin_map_alphenix.py +590 -590
  191. openrem/remapp/tests/test_files/skin_map_zee.py +354 -354
  192. openrem/remapp/tests/test_filters_ct.py +321 -321
  193. openrem/remapp/tests/test_filters_dx.py +92 -92
  194. openrem/remapp/tests/test_filters_mammo.py +183 -183
  195. openrem/remapp/tests/test_filters_rf.py +118 -118
  196. openrem/remapp/tests/test_get_values.py +72 -72
  197. openrem/remapp/tests/test_hash_id.py +65 -65
  198. openrem/remapp/tests/test_import_ct_esr_ge.py +3034 -3034
  199. openrem/remapp/tests/test_import_ct_philips_rdsr.py +42 -42
  200. openrem/remapp/tests/test_import_ct_rdsr_multiple.py +256 -256
  201. openrem/remapp/tests/test_import_ct_rdsr_siemens.py +827 -827
  202. openrem/remapp/tests/test_import_ct_rdsr_spectrumdynamics.py +91 -91
  203. openrem/remapp/tests/test_import_ct_rdsr_toshiba_dosecheck.py +67 -67
  204. openrem/remapp/tests/test_import_ct_rdsr_toshiba_multivaluesd.py +33 -33
  205. openrem/remapp/tests/test_import_ct_rdsr_toshiba_pixelmed.py +118 -118
  206. openrem/remapp/tests/test_import_ct_sc_philips.py +44 -44
  207. openrem/remapp/tests/test_import_dual_rdsr.py +110 -110
  208. openrem/remapp/tests/test_import_dx.py +1267 -1191
  209. openrem/remapp/tests/test_import_dx_rdsr.py +1250 -1253
  210. openrem/remapp/tests/test_import_mam.py +438 -438
  211. openrem/remapp/tests/test_import_mg_im_hol_proj.py +46 -46
  212. openrem/remapp/tests/test_import_mg_rdsr.py +586 -586
  213. openrem/remapp/tests/test_import_nm_image.py +420 -420
  214. openrem/remapp/tests/test_import_nm_siemens_rdsr.py +396 -396
  215. openrem/remapp/tests/test_import_px.py +161 -161
  216. openrem/remapp/tests/test_import_rf_rdsr.py +420 -418
  217. openrem/remapp/tests/test_missing_date.py +42 -42
  218. openrem/remapp/tests/test_not_patient.py +60 -60
  219. openrem/remapp/tests/test_openskin.py +272 -272
  220. openrem/remapp/tests/test_patient_id_settings.py +72 -72
  221. openrem/remapp/tests/test_pt_size_import.py +232 -232
  222. openrem/remapp/tests/test_rf_detail.py +113 -113
  223. openrem/remapp/tests/test_rf_high_dose_alert.py +361 -361
  224. openrem/remapp/tools/background.py +361 -361
  225. openrem/remapp/tools/check_standard_name_status.py +47 -0
  226. openrem/remapp/tools/check_uid.py +70 -70
  227. openrem/remapp/tools/dcmdatetime.py +248 -248
  228. openrem/remapp/tools/default_import.py +44 -47
  229. openrem/remapp/tools/get_values.py +230 -230
  230. openrem/remapp/tools/hash_id.py +58 -58
  231. openrem/remapp/tools/make_skin_map.py +448 -406
  232. openrem/remapp/tools/not_patient_indicators.py +72 -72
  233. openrem/remapp/tools/openskin/calc_exp_map.py +173 -173
  234. openrem/remapp/tools/openskin/geomclass.py +475 -475
  235. openrem/remapp/tools/openskin/geomfunc.py +433 -432
  236. openrem/remapp/tools/openskin/skinmap.py +417 -417
  237. openrem/remapp/tools/populate_summary.py +185 -193
  238. openrem/remapp/tools/save_skin_map_structure.py +73 -73
  239. openrem/remapp/tools/send_high_dose_alert_emails.py +238 -207
  240. openrem/remapp/urls.py +456 -448
  241. openrem/remapp/version.py +11 -11
  242. openrem/remapp/views.py +1147 -1052
  243. openrem/remapp/views_admin.py +3876 -3936
  244. openrem/remapp/views_charts_ct.py +2110 -2058
  245. openrem/remapp/views_charts_dx.py +1906 -1836
  246. openrem/remapp/views_charts_mg.py +1349 -1196
  247. openrem/remapp/views_charts_nm.py +535 -535
  248. openrem/remapp/views_charts_rf.py +1219 -1241
  249. openrem/remapp/views_openskin.py +379 -384
  250. openrem/sample-config/openrem-consumer.service +12 -12
  251. openrem/sample-config/openrem-gunicorn.service +13 -13
  252. openrem/sample-config/openrem-server +14 -13
  253. openrem/sample-config/openrem_orthanc_config_linux.lua +454 -454
  254. openrem/sample-config/openrem_orthanc_config_windows.lua +455 -455
  255. openrem/sample-config/queue-init.bat +73 -73
  256. openrem/scripts/openrem_ctphilips.py +25 -25
  257. openrem/scripts/openrem_cttoshiba.py +28 -28
  258. openrem/scripts/openrem_dx.py +22 -22
  259. openrem/scripts/openrem_mg.py +22 -22
  260. openrem/scripts/openrem_nm.py +22 -22
  261. openrem/scripts/openrem_ptsizecsv.py +17 -17
  262. openrem/scripts/openrem_qr.py +12 -12
  263. openrem/scripts/openrem_rdsr.py +25 -25
  264. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/METADATA +39 -29
  265. openrem-1.0.0b3.dist-info/RECORD +379 -0
  266. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/WHEEL +1 -1
  267. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/COPYING-GPLv3 +674 -674
  268. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/LICENSE +22 -22
  269. OpenREM-1.0.0b2.dist-info/RECORD +0 -373
  270. openrem/remapp/static/js/charts/plotly-2.17.1.min.js +0 -8
  271. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ctphilips.py +0 -0
  272. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_cttoshiba.py +0 -0
  273. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_dx.py +0 -0
  274. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_mg.py +0 -0
  275. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_nm.py +0 -0
  276. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ptsizecsv.py +0 -0
  277. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_qr.py +0 -0
  278. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_rdsr.py +0 -0
  279. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/top_level.txt +0 -0
@@ -1,654 +1,667 @@
1
- # OpenREM - Radiation Exposure Monitoring tools for the physicist
2
- # Copyright (C) 2012,2013 The Royal Marsden NHS Foundation Trust
3
- #
4
- # This program is free software: you can redistribute it and/or modify
5
- # it under the terms of the GNU General Public License as published by
6
- # the Free Software Foundation, either version 3 of the License, or
7
- # (at your option) any later version.
8
- #
9
- # This program is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- # GNU General Public License for more details.
13
- #
14
- # Additional permission under section 7 of GPLv3:
15
- # You shall not make any use of the name of The Royal Marsden NHS
16
- # Foundation trust in connection with this Program in any press or
17
- # other public announcement without the prior written consent of
18
- # The Royal Marsden NHS Foundation Trust.
19
- #
20
- # You should have received a copy of the GNU General Public License
21
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
-
23
- """
24
- .. module:: rdsr.
25
- :synopsis: Module to extract radiation dose related data from DICOM Radiation SR objects
26
- or Radiopharmaceutical Radiation SR objects
27
-
28
- .. moduleauthor:: Ed McDonagh
29
-
30
- """
31
- from collections import OrderedDict
32
- from datetime import timedelta
33
- import logging
34
- import os
35
- import sys
36
-
37
- import django
38
- from django.db.models import Sum, ObjectDoesNotExist
39
- import pydicom
40
-
41
- from openrem.remapp.tools.background import (
42
- record_task_error_exit,
43
- record_task_related_query,
44
- record_task_info,
45
- run_in_background_with_limits,
46
- )
47
-
48
- # setup django/OpenREM.
49
- basepath = os.path.dirname(__file__)
50
- projectpath = os.path.abspath(os.path.join(basepath, "..", ".."))
51
- if projectpath not in sys.path:
52
- sys.path.insert(1, projectpath)
53
- os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
54
- django.setup()
55
-
56
- from ..tools.check_uid import record_sop_instance_uid
57
- from ..tools.get_values import (
58
- get_value_kw,
59
- )
60
- from ..tools.make_skin_map import make_skin_map
61
- from ..tools.send_high_dose_alert_emails import send_rf_high_dose_alert_email
62
- from .extract_common import ( # pylint: disable=wrong-import-order, wrong-import-position
63
- ct_event_type_count,
64
- patient_module_attributes,
65
- populate_mammo_agd_summary,
66
- populate_dx_rf_summary,
67
- populate_rf_delta_weeks_summary,
68
- add_standard_names,
69
- generalstudymoduleattributes,
70
- generalequipmentmoduleattributes,
71
- patientstudymoduleattributes,
72
- )
73
- from .rdsr_methods import projectionxrayradiationdose
74
- from .rrdsr_methods import _radiopharmaceuticalradiationdose
75
- from remapp.models import ( # pylint: disable=wrong-import-order, wrong-import-position
76
- AccumIntegratedProjRadiogDose,
77
- DicomDeleteSettings,
78
- GeneralStudyModuleAttr,
79
- HighDoseMetricAlertSettings,
80
- PKsForSummedRFDoseStudiesInDeltaWeeks,
81
- SkinDoseMapCalcSettings,
82
- )
83
-
84
- logger = logging.getLogger(
85
- "remapp.extractors.rdsr"
86
- ) # Explicitly named so that it is still handled when using __main__
87
-
88
-
89
- def _rdsr_rrdsr_contents(dataset, g):
90
- try:
91
- template_identifier = dataset.ContentTemplateSequence[0].TemplateIdentifier
92
- except AttributeError:
93
- try:
94
- if dataset.ContentSequence[0].ConceptCodeSequence[0].CodeValue == "113704":
95
- template_identifier = "10001"
96
- elif (
97
- dataset.ConceptNameCodeSequence[0].CodingSchemeDesignator
98
- == "99SMS_RADSUM"
99
- and dataset.ConceptNameCodeSequence[0].CodeValue == "C-10"
100
- ):
101
- template_identifier = "10001"
102
- elif dataset.ConceptCodeSequence[0].CodeValue == "113500":
103
- template_identifier = "10021"
104
- else:
105
- logger.error(
106
- "Study UID {0} of modality {1} has no template sequence - incomplete RDSR. "
107
- "Aborting.".format(
108
- g.study_instance_uid,
109
- get_value_kw("ManufacturerModelName", dataset),
110
- )
111
- )
112
- g.delete()
113
- return
114
- except AttributeError:
115
- logger.error(
116
- "Study UID {0} of modality {1} has no template sequence - incomplete RDSR. Aborting.".format(
117
- g.study_instance_uid, get_value_kw("ManufacturerModelName", dataset)
118
- )
119
- )
120
- g.delete()
121
- return
122
- if template_identifier == "10001":
123
- projectionxrayradiationdose(dataset, g, "projection")
124
- elif template_identifier == "10011":
125
- projectionxrayradiationdose(dataset, g, "ct")
126
- elif template_identifier == "10021":
127
- _radiopharmaceuticalradiationdose(dataset, g)
128
- g.save()
129
- if not g.requested_procedure_code_meaning:
130
- if "RequestAttributesSequence" in dataset and dataset[0x40, 0x275].VM:
131
- # Ugly hack to prevent issues with zero length LS16 sequence
132
- req = dataset.RequestAttributesSequence
133
- g.requested_procedure_code_meaning = get_value_kw(
134
- "RequestedProcedureDescription", req[0]
135
- )
136
- # Sometimes the above is true, but there is no RequestedProcedureDescription in that sequence, but
137
- # there is a basic field as below.
138
- if not g.requested_procedure_code_meaning:
139
- g.requested_procedure_code_meaning = get_value_kw(
140
- "RequestedProcedureDescription", dataset
141
- )
142
- g.save()
143
- else:
144
- g.requested_procedure_code_meaning = get_value_kw(
145
- "RequestedProcedureDescription", dataset
146
- )
147
- g.save()
148
-
149
- try:
150
- number_of_events_ct = (
151
- g.ctradiationdose_set.get().ctirradiationeventdata_set.count()
152
- )
153
- except ObjectDoesNotExist:
154
- number_of_events_ct = 0
155
- try:
156
- number_of_events_proj = (
157
- g.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
158
- )
159
- except ObjectDoesNotExist:
160
- number_of_events_proj = 0
161
- g.number_of_events = number_of_events_ct + number_of_events_proj
162
- g.save()
163
- if template_identifier == "10011":
164
- ct_event_type_count(g)
165
- try:
166
- g.total_dlp = (
167
- g.ctradiationdose_set.get()
168
- .ctaccumulateddosedata_set.get()
169
- .ct_dose_length_product_total
170
- )
171
- g.save()
172
- except ObjectDoesNotExist:
173
- logger.warning(
174
- "Study UID {0} of modality {1}. Unable to set summary total_dlp".format(
175
- g.study_instance_uid, get_value_kw("ManufacturerModelName", dataset)
176
- )
177
- )
178
- elif template_identifier == "10001":
179
- if g.modality_type == "MG":
180
- populate_mammo_agd_summary(g)
181
- else:
182
- populate_dx_rf_summary(g)
183
-
184
-
185
- def _get_existing_event_uids(study):
186
- """
187
- Returns all event uids stored for this study
188
- """
189
- existing_event_uids = set()
190
- s = study.ctradiationdose_set.first()
191
- if s is not None:
192
- for event in s.ctirradiationeventdata_set.all():
193
- existing_event_uids.add(event.irradiation_event_uid)
194
-
195
- s = study.projectionxrayradiationdose_set.first()
196
- if s is not None:
197
- for event in s.irradeventxraydata_set.all():
198
- existing_event_uids.add(event.irradiation_event_uid)
199
-
200
- s = study.radiopharmaceuticalradiationdose_set.first()
201
- if s is not None:
202
- for event in s.radiopharmaceuticaladministrationeventdata_set.all():
203
- existing_event_uids.add(event.radiopharmaceutical_administration_event_uid)
204
-
205
- return existing_event_uids
206
-
207
-
208
- def _update_recorded_objects(g, dataset, existing_sop_instance_uids=None):
209
- new_sop_instance_uid = dataset.SOPInstanceUID
210
- record_sop_instance_uid(g, new_sop_instance_uid)
211
- if existing_sop_instance_uids is not None:
212
- for sop_instance_uid in existing_sop_instance_uids:
213
- record_sop_instance_uid(g, sop_instance_uid)
214
-
215
-
216
- def _get_dataset_event_uids(dataset):
217
- """
218
- Collect the uids from all events that can be in a dataset
219
- and that we care about
220
- """
221
- new_event_uids = set()
222
- for content in dataset.ContentSequence:
223
- if content.ValueType and content.ValueType == "CONTAINER":
224
- if content.ConceptNameCodeSequence[0].CodeMeaning in (
225
- "CT Acquisition",
226
- "Irradiation Event X-Ray Data",
227
- "Radiopharmaceutical Administration",
228
- ):
229
- for item in content.ContentSequence:
230
- if item.ConceptNameCodeSequence[0].CodeMeaning in (
231
- "Irradiation Event UID",
232
- "Radiopharmaceutical Administration Event UID",
233
- ):
234
- new_event_uids.add("{0}".format(item.UID))
235
- return new_event_uids
236
-
237
-
238
- def _handle_study_already_existing(
239
- dataset,
240
- ):
241
- """
242
- This function checks wheter there is already a study with the same instance uid as dataset.
243
-
244
- If yes it checks the data in the dataset and the existing study
245
- returning different strategies for continuing the import:
246
-
247
- If dataset was imported already: Abort the import
248
- If dataset contains subset of events of existing study: Abort the import
249
- If dataset has same events as existing study: Abort the import
250
- If dataset has more events than existing study (Events of existing are subset):
251
- Delete old study and reimport
252
- If dataset has different events than existing study: Create a second study and import
253
- If dataset is rrdsr and only images have been imported to the study: Complete study with data from the rrdsr
254
-
255
- :return: A dict with possible keys {status, existing_sop_instance_uids, study}, where
256
- :status: One of 'abort', 'continue' (Create the study and maybe save old instance uids to it),
257
- 'drop_old_continue' (Delete old and reimport),
258
- 'retry' (Wait then call this function again, there are partially imported studies), append_rrdsr'
259
- :existing_sop_instance_uids: The sop instance uids of all files imported for this study. For e.g.
260
- drop_old_continue status this should be saved onto the new study entry. (See _update_recorded_objects)
261
- :study: The study that was used to make the decision what to do next.
262
- Only added for status='append_rrdsr' or status='drop_old_continue' at the moment
263
- """
264
- existing_sop_instance_uids = set()
265
-
266
- study_uid = dataset.StudyInstanceUID
267
- existing_study_uid_match = GeneralStudyModuleAttr.objects.filter(
268
- study_instance_uid__exact=study_uid
269
- )
270
- if existing_study_uid_match:
271
- new_sop_instance_uid = dataset.SOPInstanceUID
272
- for existing_study in existing_study_uid_match.order_by("pk"):
273
- for processed_object in existing_study.objectuidsprocessed_set.all():
274
- existing_sop_instance_uids.add(processed_object.sop_instance_uid)
275
- if new_sop_instance_uid in existing_sop_instance_uids:
276
- # We've dealt with this object before...
277
- logger.debug(
278
- "Import match on Study Instance UID {0} and object SOP Instance UID {1}. "
279
- "Will not import.".format(study_uid, new_sop_instance_uid)
280
- )
281
- record_task_error_exit("Study already in db")
282
- return {
283
- "status": "abort",
284
- "existing_sop_instance_uids": existing_sop_instance_uids,
285
- }
286
- # Either we've not seen it before, or it wasn't recorded when we did.
287
- # Next find the event UIDs in the RDSR being imported
288
- new_event_uids = _get_dataset_event_uids(dataset)
289
- logger.debug(
290
- "Import match on StudyInstUID {0}. New RDSR event UIDs {1}".format(
291
- study_uid, new_event_uids
292
- )
293
- )
294
-
295
- # Now check which event UIDs are in the database already
296
- existing_event_uids = OrderedDict()
297
- for i, existing_study in enumerate(existing_study_uid_match.order_by("pk")):
298
- existing_event_uids[i] = set()
299
- existing_event_uids[i] = _get_existing_event_uids(existing_study)
300
- logger.debug(
301
- "Import match on StudyInstUID {0}. Existing event UIDs {1}".format(
302
- study_uid, existing_event_uids
303
- )
304
- )
305
-
306
- # Now compare the two
307
- for study_index, uid_list in list(existing_event_uids.items()):
308
- if uid_list == new_event_uids:
309
- # New RDSR is the same as the existing one
310
- logger.debug(
311
- "Import match on StudyInstUID {0}. Event level match, will not import.".format(
312
- study_uid
313
- )
314
- )
315
- record_sop_instance_uid(
316
- existing_study_uid_match[study_index], new_sop_instance_uid
317
- )
318
- record_task_error_exit("Study already in db")
319
- return {
320
- "status": "abort",
321
- "existing_sop_instance_uids": existing_sop_instance_uids,
322
- }
323
- elif new_event_uids.issubset(uid_list):
324
- # New RDSR has the same but fewer events than existing one
325
- logger.debug(
326
- "Import match on StudyInstUID {0}. New RDSR events are subset of existing events. "
327
- "Will not import.".format(study_uid)
328
- )
329
- record_sop_instance_uid(
330
- existing_study_uid_match[study_index], new_sop_instance_uid
331
- )
332
- record_task_error_exit("Study already in db")
333
- return {
334
- "status": "abort",
335
- "existing_sop_instance_uids": existing_sop_instance_uids,
336
- }
337
- elif uid_list.issubset(new_event_uids):
338
- # New RDSR has the existing events and more
339
- # Check existing one had finished importing
340
- logger.debug(
341
- "Import match on StudyInstUID {0}. Existing events are subset of new events. Will"
342
- " import.".format(study_uid)
343
- )
344
- return {
345
- "status": "drop_old_continue",
346
- "existing_sop_instance_uids": existing_sop_instance_uids,
347
- "study": existing_study_uid_match[study_index],
348
- }
349
- elif None in uid_list:
350
- # This happens for NM studies where only images have been imported so far
351
- # because they add a RadiopharmaceuticalAdministrationEventData Object without UID.
352
- logger.debug(
353
- f"Import match on StudyInstUID {study_uid}. There is already a NM study for"
354
- "which only Images where imported so far. Will delete the "
355
- "RadiopharmaceuticalAdministrationEventData and RadiopharmaceuticalRadioationDose"
356
- "and reimport, but keep the PET_Series data if present."
357
- )
358
- study = existing_study_uid_match[study_index]
359
- if study.modality_type == "NM" and (
360
- dataset.SOPClassUID
361
- == "1.2.840.10008.5.1.4.1.1.88.68" # Radiopharmaceutical Radiation Dose SR
362
- and dataset.ConceptNameCodeSequence[0].CodeValue
363
- == "113500" # Radiopharmaceutical Radiation Dose Report
364
- ):
365
- tmp = study.radiopharmaceuticalradiationdose_set.first()
366
- if tmp is not None:
367
- tmp = tmp.radiopharmaceuticaladministrationeventdata_set.first()
368
- if tmp is not None:
369
- if tmp.radiopharmaceutical_administration_event_uid is None:
370
- return {
371
- "status": "append_rrdsr",
372
- "existing_sop_instance_uids": existing_sop_instance_uids,
373
- "study": study,
374
- }
375
-
376
- return {
377
- "status": "continue",
378
- "existing_sop_instance_uids": existing_sop_instance_uids,
379
- }
380
-
381
-
382
- def _rdsr2db(dataset):
383
- if "StudyInstanceUID" in dataset:
384
- study_uid = dataset.StudyInstanceUID
385
- study_uid = dataset.StudyInstanceUID
386
- record_task_info(f"Study UID: {study_uid.replace('.', '. ')}")
387
- record_task_related_query(study_uid)
388
- existing = _handle_study_already_existing(dataset)
389
-
390
- if existing["status"] == "abort":
391
- return
392
- elif existing["status"] == "append_rrdsr":
393
- _update_recorded_objects(existing["study"], dataset)
394
- _rdsr_rrdsr_contents(dataset, existing["study"])
395
- radios = existing["study"].radiopharmaceuticalradiationdose_set.all()
396
- if (
397
- radios[0]
398
- .radiopharmaceuticaladministrationeventdata_set.get()
399
- .radiopharmaceutical_administration_event_uid
400
- is None
401
- ):
402
- radio_old, radio_new = (radios[0], radios[1])
403
- else:
404
- radio_new, radio_old = (radios[0], radios[1])
405
- radio_old.petseries_set.update(radiopharmaceutical_radiation_dose=radio_new)
406
- radio_old.delete()
407
- return
408
- elif existing["status"] == "drop_old_continue":
409
- existing["study"].delete()
410
- elif existing["status"] == "continue":
411
- pass # We are allowed to proceed importing
412
-
413
- g = GeneralStudyModuleAttr.objects.create()
414
- if not g: # Allows import to be aborted if no template found
415
- return
416
- g.save()
417
- if existing["status"] == "drop_old_continue":
418
- _update_recorded_objects(g, dataset, existing["existing_sop_instance_uids"])
419
- else:
420
- _update_recorded_objects(g, dataset)
421
- generalstudymoduleattributes(dataset, g, logger)
422
- generalequipmentmoduleattributes(dataset, g)
423
- _rdsr_rrdsr_contents(dataset, g)
424
- patientstudymoduleattributes(dataset, g)
425
- patient_module_attributes(dataset, g)
426
-
427
- try:
428
- SkinDoseMapCalcSettings.objects.get()
429
- except ObjectDoesNotExist:
430
- SkinDoseMapCalcSettings.objects.create()
431
-
432
- enable_skin_dose_maps = SkinDoseMapCalcSettings.objects.values_list(
433
- "enable_skin_dose_maps", flat=True
434
- )[0]
435
- calc_on_import = SkinDoseMapCalcSettings.objects.values_list(
436
- "calc_on_import", flat=True
437
- )[0]
438
- if g.modality_type == "RF" and enable_skin_dose_maps and calc_on_import:
439
- run_in_background_with_limits(
440
- make_skin_map,
441
- "make_skin_map",
442
- 0,
443
- {"make_skin_map": 1},
444
- g.pk,
445
- )
446
-
447
- # Calculate summed total DAP and dose at RP for studies that have this study's patient ID, going back week_delta
448
- # weeks in time from this study date. Only do this if activated in the fluoro alert settings (check whether
449
- # HighDoseMetricAlertSettings.calc_accum_dose_over_delta_weeks_on_import is True).
450
- if g.modality_type == "RF":
451
-
452
- try:
453
- HighDoseMetricAlertSettings.objects.get()
454
- except ObjectDoesNotExist:
455
- HighDoseMetricAlertSettings.objects.create()
456
-
457
- week_delta = HighDoseMetricAlertSettings.objects.values_list(
458
- "accum_dose_delta_weeks", flat=True
459
- )[0]
460
- calc_accum_dose_over_delta_weeks_on_import = (
461
- HighDoseMetricAlertSettings.objects.values_list(
462
- "calc_accum_dose_over_delta_weeks_on_import", flat=True
463
- )[0]
464
- )
465
- if calc_accum_dose_over_delta_weeks_on_import:
466
-
467
- all_rf_studies = GeneralStudyModuleAttr.objects.filter(
468
- modality_type__exact="RF"
469
- ).all()
470
-
471
- patient_id = g.patientmoduleattr_set.values_list("patient_id", flat=True)[0]
472
- if patient_id:
473
- study_date = g.study_date
474
- oldest_date = study_date - timedelta(weeks=week_delta)
475
-
476
- # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
477
- # The try and except parts of this code are here because some of the studies in my database didn't have the
478
- # expected data in the related fields - not sure why. Perhaps an issue with the extractor routine?
479
- try:
480
- g.projectionxrayradiationdose_set.get().accumxraydose_set.all()
481
- except ObjectDoesNotExist:
482
- g.projectionxrayradiationdose_set.get().accumxraydose_set.create()
483
-
484
- for (
485
- accumxraydose
486
- ) in g.projectionxrayradiationdose_set.get().accumxraydose_set.all():
487
- try:
488
- accumxraydose.accumintegratedprojradiogdose_set.get()
489
- except:
490
- accumxraydose.accumintegratedprojradiogdose_set.create()
491
- # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
492
-
493
- for (
494
- accumxraydose
495
- ) in g.projectionxrayradiationdose_set.get().accumxraydose_set.all():
496
- accum_int_proj_pk = (
497
- accumxraydose.accumintegratedprojradiogdose_set.get().pk
498
- )
499
-
500
- accum_int_proj_to_update = (
501
- AccumIntegratedProjRadiogDose.objects.get(pk=accum_int_proj_pk)
502
- )
503
-
504
- included_studies = all_rf_studies.filter(
505
- patientmoduleattr__patient_id__exact=patient_id,
506
- study_date__range=[oldest_date, study_date],
507
- )
508
-
509
- bulk_entries = []
510
- for pk in included_studies.values_list("pk", flat=True):
511
- if not PKsForSummedRFDoseStudiesInDeltaWeeks.objects.filter(
512
- general_study_module_attributes_id__exact=g.pk
513
- ).filter(study_pk_in_delta_weeks__exact=pk):
514
- new_entry = PKsForSummedRFDoseStudiesInDeltaWeeks()
515
- new_entry.general_study_module_attributes_id = g.pk
516
- new_entry.study_pk_in_delta_weeks = pk
517
- bulk_entries.append(new_entry)
518
- if len(bulk_entries):
519
- PKsForSummedRFDoseStudiesInDeltaWeeks.objects.bulk_create(
520
- bulk_entries
521
- )
522
-
523
- accum_totals = included_studies.aggregate(
524
- Sum(
525
- "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_area_product_total"
526
- ),
527
- Sum(
528
- "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_rp_total"
529
- ),
530
- )
531
- accum_int_proj_to_update.dose_area_product_total_over_delta_weeks = accum_totals[
532
- "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_area_product_total__sum"
533
- ]
534
- accum_int_proj_to_update.dose_rp_total_over_delta_weeks = accum_totals[
535
- "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_rp_total__sum"
536
- ]
537
- accum_int_proj_to_update.save()
538
- populate_rf_delta_weeks_summary(g)
539
-
540
- # Send an e-mail to all high dose alert recipients if this study is at or above threshold levels
541
- send_alert_emails_ref = HighDoseMetricAlertSettings.objects.values_list(
542
- "send_high_dose_metric_alert_emails_ref", flat=True
543
- )[0]
544
- send_alert_emails_skin = HighDoseMetricAlertSettings.objects.values_list(
545
- "send_high_dose_metric_alert_emails_skin", flat=True
546
- )[0]
547
- if send_alert_emails_ref and not send_alert_emails_skin:
548
- send_rf_high_dose_alert_email(g.pk)
549
-
550
- # Add standard names
551
- add_standard_names(g)
552
-
553
-
554
- def _fix_toshiba_vhp(dataset):
555
- """
556
- Replace forward slash in multi-value decimal string VR with back slash
557
- :param dataset: DICOM dataset
558
- :return: Repaired DICOM dataset
559
- """
560
-
561
- for cont in dataset.ContentSequence:
562
- if cont.ConceptNameCodeSequence[0].CodeMeaning == "CT Acquisition":
563
- for cont2 in cont.ContentSequence:
564
- if (
565
- cont2.ConceptNameCodeSequence[0].CodeMeaning
566
- == "Dose Reduce Parameters"
567
- and cont2.ConceptNameCodeSequence[0].CodingSchemeDesignator
568
- == "99TOSHIBA-TMSC"
569
- ):
570
- for cont3 in cont2.ContentSequence:
571
- if (
572
- cont3.ConceptNameCodeSequence[0].CodeMeaning
573
- == "Standard deviation of population"
574
- ):
575
- try:
576
- cont3.MeasuredValueSequence[0].NumericValue
577
- except ValueError:
578
- vhp_sd = dict.__getitem__(
579
- cont3.MeasuredValueSequence[0], 0x40A30A
580
- )
581
- vhp_sd_value = vhp_sd.__getattribute__("value")
582
- if "/" in vhp_sd_value:
583
- vhp_sd_value = vhp_sd_value.replace("/", "\\")
584
- new_vhp_sd = vhp_sd._replace(value=vhp_sd_value)
585
- dict.__setitem__(
586
- cont3.MeasuredValueSequence[0],
587
- 0x40A30A,
588
- new_vhp_sd,
589
- )
590
-
591
-
592
- def rdsr(rdsr_file):
593
- """Extract radiation dose related data from DICOM Radiation SR objects.
594
-
595
- :param rdsr_file: relative or absolute path to Radiation Dose Structured Report.
596
- :type rdsr_file: str.
597
- """
598
-
599
- try:
600
- del_settings = DicomDeleteSettings.objects.get()
601
- del_rdsr = del_settings.del_rdsr
602
- except ObjectDoesNotExist:
603
- del_rdsr = False
604
-
605
- dataset = pydicom.dcmread(rdsr_file)
606
- try:
607
- dataset.decode()
608
- except ValueError as e:
609
- if "Invalid tag (0040, a30a): invalid literal for float()" in e.message:
610
- _fix_toshiba_vhp(dataset)
611
- dataset.decode()
612
-
613
- if (
614
- dataset.SOPClassUID
615
- in (
616
- "1.2.840.10008.5.1.4.1.1.88.67", # X-Ray Radiation Dose SR
617
- "1.2.840.10008.5.1.4.1.1.88.22", # Enhanced SR
618
- )
619
- and dataset.ConceptNameCodeSequence[0].CodeValue
620
- == "113701" # X-Ray Radiation Dose Report
621
- ):
622
- logger.debug("rdsr.py extracting from {0}".format(rdsr_file))
623
- _rdsr2db(dataset)
624
- elif (
625
- dataset.SOPClassUID == ("1.2.840.10008.5.1.4.1.1.88.22") # Enhanced SR
626
- and dataset.ConceptNameCodeSequence[0].CodingSchemeDesignator
627
- == "99SMS_RADSUM" # Siemens Arcadis
628
- and dataset.ConceptNameCodeSequence[0].CodeValue == "C-10"
629
- ):
630
- logger.debug("rdsr.py extracting from {0}".format(rdsr_file))
631
- _rdsr2db(dataset)
632
- elif (
633
- dataset.SOPClassUID
634
- == "1.2.840.10008.5.1.4.1.1.88.68" # Radiopharmaceutical Radiation Dose SR
635
- and dataset.ConceptNameCodeSequence[0].CodeValue
636
- == "113500" # Radiopharmaceutical Radiation Dose Report
637
- ):
638
- logger.debug(f"rdsr.py extracting from {rdsr_file}")
639
- _rdsr2db(dataset)
640
- else:
641
- logger.warning(
642
- "rdsr.py not attempting to extract from {0}, not a radiation dose structured report".format(
643
- rdsr_file
644
- )
645
- )
646
- record_task_error_exit(
647
- f"Not attempting to extract from {rdsr_file}, not an rdsr"
648
- )
649
- return 1
650
-
651
- if del_rdsr:
652
- os.remove(rdsr_file)
653
-
654
- return 0
1
+ # OpenREM - Radiation Exposure Monitoring tools for the physicist
2
+ # Copyright (C) 2012,2013 The Royal Marsden NHS Foundation Trust
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # Additional permission under section 7 of GPLv3:
15
+ # You shall not make any use of the name of The Royal Marsden NHS
16
+ # Foundation trust in connection with this Program in any press or
17
+ # other public announcement without the prior written consent of
18
+ # The Royal Marsden NHS Foundation Trust.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
+
23
+ """
24
+ .. module:: rdsr.
25
+ :synopsis: Module to extract radiation dose related data from DICOM Radiation SR objects
26
+ or Radiopharmaceutical Radiation SR objects
27
+
28
+ .. moduleauthor:: Ed McDonagh
29
+
30
+ """
31
+ from collections import OrderedDict
32
+ from datetime import timedelta
33
+ import logging
34
+ import os
35
+ import sys
36
+
37
+ import django
38
+ from django.db.models import Sum, ObjectDoesNotExist
39
+ import pydicom
40
+
41
+ from openrem.remapp.tools.background import (
42
+ record_task_error_exit,
43
+ record_task_related_query,
44
+ record_task_info,
45
+ run_in_background_with_limits,
46
+ )
47
+
48
+ # setup django/OpenREM.
49
+ basepath = os.path.dirname(__file__)
50
+ projectpath = os.path.abspath(os.path.join(basepath, "..", ".."))
51
+ if projectpath not in sys.path:
52
+ sys.path.insert(1, projectpath)
53
+ os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
54
+ django.setup()
55
+
56
+ from ..tools.check_uid import record_sop_instance_uid
57
+ from ..tools.get_values import (
58
+ get_value_kw,
59
+ )
60
+ from ..tools.make_skin_map import (
61
+ make_skin_map,
62
+ skin_dose_maps_enabled_for_xray_system,
63
+ )
64
+ from ..tools.send_high_dose_alert_emails import send_rf_high_dose_alert_email
65
+ from .extract_common import ( # pylint: disable=wrong-import-order, wrong-import-position
66
+ ct_event_type_count,
67
+ patient_module_attributes,
68
+ populate_mammo_agd_summary,
69
+ populate_dx_rf_summary,
70
+ populate_rf_delta_weeks_summary,
71
+ add_standard_names,
72
+ generalstudymoduleattributes,
73
+ generalequipmentmoduleattributes,
74
+ patientstudymoduleattributes,
75
+ )
76
+ from .rdsr_methods import projectionxrayradiationdose
77
+ from .rrdsr_methods import _radiopharmaceuticalradiationdose
78
+ from remapp.models import ( # pylint: disable=wrong-import-order, wrong-import-position
79
+ AccumIntegratedProjRadiogDose,
80
+ DicomDeleteSettings,
81
+ GeneralStudyModuleAttr,
82
+ HighDoseMetricAlertSettings,
83
+ PKsForSummedRFDoseStudiesInDeltaWeeks,
84
+ SkinDoseMapCalcSettings,
85
+ )
86
+
87
+ logger = logging.getLogger(
88
+ "remapp.extractors.rdsr"
89
+ ) # Explicitly named so that it is still handled when using __main__
90
+
91
+
92
+ def _rdsr_rrdsr_contents(dataset, g):
93
+ try:
94
+ template_identifier = dataset.ContentTemplateSequence[0].TemplateIdentifier
95
+ except AttributeError:
96
+ try:
97
+ if dataset.ContentSequence[0].ConceptCodeSequence[0].CodeValue == "113704":
98
+ template_identifier = "10001"
99
+ elif (
100
+ dataset.ConceptNameCodeSequence[0].CodingSchemeDesignator
101
+ == "99SMS_RADSUM"
102
+ and dataset.ConceptNameCodeSequence[0].CodeValue == "C-10"
103
+ ):
104
+ template_identifier = "10001"
105
+ elif dataset.ConceptCodeSequence[0].CodeValue == "113500":
106
+ template_identifier = "10021"
107
+ else:
108
+ logger.error(
109
+ "Study UID {0} of modality {1} has no template sequence - incomplete RDSR. "
110
+ "Aborting.".format(
111
+ g.study_instance_uid,
112
+ get_value_kw("ManufacturerModelName", dataset),
113
+ )
114
+ )
115
+ g.delete()
116
+ return
117
+ except AttributeError:
118
+ logger.error(
119
+ "Study UID {0} of modality {1} has no template sequence - incomplete RDSR. Aborting.".format(
120
+ g.study_instance_uid, get_value_kw("ManufacturerModelName", dataset)
121
+ )
122
+ )
123
+ g.delete()
124
+ return
125
+ if template_identifier == "10001":
126
+ projectionxrayradiationdose(dataset, g, "projection")
127
+ elif template_identifier == "10011":
128
+ projectionxrayradiationdose(dataset, g, "ct")
129
+ elif template_identifier == "10021":
130
+ _radiopharmaceuticalradiationdose(dataset, g)
131
+ g.save()
132
+ if not g.requested_procedure_code_meaning:
133
+ if "RequestAttributesSequence" in dataset and dataset[0x40, 0x275].VM:
134
+ # Ugly hack to prevent issues with zero length LS16 sequence
135
+ req = dataset.RequestAttributesSequence
136
+ g.requested_procedure_code_meaning = get_value_kw(
137
+ "RequestedProcedureDescription", req[0]
138
+ )
139
+ # Sometimes the above is true, but there is no RequestedProcedureDescription in that sequence, but
140
+ # there is a basic field as below.
141
+ if not g.requested_procedure_code_meaning:
142
+ g.requested_procedure_code_meaning = get_value_kw(
143
+ "RequestedProcedureDescription", dataset
144
+ )
145
+ g.save()
146
+ else:
147
+ g.requested_procedure_code_meaning = get_value_kw(
148
+ "RequestedProcedureDescription", dataset
149
+ )
150
+ g.save()
151
+
152
+ try:
153
+ number_of_events_ct = (
154
+ g.ctradiationdose_set.get().ctirradiationeventdata_set.count()
155
+ )
156
+ except ObjectDoesNotExist:
157
+ number_of_events_ct = 0
158
+ try:
159
+ number_of_events_proj = (
160
+ g.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
161
+ )
162
+ except ObjectDoesNotExist:
163
+ number_of_events_proj = 0
164
+ g.number_of_events = number_of_events_ct + number_of_events_proj
165
+ g.save()
166
+ if template_identifier == "10011":
167
+ ct_event_type_count(g)
168
+ try:
169
+ g.total_dlp = (
170
+ g.ctradiationdose_set.get()
171
+ .ctaccumulateddosedata_set.get()
172
+ .ct_dose_length_product_total
173
+ )
174
+ g.save()
175
+ except ObjectDoesNotExist:
176
+ logger.warning(
177
+ "Study UID {0} of modality {1}. Unable to set summary total_dlp".format(
178
+ g.study_instance_uid, get_value_kw("ManufacturerModelName", dataset)
179
+ )
180
+ )
181
+ elif template_identifier == "10001":
182
+ if g.modality_type == "MG":
183
+ populate_mammo_agd_summary(g)
184
+ else:
185
+ populate_dx_rf_summary(g)
186
+
187
+
188
+ def _get_existing_event_uids(study):
189
+ """
190
+ Returns all event uids stored for this study
191
+ """
192
+ existing_event_uids = set()
193
+ s = study.ctradiationdose_set.first()
194
+ if s is not None:
195
+ for event in s.ctirradiationeventdata_set.all():
196
+ existing_event_uids.add(event.irradiation_event_uid)
197
+
198
+ s = study.projectionxrayradiationdose_set.first()
199
+ if s is not None:
200
+ for event in s.irradeventxraydata_set.all():
201
+ existing_event_uids.add(event.irradiation_event_uid)
202
+
203
+ s = study.radiopharmaceuticalradiationdose_set.first()
204
+ if s is not None:
205
+ for event in s.radiopharmaceuticaladministrationeventdata_set.all():
206
+ existing_event_uids.add(event.radiopharmaceutical_administration_event_uid)
207
+
208
+ return existing_event_uids
209
+
210
+
211
+ def _update_recorded_objects(g, dataset, existing_sop_instance_uids=None):
212
+ new_sop_instance_uid = dataset.SOPInstanceUID
213
+ record_sop_instance_uid(g, new_sop_instance_uid)
214
+ if existing_sop_instance_uids is not None:
215
+ for sop_instance_uid in existing_sop_instance_uids:
216
+ record_sop_instance_uid(g, sop_instance_uid)
217
+
218
+
219
+ def _get_dataset_event_uids(dataset):
220
+ """
221
+ Collect the uids from all events that can be in a dataset
222
+ and that we care about
223
+ """
224
+ new_event_uids = set()
225
+ for content in dataset.ContentSequence:
226
+ if content.ValueType and content.ValueType == "CONTAINER":
227
+ if content.ConceptNameCodeSequence[0].CodeMeaning in (
228
+ "CT Acquisition",
229
+ "Irradiation Event X-Ray Data",
230
+ "Radiopharmaceutical Administration",
231
+ ):
232
+ for item in content.ContentSequence:
233
+ if item.ConceptNameCodeSequence[0].CodeMeaning in (
234
+ "Irradiation Event UID",
235
+ "Radiopharmaceutical Administration Event UID",
236
+ ):
237
+ new_event_uids.add("{0}".format(item.UID))
238
+ return new_event_uids
239
+
240
+
241
+ def _handle_study_already_existing(
242
+ dataset,
243
+ ):
244
+ """
245
+ This function checks wheter there is already a study with the same instance uid as dataset.
246
+
247
+ If yes it checks the data in the dataset and the existing study
248
+ returning different strategies for continuing the import:
249
+
250
+ If dataset was imported already: Abort the import
251
+ If dataset contains subset of events of existing study: Abort the import
252
+ If dataset has same events as existing study: Abort the import
253
+ If dataset has more events than existing study (Events of existing are subset):
254
+ Delete old study and reimport
255
+ If dataset has different events than existing study: Create a second study and import
256
+ If dataset is rrdsr and only images have been imported to the study: Complete study with data from the rrdsr
257
+
258
+ :return: A dict with possible keys {status, existing_sop_instance_uids, study}, where
259
+ :status: One of 'abort', 'continue' (Create the study and maybe save old instance uids to it),
260
+ 'drop_old_continue' (Delete old and reimport),
261
+ 'retry' (Wait then call this function again, there are partially imported studies), append_rrdsr'
262
+ :existing_sop_instance_uids: The sop instance uids of all files imported for this study. For e.g.
263
+ drop_old_continue status this should be saved onto the new study entry. (See _update_recorded_objects)
264
+ :study: The study that was used to make the decision what to do next.
265
+ Only added for status='append_rrdsr' or status='drop_old_continue' at the moment
266
+ """
267
+ existing_sop_instance_uids = set()
268
+
269
+ study_uid = dataset.StudyInstanceUID
270
+ existing_study_uid_match = GeneralStudyModuleAttr.objects.filter(
271
+ study_instance_uid__exact=study_uid
272
+ )
273
+ if existing_study_uid_match:
274
+ new_sop_instance_uid = dataset.SOPInstanceUID
275
+ for existing_study in existing_study_uid_match.order_by("pk"):
276
+ for processed_object in existing_study.objectuidsprocessed_set.all():
277
+ existing_sop_instance_uids.add(processed_object.sop_instance_uid)
278
+ if new_sop_instance_uid in existing_sop_instance_uids:
279
+ # We've dealt with this object before...
280
+ logger.debug(
281
+ "Import match on Study Instance UID {0} and object SOP Instance UID {1}. "
282
+ "Will not import.".format(study_uid, new_sop_instance_uid)
283
+ )
284
+ record_task_error_exit("Study already in db")
285
+ return {
286
+ "status": "abort",
287
+ "existing_sop_instance_uids": existing_sop_instance_uids,
288
+ }
289
+ # Either we've not seen it before, or it wasn't recorded when we did.
290
+ # Next find the event UIDs in the RDSR being imported
291
+ new_event_uids = _get_dataset_event_uids(dataset)
292
+ logger.debug(
293
+ "Import match on StudyInstUID {0}. New RDSR event UIDs {1}".format(
294
+ study_uid, new_event_uids
295
+ )
296
+ )
297
+
298
+ # Now check which event UIDs are in the database already
299
+ existing_event_uids = OrderedDict()
300
+ for i, existing_study in enumerate(existing_study_uid_match.order_by("pk")):
301
+ existing_event_uids[i] = set()
302
+ existing_event_uids[i] = _get_existing_event_uids(existing_study)
303
+ logger.debug(
304
+ "Import match on StudyInstUID {0}. Existing event UIDs {1}".format(
305
+ study_uid, existing_event_uids
306
+ )
307
+ )
308
+
309
+ # Now compare the two
310
+ for study_index, uid_list in list(existing_event_uids.items()):
311
+ if uid_list == new_event_uids:
312
+ # New RDSR is the same as the existing one
313
+ logger.debug(
314
+ "Import match on StudyInstUID {0}. Event level match, will not import.".format(
315
+ study_uid
316
+ )
317
+ )
318
+ record_sop_instance_uid(
319
+ existing_study_uid_match[study_index], new_sop_instance_uid
320
+ )
321
+ record_task_error_exit("Study already in db")
322
+ return {
323
+ "status": "abort",
324
+ "existing_sop_instance_uids": existing_sop_instance_uids,
325
+ }
326
+ elif new_event_uids.issubset(uid_list):
327
+ # New RDSR has the same but fewer events than existing one
328
+ logger.debug(
329
+ "Import match on StudyInstUID {0}. New RDSR events are subset of existing events. "
330
+ "Will not import.".format(study_uid)
331
+ )
332
+ record_sop_instance_uid(
333
+ existing_study_uid_match[study_index], new_sop_instance_uid
334
+ )
335
+ record_task_error_exit("Study already in db")
336
+ return {
337
+ "status": "abort",
338
+ "existing_sop_instance_uids": existing_sop_instance_uids,
339
+ }
340
+ elif uid_list.issubset(new_event_uids):
341
+ # New RDSR has the existing events and more
342
+ # Check existing one had finished importing
343
+ logger.debug(
344
+ "Import match on StudyInstUID {0}. Existing events are subset of new events. Will"
345
+ " import.".format(study_uid)
346
+ )
347
+ return {
348
+ "status": "drop_old_continue",
349
+ "existing_sop_instance_uids": existing_sop_instance_uids,
350
+ "study": existing_study_uid_match[study_index],
351
+ }
352
+ elif None in uid_list:
353
+ # This happens for NM studies where only images have been imported so far
354
+ # because they add a RadiopharmaceuticalAdministrationEventData Object without UID.
355
+ logger.debug(
356
+ f"Import match on StudyInstUID {study_uid}. There is already a NM study for"
357
+ "which only Images where imported so far. Will delete the "
358
+ "RadiopharmaceuticalAdministrationEventData and RadiopharmaceuticalRadioationDose"
359
+ "and reimport, but keep the PET_Series data if present."
360
+ )
361
+ study = existing_study_uid_match[study_index]
362
+ if study.modality_type == "NM" and (
363
+ dataset.SOPClassUID
364
+ == "1.2.840.10008.5.1.4.1.1.88.68" # Radiopharmaceutical Radiation Dose SR
365
+ and dataset.ConceptNameCodeSequence[0].CodeValue
366
+ == "113500" # Radiopharmaceutical Radiation Dose Report
367
+ ):
368
+ tmp = study.radiopharmaceuticalradiationdose_set.first()
369
+ if tmp is not None:
370
+ tmp = tmp.radiopharmaceuticaladministrationeventdata_set.first()
371
+ if tmp is not None:
372
+ if tmp.radiopharmaceutical_administration_event_uid is None:
373
+ return {
374
+ "status": "append_rrdsr",
375
+ "existing_sop_instance_uids": existing_sop_instance_uids,
376
+ "study": study,
377
+ }
378
+
379
+ return {
380
+ "status": "continue",
381
+ "existing_sop_instance_uids": existing_sop_instance_uids,
382
+ }
383
+
384
+
385
+ def _rdsr2db(dataset):
386
+ if "StudyInstanceUID" in dataset:
387
+ study_uid = dataset.StudyInstanceUID
388
+ study_uid = dataset.StudyInstanceUID
389
+ record_task_info(f"Study UID: {study_uid.replace('.', '. ')}")
390
+ record_task_related_query(study_uid)
391
+ existing = _handle_study_already_existing(dataset)
392
+
393
+ if existing["status"] == "abort":
394
+ return
395
+ elif existing["status"] == "append_rrdsr":
396
+ _update_recorded_objects(existing["study"], dataset)
397
+ _rdsr_rrdsr_contents(dataset, existing["study"])
398
+ radios = existing["study"].radiopharmaceuticalradiationdose_set.all()
399
+ if (
400
+ radios[0]
401
+ .radiopharmaceuticaladministrationeventdata_set.get()
402
+ .radiopharmaceutical_administration_event_uid
403
+ is None
404
+ ):
405
+ radio_old, radio_new = (radios[0], radios[1])
406
+ else:
407
+ radio_new, radio_old = (radios[0], radios[1])
408
+ radio_old.petseries_set.update(radiopharmaceutical_radiation_dose=radio_new)
409
+ radio_old.delete()
410
+ return
411
+ elif existing["status"] == "drop_old_continue":
412
+ existing["study"].delete()
413
+ elif existing["status"] == "continue":
414
+ pass # We are allowed to proceed importing
415
+
416
+ g = GeneralStudyModuleAttr.objects.create()
417
+ if not g: # Allows import to be aborted if no template found
418
+ return
419
+ g.save()
420
+ if existing["status"] == "drop_old_continue":
421
+ _update_recorded_objects(g, dataset, existing["existing_sop_instance_uids"])
422
+ else:
423
+ _update_recorded_objects(g, dataset)
424
+ generalstudymoduleattributes(dataset, g, logger)
425
+ generalequipmentmoduleattributes(dataset, g)
426
+ _rdsr_rrdsr_contents(dataset, g)
427
+ patientstudymoduleattributes(dataset, g)
428
+ patient_module_attributes(dataset, g)
429
+
430
+ try:
431
+ SkinDoseMapCalcSettings.objects.get()
432
+ except ObjectDoesNotExist:
433
+ SkinDoseMapCalcSettings.objects.create()
434
+
435
+ enable_skin_dose_maps = SkinDoseMapCalcSettings.objects.values_list(
436
+ "enable_skin_dose_maps", flat=True
437
+ )[0]
438
+ calc_on_import = SkinDoseMapCalcSettings.objects.values_list(
439
+ "calc_on_import", flat=True
440
+ )[0]
441
+ if g.modality_type == "RF" and enable_skin_dose_maps and calc_on_import:
442
+ skin_maps_enabled = skin_dose_maps_enabled_for_xray_system(g)
443
+ if skin_maps_enabled:
444
+ run_in_background_with_limits(
445
+ make_skin_map,
446
+ "make_skin_map",
447
+ 0,
448
+ {"make_skin_map": 1},
449
+ g.pk,
450
+ )
451
+
452
+ # Calculate summed total DAP and dose at RP for studies that have this study's patient ID, going back week_delta
453
+ # weeks in time from this study date. Only do this if activated in the fluoro alert settings (check whether
454
+ # HighDoseMetricAlertSettings.calc_accum_dose_over_delta_weeks_on_import is True).
455
+ if g.modality_type == "RF":
456
+
457
+ try:
458
+ HighDoseMetricAlertSettings.objects.get()
459
+ except ObjectDoesNotExist:
460
+ HighDoseMetricAlertSettings.objects.create()
461
+
462
+ week_delta = HighDoseMetricAlertSettings.objects.values_list(
463
+ "accum_dose_delta_weeks", flat=True
464
+ )[0]
465
+ calc_accum_dose_over_delta_weeks_on_import = (
466
+ HighDoseMetricAlertSettings.objects.values_list(
467
+ "calc_accum_dose_over_delta_weeks_on_import", flat=True
468
+ )[0]
469
+ )
470
+ if calc_accum_dose_over_delta_weeks_on_import:
471
+
472
+ all_rf_studies = GeneralStudyModuleAttr.objects.filter(
473
+ modality_type__exact="RF"
474
+ ).all()
475
+
476
+ patient_id = g.patientmoduleattr_set.values_list("patient_id", flat=True)[0]
477
+ if patient_id:
478
+ study_date = g.study_date
479
+ oldest_date = study_date - timedelta(weeks=week_delta)
480
+
481
+ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
482
+ # The try and except parts of this code are here because some of the studies in my database didn't have the
483
+ # expected data in the related fields - not sure why. Perhaps an issue with the extractor routine?
484
+ try:
485
+ g.projectionxrayradiationdose_set.get().accumxraydose_set.all()
486
+ except ObjectDoesNotExist:
487
+ g.projectionxrayradiationdose_set.get().accumxraydose_set.create()
488
+
489
+ for (
490
+ accumxraydose
491
+ ) in g.projectionxrayradiationdose_set.get().accumxraydose_set.all():
492
+ try:
493
+ accumxraydose.accumintegratedprojradiogdose_set.get()
494
+ except:
495
+ accumxraydose.accumintegratedprojradiogdose_set.create()
496
+ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
497
+
498
+ for (
499
+ accumxraydose
500
+ ) in g.projectionxrayradiationdose_set.get().accumxraydose_set.all():
501
+ accum_int_proj_pk = (
502
+ accumxraydose.accumintegratedprojradiogdose_set.get().pk
503
+ )
504
+
505
+ accum_int_proj_to_update = (
506
+ AccumIntegratedProjRadiogDose.objects.get(pk=accum_int_proj_pk)
507
+ )
508
+
509
+ included_studies = all_rf_studies.filter(
510
+ patientmoduleattr__patient_id__exact=patient_id,
511
+ study_date__range=[oldest_date, study_date],
512
+ )
513
+
514
+ bulk_entries = []
515
+ for pk in included_studies.values_list("pk", flat=True):
516
+ if not PKsForSummedRFDoseStudiesInDeltaWeeks.objects.filter(
517
+ general_study_module_attributes_id__exact=g.pk
518
+ ).filter(study_pk_in_delta_weeks__exact=pk):
519
+ new_entry = PKsForSummedRFDoseStudiesInDeltaWeeks()
520
+ new_entry.general_study_module_attributes_id = g.pk
521
+ new_entry.study_pk_in_delta_weeks = pk
522
+ bulk_entries.append(new_entry)
523
+ if len(bulk_entries):
524
+ PKsForSummedRFDoseStudiesInDeltaWeeks.objects.bulk_create(
525
+ bulk_entries
526
+ )
527
+
528
+ accum_totals = included_studies.aggregate(
529
+ Sum(
530
+ "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_area_product_total"
531
+ ),
532
+ Sum(
533
+ "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_rp_total"
534
+ ),
535
+ )
536
+ accum_int_proj_to_update.dose_area_product_total_over_delta_weeks = accum_totals[
537
+ "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_area_product_total__sum"
538
+ ]
539
+ accum_int_proj_to_update.dose_rp_total_over_delta_weeks = accum_totals[
540
+ "projectionxrayradiationdose__accumxraydose__accumintegratedprojradiogdose__dose_rp_total__sum"
541
+ ]
542
+ accum_int_proj_to_update.save()
543
+ populate_rf_delta_weeks_summary(g)
544
+
545
+ # Send an e-mail to all high dose alert recipients if this study is at or above threshold levels
546
+ send_alert_emails_ref = HighDoseMetricAlertSettings.objects.values_list(
547
+ "send_high_dose_metric_alert_emails_ref", flat=True
548
+ )[0]
549
+ send_alert_emails_skin = HighDoseMetricAlertSettings.objects.values_list(
550
+ "send_high_dose_metric_alert_emails_skin", flat=True
551
+ )[0]
552
+ if send_alert_emails_ref and not send_alert_emails_skin:
553
+ send_rf_high_dose_alert_email(g.pk)
554
+
555
+ # Add standard names
556
+ add_standard_names(g)
557
+
558
+
559
+ def _fix_toshiba_vhp(dataset):
560
+ """
561
+ Replace forward slash in multi-value decimal string VR with back slash
562
+ :param dataset: DICOM dataset
563
+ :return: Repaired DICOM dataset
564
+ """
565
+
566
+ for cont in dataset.ContentSequence:
567
+ if cont.ConceptNameCodeSequence[0].CodeMeaning == "CT Acquisition":
568
+ for cont2 in cont.ContentSequence:
569
+ if (
570
+ cont2.ConceptNameCodeSequence[0].CodeMeaning
571
+ == "Dose Reduce Parameters"
572
+ and cont2.ConceptNameCodeSequence[0].CodingSchemeDesignator
573
+ == "99TOSHIBA-TMSC"
574
+ ):
575
+ for cont3 in cont2.ContentSequence:
576
+ if (
577
+ cont3.ConceptNameCodeSequence[0].CodeMeaning
578
+ == "Standard deviation of population"
579
+ ):
580
+ try:
581
+ cont3.MeasuredValueSequence[0].NumericValue
582
+ except ValueError:
583
+ vhp_sd = dict.__getitem__(
584
+ cont3.MeasuredValueSequence[0], 0x40A30A
585
+ )
586
+ vhp_sd_value = vhp_sd.__getattribute__("value")
587
+ if "/" in vhp_sd_value:
588
+ vhp_sd_value = vhp_sd_value.replace("/", "\\")
589
+ new_vhp_sd = vhp_sd._replace(value=vhp_sd_value)
590
+ dict.__setitem__(
591
+ cont3.MeasuredValueSequence[0],
592
+ 0x40A30A,
593
+ new_vhp_sd,
594
+ )
595
+
596
+
597
+ def rdsr(rdsr_file):
598
+ """Extract radiation dose related data from DICOM Radiation SR objects.
599
+
600
+ :param rdsr_file: relative or absolute path to Radiation Dose Structured Report.
601
+ :type rdsr_file: str.
602
+ """
603
+
604
+ try:
605
+ del_settings = DicomDeleteSettings.objects.get()
606
+ del_rdsr = del_settings.del_rdsr
607
+ except ObjectDoesNotExist:
608
+ del_rdsr = False
609
+
610
+ try:
611
+ dataset = pydicom.dcmread(rdsr_file)
612
+ except FileNotFoundError:
613
+ logger.warning(
614
+ f"rdsr.py not attempting to extract from {rdsr_file}, the file does not exist"
615
+ )
616
+ record_task_error_exit(
617
+ f"Not attempting to extract from {rdsr_file}, the file does not exist"
618
+ )
619
+ return 1
620
+
621
+ try:
622
+ dataset.decode()
623
+ except ValueError as e:
624
+ if "Invalid tag (0040, a30a): invalid literal for float()" in e.message:
625
+ _fix_toshiba_vhp(dataset)
626
+ dataset.decode()
627
+
628
+ if (
629
+ dataset.SOPClassUID
630
+ in (
631
+ "1.2.840.10008.5.1.4.1.1.88.67", # X-Ray Radiation Dose SR
632
+ "1.2.840.10008.5.1.4.1.1.88.22", # Enhanced SR
633
+ )
634
+ and dataset.ConceptNameCodeSequence[0].CodeValue
635
+ == "113701" # X-Ray Radiation Dose Report
636
+ ):
637
+ logger.debug("rdsr.py extracting from {0}".format(rdsr_file))
638
+ _rdsr2db(dataset)
639
+ elif (
640
+ dataset.SOPClassUID == ("1.2.840.10008.5.1.4.1.1.88.22") # Enhanced SR
641
+ and dataset.ConceptNameCodeSequence[0].CodingSchemeDesignator
642
+ == "99SMS_RADSUM" # Siemens Arcadis
643
+ and dataset.ConceptNameCodeSequence[0].CodeValue == "C-10"
644
+ ):
645
+ logger.debug("rdsr.py extracting from {0}".format(rdsr_file))
646
+ _rdsr2db(dataset)
647
+ elif (
648
+ dataset.SOPClassUID
649
+ == "1.2.840.10008.5.1.4.1.1.88.68" # Radiopharmaceutical Radiation Dose SR
650
+ and dataset.ConceptNameCodeSequence[0].CodeValue
651
+ == "113500" # Radiopharmaceutical Radiation Dose Report
652
+ ):
653
+ logger.debug(f"rdsr.py extracting from {rdsr_file}")
654
+ _rdsr2db(dataset)
655
+ else:
656
+ logger.warning(
657
+ f"rdsr.py not attempting to extract from {rdsr_file}, not a radiation dose structured report"
658
+ )
659
+ record_task_error_exit(
660
+ f"Not attempting to extract from {rdsr_file}, not an rdsr"
661
+ )
662
+ return 1
663
+
664
+ if del_rdsr:
665
+ os.remove(rdsr_file)
666
+
667
+ return 0