Skip to content

Commit

Permalink
Calc impac privates (#70)
Browse files Browse the repository at this point in the history
* provide utility for adding impac private elements to spot scanning plans for interop testing purposes

* made the script executable
  • Loading branch information
sjswerdloff authored Aug 17, 2024
1 parent 95fe95a commit b7a9086
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{"name":"Python Debugger: Current File with Arguments","type":"debugpy","request":"launch","program":"${file}","console":"integratedTerminal","args":["${command:pickArgs}"]},
{
"name": "Python Debugger: Current File with Arguments",
"type": "debugpy",
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"HHMM",
"HHMMSS",
"ilename",
"impac",
"IPDW",
"irectory",
"Mddhhmm",
Expand All @@ -33,6 +34,7 @@
"nactionscu",
"ncreate",
"ncreatescu",
"ndigits",
"nget",
"nset",
"nsetscu",
Expand All @@ -47,6 +49,7 @@
"repval",
"rtss",
"sessionmaker",
"sobp",
"storescp",
"storescu",
"tsyntax",
Expand Down
83 changes: 83 additions & 0 deletions calculate_and_add_impac_privates_to_rtionplan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
import json
import math
import sys

from pydicom import Dataset, datadict, dcmread, dcmwrite

from impac_privates import impac_private_dict

if __name__ == "__main__":
energy_range_mm_dict = {}
energy_string_range_dict = {}
ion_plan_ds: Dataset = None
# Values calculated on PSTAR NIST web site at integer energies in MeV
# Assuming Linear Interpolation between integer energies is close enough
# for non-clinical purposes, like testing workflow/interoperability.
with open("proton_range_millimeters_water.json", "r") as f:
energy_string_range_dict = json.load(f)
energy_range_mm_dict = {int(x): y for x, y in energy_string_range_dict.items()}

datadict.add_private_dict_entries("IMPAC", impac_private_dict)

with open(sys.argv[1], "rb") as g:
ion_plan_ds = dcmread(g)

try:
impac_block = ion_plan_ds.IonBeamSequence[0].private_block(0x300B, "IMPAC")
if 0x04 in impac_block:
print(f"File already contains IMPAC private for {impac_private_dict[0x300B1004][2]}")
if len(sys.argv) < 2: # unless they want to force it with a -f or something
sys.exit()
except KeyError:
pass # no impac block so we're good to go

new_plan_name = sys.argv[1].removesuffix(".dcm") + "_ims_pvt.dcm"
for beam in ion_plan_ds.IonBeamSequence:
if beam.ScanMode not in ["MODULATED", "MODULATED_SPEC"]:
print(f"Scan Mode is {beam.ScanMode} which isn't supported in this script/module")
sys.exit()
cp_sequence = beam.IonControlPointSequence
cp_energy_list = [x.NominalBeamEnergy for x in cp_sequence]
max_energy = max(cp_energy_list)
min_energy = min(cp_energy_list)
ceiling_max_energy = math.ceil(max_energy)
floor_max_energy = math.floor(max_energy)
ceiling_max_weight = max_energy - floor_max_energy

ceiling_min_energy = math.ceil(min_energy)
floor_min_energy = math.floor(min_energy)
ceiling_min_weight = min_energy - floor_min_energy

# this isn't taking in to account a Range Shifter or a Range Modulator
# but for simple interoperability and workflow testing, it will do.
distal_range_millimeters = (
energy_range_mm_dict[floor_max_energy] * (1.0 - ceiling_max_weight)
+ energy_range_mm_dict[ceiling_max_energy] * ceiling_max_weight
)
proximal_range_millimeters = (
energy_range_mm_dict[floor_min_energy] * (1.0 - ceiling_min_weight)
+ energy_range_mm_dict[ceiling_min_energy] * ceiling_min_weight
)
sobp_equivalent_mm = round(distal_range_millimeters - proximal_range_millimeters, ndigits=1)

beam_max_radius_squared = 0
for cp in cp_sequence:
x_values = cp.ScanSpotPositionMap[0::2]
y_values = cp.ScanSpotPositionMap[1::2]
tuple_list = []
r_squared_list = [x * x + y * y for x, y in zip(x_values, y_values)]
max_r_squared = max(r_squared_list)
if beam_max_radius_squared < max_r_squared:
beam_max_radius_squared = max_r_squared

beam_max_radius = math.sqrt(beam_max_radius_squared)
new_private_block = beam.private_block(0x300B, "IMPAC", create=True)
new_private_block.add_new(0x04, "FL", round(distal_range_millimeters, 1))
new_private_block.add_new(0x02, "FL", round(beam_max_radius, 1))
new_private_block.add_new(0x0E, "FL", round(sobp_equivalent_mm, 1))

dcmwrite(
new_plan_name, ion_plan_ds, write_like_original=True
) # assume the input was ExplicitLittleEndian for later tasks, see convert_to_explicit_little_endian.py
print(f"Wrote updated file to {new_plan_name}")
50 changes: 50 additions & 0 deletions convert_to_explicit_little_endian.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python
""" if the data contains private elements that aren't known to pydicom,
and the file is transmitted to qrscp or some other system that doesn't handle unknown privates
when using *implicit* little endian,
then this conversion is needed to avoid data loss later on.
@author: stuartswerdloff
"""
import sys
from pathlib import Path

from pydicom import dcmread
from pydicom.uid import ExplicitVRLittleEndian


def convert(filename: str | Path, output_filename: str | Path):
try:
f = open(filename, "rb")
ds = dcmread(f, force=True)

f.close()
ds.is_little_endian = True

# if the data contains private elements that aren't known to pydicom,
# and the file is transmitted to qrscp or some other system that doesn't handle unknown privates
# when using *implicit* little endian,
# then this is needed to avoid data loss later on.
ds.is_implicit_VR = False

ds.ensure_file_meta()
ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
ds.fix_meta_info()
ds.save_as(output_filename, write_like_original=False)
except IOError:
print("Cannot read input file {0!s}".format(filename))
raise


if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"{sys.argv[0]} input_file_path output_file_path")
sys.exit()

filename = sys.argv[1]
output_filename = sys.argv[2]
print(filename)
try:
convert(filename=filename, output_filename=output_filename)
except IOError:
print("Cannot read input file {0!s}".format(filename))
sys.exit()
49 changes: 49 additions & 0 deletions impac_privates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
impac_private_dict = {
0x30091001: ("FL", "1", "Measured Distal Target Distance", ""), # noqa
0x30091002: ("FL", "1", "Specified Primary Ambient Meterset", ""), # noqa
0x30091003: ("FL", "1", "Delivered Primary Ambient Meterset", ""), # noqa
0x30091004: ("FL", "1", "Current Temperature", ""), # noqa
0x30091005: ("FL", "1", "Current Pressure", ""), # noqa
0x30091006: ("FL", "1", "TP Correction Factor", ""), # noqa
0x30091007: ("FL", "1", "Measured Uncollimated Field Diameter", ""), # noqa
0x30091008: ("FL", "1", "Measured SOBP Width", ""), # noqa
0x30091010: ("FL", "1", "Table Top Vertical Correction", ""), # noqa
0x30091011: ("FL", "1", "Table Top Longitudinal Correction", ""), # noqa
0x30091012: ("FL", "1", "Table Top Lateral Correction", ""), # noqa
0x30091013: ("FL", "1", "Patient Support Angle Correction", ""), # noqa
0x30091014: ("FL", "1", "Patient Support Pitch Angle Correction", ""), # noqa
0x30091015: ("FL", "1", "Patient Support Roll Angle Correction", ""), # noqa
0x30091016: ("IS", "1", "Number of Paintings Fully Delivered", ""), # noqa
0x30091017: ("IS", "1", "Treatment Termination Scan Spot Index", ""), # noqa
0x30091047: ("FL", "2-n", "Line Scan Metersets Delivered", ""), # noqa
0x300B1001: ("FL", "1", "Distal Target Distance Tolerance", ""), # noqa
0x300B1002: ("FL", "1", "Maximum Collimated Field Diameter", ""), # noqa
0x300B1003: ("CS", "1", "Beam Check Flag", ""), # noqa
0x300B1004: ("FL", "1", "Planned Distal Target Distance", ""), # noqa
0x300B1005: ("CS", "1", "Treatment Delivery Status", ""), # noqa
0x300B1006: ("CS", "1", "Treatment Machine Mode", ""), # noqa
0x300B1007: ("CS", "1", "Position Correction Flag", ""), # noqa
0x300B1008: ("SH", "1", "Beam Line Data Table Version", ""), # noqa
0x300B1009: ("CS", "1", "Respiratory Gating Flag", ""), # noqa
0x300B100A: ("FL", "1", "Respiratory Gating Cycle", ""), # noqa
0x300B100B: ("FL", "1", "Flat Top Length", ""), # noqa
0x300B100C: ("FL", "1", "Spill Length", ""), # noqa
0x300B100D: ("FL", "1", "Uncollimated Field Diameter Tolerance", ""), # noqa
0x300B100E: ("FL", "1", "Nominal SOBP Width", ""), # noqa
0x300B1011: ("FL", "1", "Nominal SOBP Width Tolerance", ""), # noqa
0x300B1012: ("FL", "1", "TP Corrected Meterset Tolerance", ""), # noqa
0x300B1013: ("IS", "1", "Number of Pieces", ""), # noqa
0x300B1014: ("FL", "1-n", "Change Check Data Before", ""), # noqa
0x300B1015: ("FL", "1-n", "Change Check Data After", ""), # noqa
0x300B1016: ("FL", "1", "Beam Intensity", ""), # noqa
0x300B1017: ("FL", "1", "Peak Range", ""), # noqa
0x300B1018: ("DS", "1", "Planned Patient Support Angle", ""), # noqa
0x300B101A: ("FL", "1", "Planned Table Top Roll Angle", ""), # noqa
0x300B1020: ("DS", "2", "Respiratory Phase Gating Duty Cycle", ""), # noqa
0x300B1090: ("SH", "2", "Line Spot Tune ID", ""), # noqa
0x300B1092: ("IS", "1", "Number of Line Scan Spot Positions", ""), # noqa
0x300B1094: ("FL", "2-n", "Line Scan Position Map", ""), # noqa
0x300B1096: ("FL", "2-n", "Line Scan Meterset Weights", ""), # noqa
0x300B1098: ("FL", "2", "Line Scanning Spot Size", ""), # noqa
0x300B109A: ("IS", "1", "Number of Line Scan Paintings", ""), # noqa
}
1 change: 1 addition & 0 deletions proton_range_millimeters_water.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"1": 0.02, "2": 0.08, "3": 0.15, "4": 0.25, "5": 0.36, "6": 0.5, "7": 0.65, "8": 0.83, "9": 1.02, "10": 1.23, "11": 1.45, "12": 1.7, "13": 1.96, "14": 2.24, "15": 2.54, "16": 2.85, "17": 3.17, "18": 3.52, "19": 3.88, "20": 4.25, "21": 4.64, "22": 5.05, "23": 5.47, "24": 5.91, "25": 6.36, "26": 6.83, "27": 7.31, "28": 7.8, "29": 8.31, "30": 8.84, "31": 9.38, "32": 9.93, "33": 10.5, "34": 11.08, "35": 11.68, "36": 12.29, "37": 12.91, "38": 13.55, "39": 14.2, "40": 14.86, "41": 15.54, "42": 16.23, "43": 16.94, "44": 17.66, "45": 18.39, "46": 19.13, "47": 19.89, "48": 20.66, "49": 21.44, "50": 22.24, "51": 23.05, "52": 23.87, "53": 24.7, "54": 25.55, "55": 26.41, "56": 27.28, "57": 28.16, "58": 29.06, "59": 29.97, "60": 30.89, "61": 31.82, "62": 32.76, "63": 33.72, "64": 34.69, "65": 35.67, "66": 36.66, "67": 37.66, "68": 38.68, "69": 39.71, "70": 40.75, "71": 41.8, "72": 42.86, "73": 43.93, "74": 45.02, "75": 46.11, "76": 47.22, "77": 48.34, "78": 49.47, "79": 50.61, "80": 51.76, "81": 52.93, "82": 54.1, "83": 55.29, "84": 56.48, "85": 57.69, "86": 58.91, "87": 60.14, "88": 61.38, "89": 62.63, "90": 63.89, "91": 65.16, "92": 66.44, "93": 67.73, "94": 69.04, "95": 70.35, "96": 71.67, "97": 73.01, "98": 74.35, "99": 75.71, "100": 77.07, "101": 78.45, "102": 79.83, "103": 81.23, "104": 82.64, "105": 84.05, "106": 85.48, "107": 86.91, "108": 88.36, "109": 89.81, "110": 91.28, "111": 92.75, "112": 94.24, "113": 95.73, "114": 97.24, "115": 98.75, "116": 100.3, "117": 101.8, "118": 103.4, "119": 104.9, "120": 106.5, "121": 108.0, "122": 109.6, "123": 111.2, "124": 112.8, "125": 114.4, "126": 116.0, "127": 117.7, "128": 119.3, "129": 120.9, "130": 122.6, "131": 124.3, "132": 125.9, "133": 127.6, "134": 129.3, "135": 131.0, "136": 132.7, "137": 134.4, "138": 136.2, "139": 137.9, "140": 139.6, "141": 141.4, "142": 143.1, "143": 144.9, "144": 146.7, "145": 148.5, "146": 150.3, "147": 152.1, "148": 153.9, "149": 155.7, "150": 157.6, "151": 159.4, "152": 161.2, "153": 163.1, "154": 165.0, "155": 166.8, "156": 168.7, "157": 170.6, "158": 172.5, "159": 174.4, "160": 176.3, "161": 178.2, "162": 180.2, "163": 182.1, "164": 184.1, "165": 186.0, "166": 188.0, "167": 189.9, "168": 191.9, "169": 193.9, "170": 195.9, "171": 197.9, "172": 199.9, "173": 201.9, "174": 204.0, "175": 206.0, "176": 208.0, "177": 210.1, "178": 212.1, "179": 214.2, "180": 216.3, "181": 218.3, "182": 220.4, "183": 222.5, "184": 224.6, "185": 226.7, "186": 228.9, "187": 231.0, "188": 233.1, "189": 235.3, "190": 237.4, "191": 239.6, "192": 241.7, "193": 243.9, "194": 246.1, "195": 248.3, "196": 250.4, "197": 252.6, "198": 254.8, "199": 257.1, "200": 259.3, "201": 261.5, "202": 263.7, "203": 266.0, "204": 268.2, "205": 270.5, "206": 272.8, "207": 275.0, "208": 277.3, "209": 279.6, "210": 281.9, "211": 284.2, "212": 286.5, "213": 288.8, "214": 291.1, "215": 293.4, "216": 295.8, "217": 298.1, "218": 300.4, "219": 302.8, "220": 305.2, "221": 307.5, "222": 309.9, "223": 312.3, "224": 314.7, "225": 317.1, "226": 319.4, "227": 321.9, "228": 324.3, "229": 326.7, "230": 329.1, "231": 331.5, "232": 334.0, "233": 336.4, "234": 338.9, "235": 341.3, "236": 343.8, "237": 346.3, "238": 348.7, "239": 351.2, "240": 353.7, "241": 356.2, "242": 358.7, "243": 361.2, "244": 363.7, "245": 366.3, "246": 368.8, "247": 371.3, "248": 373.9, "249": 376.4, "250": 379.0, "251": 381.5, "252": 384.1, "253": 386.6, "254": 389.2, "255": 391.8, "256": 394.4, "257": 397.0, "258": 399.6, "259": 402.2, "260": 404.8, "261": 407.4, "262": 410.0, "263": 412.7, "264": 415.3, "265": 417.9, "266": 420.6, "267": 423.2, "268": 425.9, "269": 428.6, "270": 431.2, "271": 433.9, "272": 436.6, "273": 439.3, "274": 442.0, "275": 444.7, "276": 447.4, "277": 450.1, "278": 452.8, "279": 455.5, "280": 458.3, "281": 461.0, "282": 463.7, "283": 466.5, "284": 469.2, "285": 472.0, "286": 474.7, "287": 477.5, "288": 480.3, "289": 483.0, "290": 485.8, "291": 488.6, "292": 491.4, "293": 494.2, "294": 497.0, "295": 499.8, "296": 502.6, "297": 505.4, "298": 508.3, "299": 511.1, "300": 513.9, "301": 516.8, "302": 519.6, "303": 522.5, "304": 525.3, "305": 528.2, "306": 531.1, "307": 533.9, "308": 536.8, "309": 539.7, "310": 542.6, "311": 545.5, "312": 548.4, "313": 551.3, "314": 554.2, "315": 557.1, "316": 560.0, "317": 562.9, "318": 565.9, "319": 568.8, "320": 571.7, "321": 574.7, "322": 577.6, "323": 580.6, "324": 583.5, "325": 586.5, "326": 589.5, "327": 592.4, "328": 595.4, "329": 598.4, "330": 601.4}

0 comments on commit b7a9086

Please sign in to comment.