""" Review on April 2022 by VR Accepted on 10th of April 2022 by DAD @author: DAD Comment: Les variables i, s, S ne sont pas utilisé dans la fonction validate_numeric Les variables d, i, s, S, V ne sont pas utilisé dans la fonction validate_prefix A améliorer: si deux beamsets ont le même numéro (traité en même temps) le suffixe T n'apparaitra que sur celui qui a des rotations de tables le beamset sans rotation de table n'aura aucun suffixe, pas même T0. """ import sys, os, getpass sys.path.append("//sih-fs/service/rtheonco/Physique/Scripting/Sources/stluc_module_importable/") import rxtheonco_tkinter as tk from collections import OrderedDict import tkinter from tkinter import messagebox, IntVar, BooleanVar import tkinter.font as tkfont from operator import getitem,itemgetter def newest(): path = r'C:\Program Files\RaySearch Laboratories' files = os.listdir(path) paths = [os.path.join(path, basename) for basename in files if not 'Launcher' in basename] return max(paths, key=os.path.getctime) try: pid_file_path = os.path.join(os.environ.get('userprofile'), 'AppData', 'Local', 'Temp', 'raystation.pid') with open(pid_file_path) as f: os.environ['RAYSTATION_PID'] = f.read() script_client_path = os.path.join(newest(), 'ScriptClient') sys.path.append(script_client_path) except: print("Run from RayStation") from connect import * from connect import * beam_set = get_current("BeamSet") machine_db = get_current('MachineDB') str_of_beams = '' def drr_annotation(lst_beam_set): str_of_beams = '' for DRR_settings in beam_set.DrrSettings: for DRR in DRR_settings.DrrSettingsPerBeam: description = DRR.ForBeam.Description str_of_beams += DRR.ForBeam.Name + '\n' DRR.Description = description def convertvarian(angle): if angle > 180: retangle = 540 - angle else: retangle = 180 - angle return retangle def renamenumberbeams(prefix, start, beam_set_lst): # Fonction qui ordonne les faisceaux dans le sens CCW et les renommes # Remplace prefix et beam_set par une liste de préfixes et une liste de beamsets # print 'start ', beam_set.DicomPlanLabel print('prefix =', prefix) b_count = 0 dico = {} for bs, pre in zip(beam_set_lst, prefix): for beam in bs.Beams: # If 2 names are identical, the system will overide the previous one # Might use the beam name+"_"+beamset name and after that use a split('_')[0] to get the beam information, or include the beam name in the dictionnary dico_name = beam.Name + '_' + bs.DicomPlanLabel dico[dico_name] = [bs, pre, beam.Name] b_count += 1 i = 0 foundit = 0 totbms = 0 if b_count > 0: gantries = [] collimators = [] tables = [] energies = [] rptgantry = 0 difcol = 0 diftbl = 0 difen = 0 sortgantries = [] beamnumbers = [] beam_set = [] gantryangle = 0 colangle = 0 tableangle = 0 area = 0 # Itère sur l'ensemble des faisceaux du beamset k = 0 for i in dico: # Remplis les listes d'angle de gantry, de collimateur et de table if dico[i][0].Beams[dico[i][2]].Wedge: wedge = 1 else: wedge = 0 if dico[i][0].Beams[dico[i][2]].DeliveryTechnique in ['SMLC', 'DMLC']: machineModel = machine_db.GetTreatmentMachine(machineName=dico[i][0].MachineReference.MachineName, lockMode=None) nb_leaf = machineModel.Physics.MlcPhysics.UpperLayer.NumOfLeafPairs lfCenPos = machineModel.Physics.MlcPhysics.UpperLayer.LeafCenterPositions lfWidths = machineModel.Physics.MlcPhysics.UpperLayer.LeafWidths for seg in dico[i][0].Beams[dico[i][2]].Segments: area = 0 for lf in range(int((nb_leaf / 2) - 1 + int(seg.JawPositions[2])), int((nb_leaf / 2) - 1 + int(seg.JawPositions[3]))): area += abs(seg.LeafPositions[1][lf] - seg.LeafPositions[0][lf]) * lfWidths[lf] else: area = 1 gantryangle = round(dico[i][0].Beams[dico[i][2]].GantryAngle, 1) print('gantry =', gantryangle) colangle = round(dico[i][0].Beams[dico[i][2]].InitialCollimatorAngle, 1) print('colli =', colangle) tableangle = round(dico[i][0].Beams[dico[i][2]].CouchRotationAngle, 1) print('table =', tableangle) if k != 0: if gantryangle in gantries: rptgantry = 1 if not colangle in collimators: difcol = 1 if not tableangle in tables: diftbl = 1 if not dico[i][0].Beams[dico[i][2]].MachineReference.Energy in energies: difen = 1 k += 1 gantries.append(gantryangle) collimators.append(colangle) tables.append(tableangle) energies.append(dico[i][0].Beams[dico[i][2]].MachineReference.Energy) beamnumbers.append(dico[i][0].Beams[dico[i][2]].Number) beam_set.append(dico[i][0]) # Trie les faisceaux dans le sens CCW if dico[i][0].Beams[dico[i][2]].GantryAngle > 180: sortgantries.append( [dico[i][0].Beams[dico[i][2]], round(540 - dico[i][0].Beams[dico[i][2]].GantryAngle, 1), round(dico[i][0].Beams[dico[i][2]].CouchRotationAngle, 1), gantryangle, colangle, tableangle, dico[i][0].Beams[dico[i][2]].MachineReference.Energy, dico[i][0], dico[i][1], 1 / area, wedge]) else: sortgantries.append( [dico[i][0].Beams[dico[i][2]], round(180 - dico[i][0].Beams[dico[i][2]].GantryAngle, 1), round(dico[i][0].Beams[dico[i][2]].CouchRotationAngle, 1), gantryangle, colangle, tableangle, dico[i][0].Beams[dico[i][2]].MachineReference.Energy, dico[i][0], dico[i][1], 1 / area, wedge]) if diftbl == 0 and not ((0 in tables) or (180 in tables)): diftbl = 1 # sort: couch angle, gantry angle (CCW), wedge, field area, collimator angle, beam energy sortgantries = sorted(sortgantries, key=itemgetter(2, 1, 10, 9, 4, 6)) # sorted(sortgantries, key = operator.itemgetter(9), reverse = True) print('sortgantries =', sortgantries) j = 1 # Renome les faisceaux pour éviter de futur conflit de nom for row in sortgantries: i = row[0] i.Number = 10 + totbms + max(beamnumbers) + j i.Description = 'z' + str(start) + str(max(beamnumbers)) + str(j) i.Name = 'z' + str(start) + str(max(beamnumbers)) + str(j) j += 1 j = 0 # Renomme les faisceaux en utilisant "G" + angle gantry, "T" + angle table, "B" pour bolus, "arc" pour VMAT, "E" pour électrons, "CC/CW" pour le sens de rotation si VMAT for row in sortgantries: i = row[0] i.Number = start + j beamdesc = '' end = '' if diftbl == 1: end = 'T' + str(row[5])[:str(row[5]).find('.')] + end for k in i.Boli: if k.Name: end += "B" if row[7].Modality == 'Electron': beamdesc = str(row[8]) + str(j + start) + "E" + str(row[3])[:str(row[3]).find('.')] + 'G' + end i.Description = str(j + start) i.Name = beamdesc if i.DeliveryTechnique == 'DynamicArc': if i.ArcRotationDirection == 'CounterClockwise': end += 'CC' else: end += 'CW' beamdesc = str(row[8]) + str(j + start) + 'G' + str(row[3])[:str(row[3]).find('.')] + end i.Description = str(j + start) i.Name = beamdesc else: beamdesc = str(row[8]) + str(j + start) + 'G' + str(row[3])[:str(row[3]).find('.')] + end i.Description = str(j + start) i.Name = beamdesc j += 1 # la fonction renvoi le nombre de faisceau renommé drr_annotation(beam_set_lst) return (int(b_count)) class NameAndNumberBeams(tkinter.Frame): def __init__(self, parent,is_bh_from_external): super(NameAndNumberBeams, self).__init__() self.plan = get_current("Plan") # définition des données de base self.beamset_checkbox_list = None self.label_result = None self.go = None self.beamset_order_label = None self.beamset_prefix_entry = None self.beamset_prefix_label = None self.beamset_checkbox = None self.bh_checkbox = None self.start_nb_entry = None self.start_nb_label = None self.beamset_order_entry = None self.parent = parent self.parent.title('RayStation Beam Numbering') self.parent.minsize(500, 100) self.is_bh_from_external=is_bh_from_external self.start_nb_entry_value = IntVar() self.checkbox_bh_value = IntVar() # Création de dictionnaire permettant d'y intégrer un nombre inconnu de variable (pour les checkbox) self.beamset_checkbox_value = {} # Création de dictionnaire permettant d'y intégrer un nombre inconnu de widget (pour les entry) self.beamset_prefix_list = {} self.beamset_order_list = {} self.text_entry = {} self.update() self.init_ui() self.is_bh_from_external = False if self.is_bh_from_external: self.set_prefix_to_bh() self.start_nb_entry_value.set(1) self.update() def set_prefix_to_bh(self): self.bh_checkbox.select() self.prefixbh() def validate_numeric(self, d, i, P, s, S, v, V, W): # récupération de l'objet widget appelant widget = self.nametowidget(W) # si la raison du callback(V) est le clavier (key) et que l'action utilisateur (d) est une tentation d'insertion if V == 'key' and d == '1': if not P.isnumeric(): messagebox.showerror(title="Erreur d'encodage", message="Veuillez entrer un nombre") # j'efface la valeur du widget widget.delete(0, "end") # j'insère la valeur 1 widget.insert(0, 1) else: # sinon je remplace par la valeur d'entré widget.delete(0, "end") widget.insert(0, P) # si la raison du callback est un focus out et que le texte est vide. Pour éviter d'avoir une erreur # lorsqu'on sélectionne le text et qu'on le remplace (on passe alors par une valeur vide elif V == 'focusout' and P == '': widget.insert(0, 1) else: widget.delete(0, "end") widget.insert(0, P) # Je réative la configuration de validation widget.after_idle(lambda W, v: self.nametowidget(W).configure(validate=v), W, v) def prefixbh(self): if self.checkbox_bh_value.get() == 1: for beamset in self.plan.BeamSets: self.text_entry[beamset.DicomPlanLabel].set("BH") def validate_prefix(self, d, i, P, s, S, v, V, W): widget = self.nametowidget(W) if len(P) > 3: messagebox.showerror(message="Le prefix doit contenir au maximum 3 caractères", title="Erreur d'encodage") widget.delete(0, "end") # je réécrit la dernière valeur sans le nouveau caractère widget.insert(0, P[:-1]) else: widget.delete(0, "end") widget.insert(0, P) widget.after_idle(lambda W, v: self.nametowidget(W).configure(validate=v), W, v) def init_ui(self): bg_frame_color = tk.lighten(tk.bg_frame, 0.3) vcmd_numeric = (self.register(self.validate_numeric), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') vcmd_prefix = (self.register(self.validate_prefix), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') start_nb_frame = tkinter.Frame(self.parent) # je précise que la fenêtre start_nb_frame se place en position haute, prend toute la largeur de la fenêtre avec # un paddinf en y de 10 start_nb_frame.pack(side="top", fill="x", pady=10) subframe = tkinter.Frame(start_nb_frame) subframe.pack(side="top", fill="x", pady=10) # Création d'un template pour la police qui sera utilisé tout le temp font_template = tkfont.Font(family=tk.font_family, size=10, weight="normal", slant="roman") # création d'un label dans la fenêtre "start_nb_frame" dont le text est positionné à gauche (justify) self.start_nb_label = tkinter.Label(subframe, text="Start Nb:", justify="left",fg=tk.fg_master) # configuration du label: font prend les caractéristiques de mon modème self.start_nb_label.configure(font=font_template) # la fonction pack permet d'insérer mon label en spécifiant ici à gauche avec un padding sur l'axe x de 10 self.start_nb_label.pack(side="left", padx=10) # afin de pouvoir récupérer les données, il faut définir ici la variable de l'entrée (textvariable), # c'est donc cette variable qu'il faudra lire plus tard pour récupérer une info. # Je fixe la taille du widget à 3 self.start_nb_entry = tkinter.Entry(subframe, validate='key', validatecommand=vcmd_numeric, width=3, textvariable=self.start_nb_entry_value,fg=tk.fg_master,background = bg_frame_color) self.start_nb_entry.pack(side="left") self.bh_checkbox = tkinter.Checkbutton(subframe, font=font_template, text="BH", variable=self.checkbox_bh_value,command=self.prefixbh,fg=tk.fg_master,selectcolor=bg_frame_color) self.bh_checkbox.pack(side="left", padx=50) compteur = 0 beamsets_frame = tkinter.Frame(start_nb_frame) beamsets_frame.pack(side="top") for beamset in self.plan.BeamSets: compteur += 1 # Je défini une nouvelle fenêtre permettant ainsi de rassembler toutes les infos de chaque beamset # sur une ligne beamset_frame = tkinter.Frame(beamsets_frame) beamset_frame.pack(pady=10) # Définition de la variable pour la checkbox dans la boucle afin de rendre chaque checkbox indépendante # On l'ajoute dans le dictionnaire avec comme clé le nom du beamset self.beamset_checkbox_value[beamset.DicomPlanLabel] = BooleanVar(value=True) # ajout des widget dans la fenêtre "beamset_frame". La fonction 'anchor="w"' permet de pousser la # checkbox à l'ouest et ainsi alligner tous les beamset, # la variable permet de récupérer l'état de la checkbox (False = décoché et True = coché) self.beamset_checkbox_list = tkinter.Checkbutton(beamset_frame, text=beamset.DicomPlanLabel, variable=self.beamset_checkbox_value[ beamset.DicomPlanLabel], font=font_template, width=40, anchor="w",selectcolor=bg_frame_color,fg=tk.fg_master) # je positionne à chaque fois à la gauche afin que chaque widget dans la frame en cours ce place côte à côte self.beamset_checkbox_list.pack(side='left',padx=(10,0)) self.beamset_checkbox_list.select() self.beamset_prefix_label = tkinter.Label(beamset_frame, font=font_template, text="Prefix:", justify="left",fg=tk.fg_master) self.beamset_prefix_label.pack(side='left') # les entrées sont placées dans un dictionnaire afin de pouvoir en récupérer l'info ultérieurement self.text_entry[beamset.DicomPlanLabel]=tkinter.StringVar() self.beamset_prefix_list[beamset.DicomPlanLabel] = tkinter.Entry(beamset_frame, width=5, validate='all', textvariable=self.text_entry[beamset.DicomPlanLabel], validatecommand=vcmd_prefix,fg=tk.fg_master) self.beamset_prefix_list[beamset.DicomPlanLabel].configure(bg=bg_frame_color) self.beamset_prefix_list[beamset.DicomPlanLabel].pack(side="left") self.beamset_order_label = tkinter.Label(beamset_frame, font=font_template, text="Order:", justify="right",fg=tk.fg_master) # J'ajoute du padding sur l'axe x que à gauche afin de m'éloigné du widget précédent mais rien à droite # afin d'être acollé à l'entrée correspondante self.beamset_order_label.pack(side='left', padx=(20, 0)) self.beamset_order_list[beamset.DicomPlanLabel] = tkinter.Entry(beamset_frame, width=3,fg=tk.fg_master) self.beamset_order_list[beamset.DicomPlanLabel].configure(bg=bg_frame_color) self.beamset_order_list[beamset.DicomPlanLabel].pack(side="left",padx=(0,10)) # je commence par effacer l'entrée parce que par défaut, avec la variable IntVar, j'ai 2 caractères self.beamset_order_list[beamset.DicomPlanLabel].delete(0, "end") # maintenant que je n'ai plus de caractère, je peux insérer en première position la valeur de mon compteur. self.beamset_order_list[beamset.DicomPlanLabel].insert(0, compteur) # Je crée mon dernier bouton "GO!" et le position en bas de page. Ce bouton prend toute la place en x, # pour ça je dois lui permettre de s'étendre self.go = tkinter.Button(master=start_nb_frame,text="GO!", font=font_template, command=self.fonction_go,fg=tk.fg_master) self.go.pack( fill='x', side="top", expand=True) # fonction associée au bouton "Go" qui utilise les données entrée dans le GUI def fonction_go(self): if self.checkbox_bh_value.get() == 1: count_bh = 1 else: count_bh = 0 # prévien l'utilisateur si le préfixe contient plus de 3 caractères. dico = {} i = 0 for beamset in self.plan.BeamSets: if self.beamset_checkbox_value[beamset.DicomPlanLabel].get() == 1: if str(self.beamset_order_list[beamset.DicomPlanLabel].get()) == '': order_to_add = 0 else: order_to_add = self.beamset_order_list[beamset.DicomPlanLabel].get() if not order_to_add in dico: dico[order_to_add] = {'BeamSet': [], 'Prefix': []} #dico[order_to_add] = {'BeamSet': self.plan.BeamSets[i], 'Prefix': str(self.beamset_prefix_list[beamset. # DicomPlanLabel].get()), # 'Order': str(self.beamset_order_list[beamset.DicomPlanLabel].get())} dico[order_to_add]['BeamSet'].append(self.plan.BeamSets[i]) dico[order_to_add]['Prefix'].append(str(self.beamset_prefix_list[beamset.DicomPlanLabel].get())) i += 1 # OrderedDict permet de garder un dictionnaire en sortie # je fais un tris sur les élements d'où le .items() et la clé permet de spécifier sur base de quel élément. order_dico = OrderedDict(sorted(dico.items())) beam_counter = 0 for i in order_dico: print('i in order_dico =', i) print('beam_counter =', beam_counter) beam_counter += renamenumberbeams(order_dico[i]['Prefix'], int(self.start_nb_entry_value.get()) + int(beam_counter), order_dico[i]['BeamSet']) sys.exit() def main(is_bh_from_external=None): root = tkinter.Tk() root.config(bg=tk.bg_frame) # la couleur de base du background est aussi fixé à celle de la fenêtre root.option_add("*background", tk.bg_frame) NameAndNumberBeams(root, is_bh_from_external) root.mainloop() if __name__ == "__main__": main()