From 2353e6e1a260a0360885064ae82930c4ffc90526 Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Sun, 4 Jan 2026 10:59:08 -0800 Subject: [PATCH 1/8] delete static method --- pytheranostics/dosimetry/organ_s_dosimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 0f460d5..46424ab 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -54,7 +54,7 @@ def check_mandatory_fields_organ(self) -> None: return None - @staticmethod + # @staticmethod def _load_human_mass_target_organs_table(self) -> pandas.DataFrame: """Load the reference human phantom masses.""" with resource_path( From 8e38ce5cb6df381b059e7748d2e325bdacf2a66c Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Sun, 4 Jan 2026 11:00:44 -0800 Subject: [PATCH 2/8] add organ mass and scaling bar to the report --- pytheranostics/misc_tools/report_generator.py | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/pytheranostics/misc_tools/report_generator.py b/pytheranostics/misc_tools/report_generator.py index 4c8bcf6..2c50e24 100644 --- a/pytheranostics/misc_tools/report_generator.py +++ b/pytheranostics/misc_tools/report_generator.py @@ -57,7 +57,9 @@ def signature_block(person, styles, width=2.5 * inch, height=0.6 * inch): return block -def create_dosimetry_pdf(json_file, output_file, calculated_by=None, approved_by=None): +def create_dosimetry_pdf( + image_bar, json_file, output_file, calculated_by=None, approved_by=None +): """Generate a dosimetry report PDF from patient JSON data. Parameters @@ -138,6 +140,25 @@ def create_dosimetry_pdf(json_file, output_file, calculated_by=None, approved_by img.drawHeight = img.imageHeight * scale mip_images.append(img) + # Add colorbar as the last "image" in the same row + colorbar_path = calling_folder / "TestDoseDB/bar.png" + + if colorbar_path.exists(): + bar_img = Image(str(colorbar_path)) + + # Make the bar much narrower (thin horizontal line) + bar_max_width = 2.7 * inch + bar_max_height = 2.4 * inch + + bar_scale = min( + bar_max_width / bar_img.imageWidth, bar_max_height / bar_img.imageHeight + ) + + bar_img.drawWidth = bar_img.imageWidth * bar_scale + bar_img.drawHeight = bar_img.imageHeight * bar_scale + + mip_images.append(bar_img) # <-- add as last image + # Put all images in one row using a Table if mip_images: mip_table = Table([mip_images]) # single row @@ -156,7 +177,8 @@ def create_dosimetry_pdf(json_file, output_file, calculated_by=None, approved_by elements.append(Spacer(1, 0.2 * inch)) caption = Paragraph( "Figure 1: Maximum Intensity Projection images of the patient across cycles. " - "The regions show the segmented organs at risk including the kidneys and the salivary glands. ", + "The regions show the segmented organs at risk including the kidneys and the salivary glands. " + f"The maximum value threshold set in all images at {image_bar/1000} kBq/ml. ", styles["Normal"], ) elements.append(caption) @@ -247,9 +269,9 @@ def create_dosimetry_pdf(json_file, output_file, calculated_by=None, approved_by elements.append(cumulative_table) # Paths to your three images image_paths = [ - calling_folder / "TestDoseDB/Gy_cummulative.png", calling_folder / "TestDoseDB/Gy_per_cycle.png", calling_folder / "TestDoseDB/Gy_per_GBq_per_cycle.png", + calling_folder / "TestDoseDB/Gy_cumulative.png", ] # Load and scale images @@ -280,7 +302,7 @@ def create_dosimetry_pdf(json_file, output_file, calculated_by=None, approved_by # Add caption elements.append(Spacer(1, 0.2 * inch)) caption = Paragraph( - "Figure 2: Cumulative AD, AD per cycle, and AD per GBq per cycle for target organs.", + "Figure 2: Absorbed dose per cycle reported in units of Gy and Gy/GBq, and cumulative absorbed dose in Gy for target organs and total tumor burden (TTB).", styles["Normal"], ) elements.append(caption) @@ -476,7 +498,7 @@ def cycle_info(cycle_n, elements, styles, data): elements.append(fig_title) organ_data_Gy_GBq = [ - ["Organ", "TIA (h)", "AD (Gy/GBq)", "AD (Gy)", "BED (Gy)"], + ["Organ", "TIA (h)", "Mass (g)", "AD (Gy/GBq)", "AD (Gy)", "BED (Gy)"], [ "Kidneys", round( @@ -486,6 +508,13 @@ def cycle_info(cycle_n, elements, styles, data): ), 2, ), + round( + ( + therapy_info[0]["VOIs"]["Kidney_Left"]["volumes_mL"]["mean"] + + therapy_info[0]["VOIs"]["Kidney_Right"]["volumes_mL"]["mean"] + ), + 2, + ), round(therapy_info[0]["Organ-level_AD"]["Kidneys"]["AD[Gy/GBq]"], 2), round(therapy_info[0]["Organ-level_AD"]["Kidneys"]["AD[Gy]"], 2), round(therapy_info[0]["Organ-level_AD"]["Kidneys"]["BED[Gy]"], 2), @@ -493,6 +522,7 @@ def cycle_info(cycle_n, elements, styles, data): [ "Red Marrow", round((therapy_info[0]["VOIs"]["BoneMarrow"]["TIA_h"]), 2), + round(therapy_info[0]["VOIs"]["BoneMarrow"]["volumes_mL"]["mean"], 2), round(therapy_info[0]["Organ-level_AD"]["Red Marrow"]["AD[Gy/GBq]"], 2), round(therapy_info[0]["Organ-level_AD"]["Red Marrow"]["AD[Gy]"], 2), "-", @@ -508,6 +538,21 @@ def cycle_info(cycle_n, elements, styles, data): ), 2, ), + round( + ( + therapy_info[0]["VOIs"]["ParotidGland_Left"]["volumes_mL"]["mean"] + + therapy_info[0]["VOIs"]["ParotidGland_Right"]["volumes_mL"][ + "mean" + ] + + therapy_info[0]["VOIs"]["SubmandibularGland_Left"]["volumes_mL"][ + "mean" + ] + + therapy_info[0]["VOIs"]["SubmandibularGland_Right"]["volumes_mL"][ + "mean" + ] + ), + 2, + ), round( therapy_info[0]["Organ-level_AD"]["Salivary Glands"]["AD[Gy/GBq]"], 2 ), @@ -515,7 +560,7 @@ def cycle_info(cycle_n, elements, styles, data): "-", ], ] - organ_table_Gy_GBq = Table(organ_data_Gy_GBq, colWidths=[1.5 * inch, 1.2 * inch]) + organ_table_Gy_GBq = Table(organ_data_Gy_GBq, colWidths=[1.5 * inch, 1.15 * inch]) organ_table_Gy_GBq.setStyle( TableStyle( [ From 59e99a15e88de534d4c942391cff046171cc139a Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Thu, 8 Jan 2026 15:18:28 -0800 Subject: [PATCH 3/8] adjust for cases with one kidney only --- pytheranostics/dosimetry/organ_s_dosimetry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 46424ab..86e3fed 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -180,7 +180,9 @@ def prepare_data(self) -> None: ].apply(lambda x: numpy.mean(x)) # Combine Kidneys. - kidneys = ["Kidney_Left", "Kidney_Right"] + kidneys = [ + s for s in self.results_fitting.index if s.startswith("Kidney_") + ] # e.g., Kidney_Left, Kidney_Right, or one kidney only self.results_fitting.loc["Kidneys"] = self.results_fitting.loc[ kidneys ].sum() From 1dbab5792a1c68a580a8097f377ba45caa3f020a Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Tue, 24 Mar 2026 15:48:56 -0700 Subject: [PATCH 4/8] initialize kinetic in nw lesions using mean kinetic from all lesions in cycle 1 --- pytheranostics/misc_tools/tools.py | 60 ++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/pytheranostics/misc_tools/tools.py b/pytheranostics/misc_tools/tools.py index 00ad17e..b582f3b 100644 --- a/pytheranostics/misc_tools/tools.py +++ b/pytheranostics/misc_tools/tools.py @@ -329,16 +329,61 @@ def initialize_biokinetics_from_prior_cycle( and roi_info["biokinectics_from_previous_cycle"] ): - # Get previous cycle parameters: - fixed_param, with_uptake, all_params, washout_ratio = ( - extract_exponential_params_from_json( - json_data=prior_treatment_data, cycle=cycle, region=roi + # --- original behavior --- + if roi in prior_treatment_data[cycle][0]["VOIs"]: + + fixed_param, with_uptake, all_params, washout_ratio = ( + extract_exponential_params_from_json( + json_data=prior_treatment_data, + cycle=cycle, + region=roi, + ) ) - ) + fit_order = len(all_params) // 2 + + # --- NEW fallback for new lesions --- + else: + fixed_params_list = [] + with_uptake = False + washout_ratio = None + + for voi_name, voi_data in prior_treatment_data[cycle][0][ + "VOIs" + ].items(): + if "Lesion" in voi_name and voi_data.get("fitting_eq") == 1: + + fixed_i, with_uptake_i, all_i, washout_ratio_i = ( + extract_exponential_params_from_json( + json_data=prior_treatment_data, + cycle=cycle, + region=voi_name, + ) + ) + + fixed_params_list.append(fixed_i) + with_uptake = with_uptake or with_uptake_i + washout_ratio = washout_ratio_i + + if len(fixed_params_list) == 0: + raise ValueError( + f"No lesions with fit_order == 1 found in {cycle} " + f"to initialize {roi}" + ) + + # mean of dictionaries (same keys guaranteed) + fixed_param = { + key: sum(d[key] for d in fixed_params_list) / len(fixed_params_list) + for key in fixed_params_list[0] + } + + fit_order = 1 + all_params = fixed_param + + # --- unchanged --- config["VOIs"][roi] = { "fixed_parameters": fixed_param, - "fit_order": len(all_params) // 2, + "fit_order": fit_order, "param_init": all_params, "with_uptake": with_uptake, "washout_ratio": washout_ratio, @@ -416,7 +461,6 @@ def plot_MIP_with_mask_outlines(ax, SPECT, masks=None, vmax=300000, label=None): if masks is not None: for organ, mask in masks.items(): organ_lower = organ.lower() - print(organ_lower) if "peak" in organ_lower: continue else: @@ -463,7 +507,7 @@ def plot_MIP_with_mask_outlines(ax, SPECT, masks=None, vmax=300000, label=None): alpha=0.7, ) - plt.xlim(30, 100) + plt.xlim(15, 105) plt.ylim(0, 234) plt.axis("off") plt.xticks([]) From 1b3485c667826857eca5e597ed4b074a3684d32b Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Tue, 24 Mar 2026 15:50:10 -0700 Subject: [PATCH 5/8] add more organs to the report --- pytheranostics/misc_tools/report_generator.py | 220 ++++++++++++++---- 1 file changed, 178 insertions(+), 42 deletions(-) diff --git a/pytheranostics/misc_tools/report_generator.py b/pytheranostics/misc_tools/report_generator.py index 2c50e24..ce78b7c 100644 --- a/pytheranostics/misc_tools/report_generator.py +++ b/pytheranostics/misc_tools/report_generator.py @@ -11,6 +11,7 @@ from reportlab.lib.units import inch from reportlab.platypus import ( Image, + KeepTogether, PageBreak, Paragraph, SimpleDocTemplate, @@ -58,7 +59,12 @@ def signature_block(person, styles, width=2.5 * inch, height=0.6 * inch): def create_dosimetry_pdf( - image_bar, json_file, output_file, calculated_by=None, approved_by=None + image_bar, + json_file, + output_file, + calculated_by=None, + approved_by=None, + comment=None, ): """Generate a dosimetry report PDF from patient JSON data. @@ -119,7 +125,11 @@ def create_dosimetry_pdf( elements.append(subject_table) elements.append(Spacer(1, 0.3 * inch)) - + if comment: + elements.append( + Paragraph(f'{comment}', styles["Normal"]) + ) + elements.append(Spacer(1, 0.3 * inch)) elements.append( Paragraph("Maximum Intensity Projection", styles["Heading3"]) ) @@ -203,10 +213,16 @@ def create_dosimetry_pdf( total_tia_kidneys = 0 total_tia_salivary = 0 total_tia_marrow = 0 + total_tia_liver = 0 + total_tia_spleen = 0 + total_tia_body = 0 total_ad_kidneys = 0 total_ad_salivary = 0 total_ad_marrow = 0 + total_ad_liver = 0 + total_ad_spleen = 0 + total_ad_body = 0 total_bed_kidneys = 0 @@ -214,10 +230,11 @@ def create_dosimetry_pdf( therapy_info = data.get(f"Cycle_0{i}", {})[0] # Kidneys - total_tia_kidneys += ( - therapy_info["VOIs"]["Kidney_Left"]["TIA_h"] - + therapy_info["VOIs"]["Kidney_Right"]["TIA_h"] - ) + kidney_labels = [k for k in therapy_info["VOIs"] if k.startswith("Kidney_")] + + for k in kidney_labels: + total_tia_kidneys += therapy_info["VOIs"][k]["TIA_h"] + total_ad_kidneys += therapy_info["Organ-level_AD"]["Kidneys"]["AD[Gy]"] total_bed_kidneys += therapy_info["Organ-level_AD"]["Kidneys"]["BED[Gy]"] @@ -234,6 +251,18 @@ def create_dosimetry_pdf( ) total_ad_salivary += therapy_info["Organ-level_AD"]["Salivary Glands"]["AD[Gy]"] + # Liver + total_tia_liver += therapy_info["VOIs"]["Liver"]["TIA_h"] + total_ad_liver += therapy_info["Organ-level_AD"]["Liver"]["AD[Gy]"] + + # Spleen + total_tia_spleen += therapy_info["VOIs"]["Spleen"]["TIA_h"] + total_ad_spleen += therapy_info["Organ-level_AD"]["Spleen"]["AD[Gy]"] + + # Body + total_tia_body += therapy_info["VOIs"]["WholeBody"]["TIA_h"] + total_ad_body += therapy_info["Organ-level_AD"]["Total Body"]["AD[Gy]"] + # Build the cumulative table cumulative_data = [ ["Organ", "Cumulative TIA (h)", "Cumulative AD (Gy)", "Cumulative BED (Gy)"], @@ -250,6 +279,9 @@ def create_dosimetry_pdf( round(total_ad_salivary, 2), "-", ], + ["Liver", round(total_tia_liver, 2), round(total_ad_liver, 2), "-"], + ["Spleen", round(total_tia_spleen, 2), round(total_ad_spleen, 2), "-"], + ["Total Body", round(total_tia_body, 2), round(total_ad_body, 2), "-"], ] cumulative_table = Table(cumulative_data, colWidths=[1.5 * inch, 1.7 * inch]) @@ -258,6 +290,7 @@ def create_dosimetry_pdf( [ ("ALIGN", (0, 0), (-1, -1), "LEFT"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), ("FONTSIZE", (0, 0), (-1, -1), 12), ("GRID", (0, 0), (-1, -1), 1, colors.black), @@ -267,31 +300,61 @@ def create_dosimetry_pdf( ) elements.append(cumulative_table) - # Paths to your three images - image_paths = [ - calling_folder / "TestDoseDB/Gy_per_cycle.png", + + # Paths + plot_paths = [ calling_folder / "TestDoseDB/Gy_per_GBq_per_cycle.png", calling_folder / "TestDoseDB/Gy_cumulative.png", ] + legend_path = calling_folder / "TestDoseDB/AD_legend.png" - # Load and scale images - imgs = [] - for path in image_paths: + # ---- Load plots (row 1) ---- + plots = [] + for path in plot_paths: img = Image(str(path)) - scale = min(max_width / img.imageWidth, max_height / img.imageHeight) / 3 + + scale = min( + (max_width * 0.45) / img.imageWidth, # half width + (max_height / 2) / img.imageHeight, + ) + img.drawWidth = img.imageWidth * scale img.drawHeight = img.imageHeight * scale - imgs.append(img) + plots.append(img) - # Create a table with 1 row and 3 columns - table = Table([imgs], colWidths=[max_width / 3] * 3) + # ---- Load legend (row 2, scaled to 70%) ---- + legend = Image(str(legend_path)) + + legend_scale = ( + min( + (max_width * 0.9) / legend.imageWidth, + (max_height / 4) / legend.imageHeight, + ) + * 0.5 + ) # 70% size + + legend.drawWidth = legend.imageWidth * legend_scale + legend.drawHeight = legend.imageHeight * legend_scale + + # ---- Build table data ---- + table_data = [ + [plots[0], plots[1]], # Row 1: two plots + [legend, ""], # Row 2: legend spanning 2 columns + ] + + table = Table( + table_data, + colWidths=[max_width * 0.45, max_width * 0.45], + ) - # Optional styling table.setStyle( TableStyle( [ + ("SPAN", (0, 1), (1, 1)), # merge legend row ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("TOPPADDING", (0, 0), (-1, -1), 6), + ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ] ) ) @@ -302,46 +365,71 @@ def create_dosimetry_pdf( # Add caption elements.append(Spacer(1, 0.2 * inch)) caption = Paragraph( - "Figure 2: Absorbed dose per cycle reported in units of Gy and Gy/GBq, and cumulative absorbed dose in Gy for target organs and total tumor burden (TTB).", + "Figure 2: Absorbed dose per cycle reported in units of Gy/GBq, and cumulative absorbed dose in Gy for target organs and total tumor burden (TTB).", styles["Normal"], ) elements.append(caption) elements.append(Spacer(1, 0.2 * inch)) + # new page + elements.append(PageBreak()) + elements.append(Paragraph("Laboratory Results Summary", styles["Heading3"])) + # Paths to trend plots trend_paths = [ calling_folder / "TestDoseDB/Hemoglobin_trend.png", calling_folder / "TestDoseDB/Platelets_trend.png", calling_folder / "TestDoseDB/eGFR_trend.png", calling_folder / "TestDoseDB/PSA_trend.png", + calling_folder / "TestDoseDB/CTCAE_legend.png", ] trend_imgs = [] - for path in trend_paths: + for i, path in enumerate(trend_paths): img = Image(str(path)) - # Scale to fit 2×2 layout - scale = min( - (max_width / 2.5) / img.imageWidth, (max_height / 2.5) / img.imageHeight - ) + + # scale plots and legend slightly differently + if i < 4: # regular plots + scale = min( + (max_width / 2.5) / img.imageWidth, + (max_height / 2.5) / img.imageHeight, + ) + else: # legend – wider, less tall + scale = min( + (max_width / 1.5) / img.imageWidth, + (max_height / 6) / img.imageHeight, + ) + img.drawWidth = img.imageWidth * scale img.drawHeight = img.imageHeight * scale trend_imgs.append(img) - # Arrange in 2×2 structure - trend_table_data = [[trend_imgs[0], trend_imgs[1]], [trend_imgs[2], trend_imgs[3]]] + # ---- Table layout ---- + trend_table_data = [ + [trend_imgs[0], trend_imgs[1]], + [trend_imgs[2], trend_imgs[3]], + [trend_imgs[4], ""], # legend row + ] - trend_table = Table(trend_table_data, colWidths=[max_width / 2.5, max_width / 2.5]) + trend_table = Table( + trend_table_data, + colWidths=[max_width / 2.5, max_width / 2.5], + ) trend_table.setStyle( TableStyle( [ ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("BOTTOMPADDING", (0, 0), (-1, -1), 12), + # merge legend row across both columns + ("SPAN", (0, 2), (1, 2)), + # spacing ("TOPPADDING", (0, 0), (-1, -1), 12), + ("BOTTOMPADDING", (0, 0), (-1, -1), 12), ] ) ) + elements.append(trend_table) # Add caption @@ -350,7 +438,7 @@ def create_dosimetry_pdf( styles["Normal"], ) elements.append(caption) - + elements.append(PageBreak()) # =============================== # Signatures Section # =============================== @@ -435,8 +523,17 @@ def biodistribution_per_cycle(cycle_n, elements, styles, data): # glob returns a list of matching files image_paths = glob.glob(str(pattern)) + image_paths = sorted(image_paths) for image_path in image_paths: + if "Lesion" in Path(image_path).name: + continue + if "Bladder" in Path(image_path).name: + continue + if "Skeleton" in Path(image_path).name: + continue + if "Remainder" in Path(image_path).name: + continue img = Image(image_path) # Compute scaling factor to fit inside page @@ -465,8 +562,8 @@ def cycle_info(cycle_n, elements, styles, data): Patient data dictionary. """ # Therapy Information Section - therapy_title = Paragraph(f"Cycle {cycle_n}", styles["Heading2"]) - elements.append(therapy_title) + # therapy_title = Paragraph(f"Cycle {cycle_n}", styles["Heading2"]) + # elements.append(therapy_title) # Therapy Information Table therapy_info = data.get(f"Cycle_0{cycle_n}", {}) @@ -489,29 +586,44 @@ def cycle_info(cycle_n, elements, styles, data): ) # Add to document - elements.append(therapy_info_para) - elements.append(Spacer(1, 0.089 * inch)) - - fig_title = Paragraph( - "Absorbed dose results for the organs at risk", styles["Heading3"] + # elements.append(therapy_info_para) + # elements.append(Spacer(1, 0.089 * inch)) + + # fig_title = Paragraph( + # "Absorbed dose results for the organs at risk", styles["Heading3"] + # ) + # elements.append(fig_title) + cycle_header_block = KeepTogether( + [ + Paragraph(f"Cycle {cycle_n}", styles["Heading2"]), + therapy_info_para, + Spacer(1, 0.089 * inch), + Paragraph( + "Absorbed dose results for the organs at risk", + styles["Heading3"], + ), + ] ) - elements.append(fig_title) + + elements.append(cycle_header_block) organ_data_Gy_GBq = [ ["Organ", "TIA (h)", "Mass (g)", "AD (Gy/GBq)", "AD (Gy)", "BED (Gy)"], [ "Kidneys", round( - ( - therapy_info[0]["VOIs"]["Kidney_Left"]["TIA_h"] - + therapy_info[0]["VOIs"]["Kidney_Right"]["TIA_h"] + sum( + therapy_info[0]["VOIs"][k]["TIA_h"] + for k in therapy_info[0]["VOIs"] + if k.startswith("Kidney_") ), 2, ), round( - ( - therapy_info[0]["VOIs"]["Kidney_Left"]["volumes_mL"]["mean"] - + therapy_info[0]["VOIs"]["Kidney_Right"]["volumes_mL"]["mean"] + sum( + therapy_info[0]["VOIs"][k]["volumes_mL"]["mean"] + for k in therapy_info[0]["VOIs"] + if k.startswith("Kidney_") ), 2, ), @@ -559,6 +671,30 @@ def cycle_info(cycle_n, elements, styles, data): round(therapy_info[0]["Organ-level_AD"]["Salivary Glands"]["AD[Gy]"], 2), "-", ], + [ + "Liver", + round((therapy_info[0]["VOIs"]["Liver"]["TIA_h"]), 2), + round(therapy_info[0]["VOIs"]["Liver"]["volumes_mL"]["mean"], 2), + round(therapy_info[0]["Organ-level_AD"]["Liver"]["AD[Gy/GBq]"], 2), + round(therapy_info[0]["Organ-level_AD"]["Liver"]["AD[Gy]"], 2), + "-", + ], + [ + "Spleen", + round((therapy_info[0]["VOIs"]["Spleen"]["TIA_h"]), 2), + round(therapy_info[0]["VOIs"]["Spleen"]["volumes_mL"]["mean"], 2), + round(therapy_info[0]["Organ-level_AD"]["Spleen"]["AD[Gy/GBq]"], 2), + round(therapy_info[0]["Organ-level_AD"]["Spleen"]["AD[Gy]"], 2), + "-", + ], + [ + "Total Body", + round((therapy_info[0]["VOIs"]["WholeBody"]["TIA_h"]), 2), + round(therapy_info[0]["VOIs"]["WholeBody"]["volumes_mL"]["mean"], 2), + round(therapy_info[0]["Organ-level_AD"]["Total Body"]["AD[Gy/GBq]"], 2), + round(therapy_info[0]["Organ-level_AD"]["Total Body"]["AD[Gy]"], 2), + "-", + ], ] organ_table_Gy_GBq = Table(organ_data_Gy_GBq, colWidths=[1.5 * inch, 1.15 * inch]) organ_table_Gy_GBq.setStyle( From deedbc65b30ba3c0fd188fa39d515f3b9a97859f Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Tue, 24 Mar 2026 15:50:53 -0700 Subject: [PATCH 6/8] add comment on BM method --- pytheranostics/dosimetry/organ_s_dosimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 86e3fed..5223910 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -233,7 +233,7 @@ def prepare_data(self) -> None: self.results_fitting.loc["Red Marrow"][ "Volume_CT_mL" - ] = 1170 # TODO volume hardcoded, think about alternatives + ] = 1170 # TODO volume hardcoded, think about alternatives #this one works for blood based method only; for imaging method it should be scaled # EANM Dosimetry Committee guidelines for bone marro and whole-body dosimetry self.results_fitting.loc["RemainderOfBody"]["Volume_CT_mL"] = ( self.config["PatientWeight_g"] From ef6da55a0fb7ee9dea38e3dd2a4f5c22783a579b Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Tue, 24 Mar 2026 15:51:18 -0700 Subject: [PATCH 7/8] adjust code for ge --- pytheranostics/dicomtools/dicomtools.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytheranostics/dicomtools/dicomtools.py b/pytheranostics/dicomtools/dicomtools.py index cf837d8..209d5c9 100644 --- a/pytheranostics/dicomtools/dicomtools.py +++ b/pytheranostics/dicomtools/dicomtools.py @@ -46,16 +46,22 @@ def make_bqml_suv( if "siemens" in self.ds.Manufacturer.lower(): self.ds.SeriesTime = self.ds.AcquisitionTime self.ds.ContentTime = self.ds.AcquisitionTime + elif "ge" in self.ds.Manufacturer.lower(): # i think it applies to ge as well + self.ds.SeriesTime = self.ds.AcquisitionTime + self.ds.ContentTime = self.ds.AcquisitionTime # Get the frame duration in seconds frame_duration = ( self.ds.RotationInformationSequence[0].ActualFrameDuration / 1000 ) # get number of projections because manufacturers scale by this in the dicomfile - n_proj = ( - self.ds.RotationInformationSequence[0].NumberOfFramesInRotation - * n_detectors - ) + if "siemens" in self.ds.Manufacturer.lower(): + n_proj = ( + self.ds.RotationInformationSequence[0].NumberOfFramesInRotation + * n_detectors + ) + elif "ge" in self.ds.Manufacturer.lower(): + n_proj = self.ds.RotationInformationSequence[0].NumberOfFramesInRotation # get voxel volume in ml vox_vol = np.append( np.asarray(self.ds.PixelSpacing), float(self.ds.SliceThickness) From f3d3c551ed8423e37d4cf2ff2ff62379fa5a554b Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Tue, 24 Mar 2026 16:22:22 -0700 Subject: [PATCH 8/8] gather existing kidneys dynamically --- pytheranostics/dosimetry/base_dosimetry.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pytheranostics/dosimetry/base_dosimetry.py b/pytheranostics/dosimetry/base_dosimetry.py index 8af3cd5..4e367d3 100644 --- a/pytheranostics/dosimetry/base_dosimetry.py +++ b/pytheranostics/dosimetry/base_dosimetry.py @@ -645,14 +645,20 @@ def calculate_bed(self, kinetic: str) -> None: ) # Gy if kinetic == "monoexp": - t_eff = numpy.log(2) / ( - ( - self.results.loc["Kidney_Left"]["Fit_params"][1] - + self.results.loc["Kidney_Right"]["Fit_params"][1] - ) - / 2 - ) + # gather existing kidneys dynamically + kidney_labels = [ + s for s in self.results.index if s.startswith("Kidney_") + ] + + # extract alpha parameters for those that exist + alphas = [self.results.loc[k]["Fit_params"][1] for k in kidney_labels] + + # compute effective half-time using the mean alpha + alpha_mean = numpy.mean(alphas) + t_eff = numpy.log(2) / alpha_mean + bed[organ] = AD + 1 / alpha_beta * t_repair / (t_repair + t_eff) * AD**2 + elif kinetic == "biexp": mean_lambda_washout = ( self.results.loc["Kidney_Left"]["Fit_params"][1]