From 8608d41eca69dc9508f8ae9a78e791ac2c9279f6 Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Wed, 5 Nov 2025 11:25:50 -0800 Subject: [PATCH 1/5] add plot_MIP_with_mask_outlines --- pytheranostics/misc_tools/tools.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/pytheranostics/misc_tools/tools.py b/pytheranostics/misc_tools/tools.py index 44b447a..c355428 100644 --- a/pytheranostics/misc_tools/tools.py +++ b/pytheranostics/misc_tools/tools.py @@ -389,3 +389,52 @@ def plot_MIP(SPECT=None, vmax=300000, figsize=(10, 5), ax=None): plt.yticks([]) return fig, ax + + +def plot_MIP_with_mask_outlines(ax, SPECT, masks=None, vmax=300000): + """Plot Maximum Intensity Projection (MIP) of SPECT data with masks outlines. + + Parameters + ---------- + ax : _type_ + _description_ + SPECT : _type_ + _description_ + masks : _type_, optional + _description_, by default None + vmax : int, optional + _description_, by default 300000 + """ + plt.sca(ax) + spect_mip = SPECT.max(axis=0) + plt.imshow(spect_mip.T, cmap="Greys", interpolation="Gaussian", vmax=vmax, vmin=0) + + if masks is not None: + for organ, mask in masks.items(): + organ_lower = organ.lower() + if "kidney" in organ_lower: + color = "lime" + elif "gland" in organ_lower: + color = "red" + elif "lesion" in organ_lower: + color = "m" + else: + continue + + mip_mask = mask.max(axis=0) + if mip_mask.shape != spect_mip.shape: + mip_mask = mip_mask.T + + plt.contour( + numpy.transpose(mip_mask, (1, 0)), + levels=[0.5], + colors=[color], + linewidths=1.5, + alpha=0.5, + ) + + plt.xlim(30, 100) + plt.ylim(0, 234) + plt.axis("off") + plt.xticks([]) + plt.yticks([]) From 1aaa6d4e35e0d37cdde31218e45f50b6255d9ccf Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Wed, 5 Nov 2025 11:32:52 -0800 Subject: [PATCH 2/5] improve formatting of variables --- pytheranostics/dosimetry/organ_s_dosimetry.py | 130 +++++++++++------- 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 0670872..4088f7c 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -117,22 +117,27 @@ def apply_sphere_method(self, df: pandas.DataFrame) -> pandas.DataFrame: def calculate_ttb(self): """Compute Total Tumor Burden (TTB) metrics and append to results_lesions.""" metrics = { - "Mass_g": self.results_lesions["Mass_g"].sum(), - "Volume_CT_mL": self.results_lesions["Volume_CT_mL"].sum(), - "TIA_h": self.results_lesions["TIA_h"].sum(), + "Mass_g": self.results_dosimetry_lesions["Mass_g"].sum(), + "Volume_CT_mL": self.results_dosimetry_lesions["Volume_CT_mL"].sum(), + "TIA_h": self.results_dosimetry_lesions["TIA_h"].sum(), "AD_Gy": ( - (self.results_lesions["Mass_g"] * self.results_lesions["AD_Gy"]).sum() + ( + self.results_dosimetry_lesions["Mass_g"] + * self.results_dosimetry_lesions["AD_Gy"] + ).sum() ) / ( - self.results_lesions["Mass_g"].sum() - if self.results_lesions["Mass_g"].sum() > 0 + self.results_dosimetry_lesions["Mass_g"].sum() + if self.results_dosimetry_lesions["Mass_g"].sum() > 0 else 0 ), } TTB = pandas.DataFrame(metrics, index=["TTB"]) - self.results_lesions = pandas.concat([self.results_lesions, TTB], axis=0) - return self.results_lesions + self.results_dosimetry_lesions = pandas.concat( + [self.results_dosimetry_lesions, TTB], axis=0 + ) + return self.results_dosimetry_lesions def prepare_data(self) -> None: """ @@ -223,6 +228,7 @@ def prepare_data(self) -> None: self.results_fitting.loc["Red Marrow"][ "Volume_CT_mL" ] = 1170 # TODO volume hardcoded, think about alternatives + self.results_fitting.loc["RemainderOfBody"]["Volume_CT_mL"] = ( self.config["PatientWeight_g"] - self.results_fitting.loc[ @@ -241,9 +247,9 @@ def prepare_data(self) -> None: self.results_fitting_organs = self.results_fitting[ ~lesion_mask ].copy() # all non-lesion entries - self.results_fitting_lesions = self.results_fitting[ - lesion_mask - ].copy() # only lesion entries + self.results_fitting_lesions = self.results[ + self.results.index.str.contains("Lesion") + ] if "TotalTumorBurden" in self.results_fitting.index: self.results_fitting.drop("TotalTumorBurden", axis=0, inplace=True) @@ -251,13 +257,13 @@ def prepare_data(self) -> None: if output_type == "Export": fmt = organ_conf["Output"]["ExportFormat"] if fmt.lower() == "olinda": - self.results_fitting = self.results_fitting.rename( + self.results_fitting_organs = self.results_fitting_organs.rename( index={"RemainderOfBody": "Total Body"} ) - self.results_fitting.loc["Total Body"]["Volume_CT_mL"] = ( + self.results_fitting_organs.loc["Total Body"]["Volume_CT_mL"] = ( self.config["PatientWeight_g"] - - self.results_fitting.loc[ - ~self.results_fitting.index.isin( + - self.results_fitting_organs.loc[ + ~self.results_fitting_organs.index.isin( ["Total Body", "RemainderOfBody"] ), "Volume_CT_mL", @@ -327,7 +333,7 @@ def process_dosimetry(self) -> None: "LesionDosimetry" ): self.results_dosimetry_lesions = self.apply_sphere_method( - self.results_fitting_lesions.index.str.contains("Lesion") + self.results_fitting_lesions ) self.results_dosimetry_lesions = self.calculate_ttb() if "Yes" in self.config["OrganLevel"]["AdditionalOptions"].get( @@ -342,12 +348,12 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: """Calculate absorbed dose per target organ based on model and disintegration data.""" model_files = { "Female": { - "beta": f'177Lu_S_values_female_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_BETA.csv', - "gamma": f'177Lu_S_values_female_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_GAMMA.csv', + "gamma": f'{self.config["Radionuclide"]}_S_values_female_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_GAMMA.csv', + "beta": f'{self.config["Radionuclide"]}_S_values_female_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_BETA.csv', }, "Male": { - "beta": f'177Lu_S_values_male_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_BETA.csv', - "gamma": f'177Lu_S_values_male_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_GAMMA.csv', + "gamma": f'{self.config["Radionuclide"]}_S_values_male_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_GAMMA.csv', + "beta": f'{self.config["Radionuclide"]}_S_values_male_{self.config["OrganLevel"]["Calculation"]["SValueSource"].lower()}_BETA.csv', }, } @@ -359,7 +365,7 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: ) print("Source organs available in the model:", svalues_beta.columns.tolist()) - print("Source organs present :", self.results_fitting.index.tolist()) + print("Source organs present :", self.results_fitting_organs.index.tolist()) self.source_organs_missing = set(svalues_beta.columns) - set( self.results_fitting.index @@ -391,7 +397,7 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: ) # Apply mass scaling - dose_df = self.perform_mass_scaling(dose_df, self.config["Gender"]) + dose_df = self.perform_mass_scaling(dose_df) # Calculate absorbed dose in Gy for injected activity dose_df["AD_total[Gy/GBq]"] = ( @@ -403,7 +409,10 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: dose_df["AD_total[Gy]"] = dose_df["AD_total[Gy/GBq]"] / 1000 * injected_activity dose_df = dose_df.reset_index(drop=True) - self.df_ad = dose_df.copy() + self.results_dosimetry_organs = dose_df.copy() + self.results_dosimetry_organs = self.results_dosimetry_organs.set_index( + "Target organ" + ) return dose_df def hollow_organ_correction(self, df: pandas.DataFrame) -> pandas.DataFrame: @@ -476,9 +485,12 @@ def redistribute_ROB_into_source_organs_missing( def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: """Multiply S-values by TIA to compute dose matrix for radiation type.""" # Path to organ masses - masses_path = path.join(MASSES_PATH, "ICRP_mass_male.csv") + masses_path = path.join( + MASSES_PATH, f"ICRP_mass_{self.config['Gender'].lower()}.csv" + ) self.organ_masses = pandas.read_csv(masses_path, index_col=0) + print(f"Applying S-values for {radiation_type} radiation...") # Handle remainder of the body # Redistribute ROB TIA into missing source organs if needed - approach consistent with MIRDcalc software if "RemainderOfBody" in tia_df.index: @@ -545,38 +557,47 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: dose_df = s_values_subset.multiply(tia_series, axis=1) # ROB + + print("Calculating Remainder of Body contributions...") dose_df["RemainderOfBody"] = 0 - total_body_mass = self.config["PatientWeight_g"] + total_body_mass = self.organ_masses.loc["Total Body", "Mass_g"] ROB_mass = tia_df.loc["RemainderOfBody", "Volume_CT_mL"] + if radiation_type == "beta": organs = s_values_subset.index.difference( tia_series.index.difference(["Red Marrow", "Osteogenic Cells"]) ) if radiation_type == "gamma": organs = s_values_subset.index - # adjust source organs so that they are different for gamma and beta radiation (in beta it is total - source organs + bone + skeleton) - + self.contribution_from_sources_dict = {radiation_type: {}} for target_organ in organs: contribution_from_sources = 0 + + self.contribution_from_sources_dict[radiation_type][ + target_organ + ] = {"sources": {}} + for source_organ in s_values.columns.difference(tia_series.index): + if radiation_type == "beta": + if target_organ == "Red Marrow" and source_organ in [ + "Trabecular Bone" + ]: + continue + if target_organ == "Osteogenic Cells" and source_organ in [ + "Cortical Bone", + "Trabecular Bone", + "Red Marrow", + ]: + continue - if target_organ.split()[0] == source_organ.split()[0]: - continue - if target_organ == "Osteogenic Cells" and source_organ in [ - "Cortical Bone", - "Trabecular Bone", - "Red Marrow", - ]: + if source_organ == "Total Body": continue - if target_organ == "Red Marrow" and source_organ in [ - "Trabecular Bone" - ]: + if target_organ.split()[0] == source_organ.split()[0]: continue if target_organ == "Total Body": continue - if source_organ == "Total Body": - continue + print(f" From source organ: {source_organ}") source_organ_mass = self.organ_masses.loc[ source_organ, "Mass_g" ] @@ -587,6 +608,11 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: contribution_from_sources += s_value_source_to_target * ( source_organ_mass / ROB_mass ) + self.contribution_from_sources_dict[radiation_type][ + target_organ + ]["sources"][ + f"contribution_from_{source_organ}" + ] = s_value_source_to_target s_value_ROB_to_target = ( s_values.loc[target_organ, "Total Body"] @@ -603,10 +629,13 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: return dose_df def perform_mass_scaling( - self, df: pandas.DataFrame, gender: str + self, + df: pandas.DataFrame, ) -> pandas.DataFrame: """Apply mass scaling to absorbed dose calculations based on patient-specific organ masses.""" - masses_path = path.join(MASSES_PATH, f"ICRP_mass_{gender.lower()}_target.csv") + masses_path = path.join( + MASSES_PATH, f"ICRP_mass_{self.config['Gender'].lower()}_target.csv" + ) model_masses_df = pandas.read_csv(masses_path, index_col=0) print("Performing mass scaling...") @@ -649,14 +678,9 @@ def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: this_dir = path.dirname(__file__) TEMPLATE_PATH = path.join(this_dir, "olindaTemplates") - if self.config["Gender"] == "Male": - template = pandas.read_csv(path.join(TEMPLATE_PATH, "adult_male.cas")) - elif self.config["Gender"] == "Female": - template = pandas.read_csv(path.join(TEMPLATE_PATH, "adult_female.cas")) - else: - print( - "Ensure that you correctly wrote patient gender in config file. Olinda supports: Male and Female." - ) + template = pandas.read_csv( + path.join(TEMPLATE_PATH, f"adult_{self.config['Gender'].lower()}.cas") + ) template.columns = ["Data"] match = re.match(r"([a-zA-Z]+)([0-9]+)", self.config["Radionuclide"]) @@ -666,13 +690,13 @@ def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: ind = template[template["Data"] == "[BEGIN NUCLIDES]"].index template.loc[ind[0] + 1, "Data"] = formatted_radionuclide + "|" - for organ in self.results_fitting.index: + for organ in self.results_fitting_organs.index: indices = template[template["Data"].str.contains(organ)].index source_organ = template.iloc[indices[0]].str.split("|")[0][0] mass_phantom = template.iloc[indices[0]].str.split("|")[0][1] - kinetic_data = self.results_fitting.loc[organ]["TIA_h"] - mass_data = round(self.results_fitting.loc[organ]["Volume_CT_mL"], 1) + kinetic_data = self.results_fitting_organs.loc[organ]["TIA_h"] + mass_data = round(self.results_fitting_organs.loc[organ]["Volume_CT_mL"], 1) # Update the template DataFrame template.iloc[indices[0]] = ( @@ -755,7 +779,7 @@ def read_Olinda_results(self, olinda_results_path: str) -> None: df_ad["Total"].fillna(0, inplace=True) df_ad["AD[Gy]"] = df_ad["Total"] * float(self.config["InjectedActivity"]) / 1000 df_ad = df_ad.rename(columns={"Total": "AD[Gy/GBq]"}) - self.df_ad = df_ad + self.results_dosimetry_organs = df_ad def compute_dose(self): """Compute Time Integrated Activity.""" From 4623ba7b476cfa22ff88982f9f6de26b67220c43 Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Wed, 5 Nov 2025 11:42:28 -0800 Subject: [PATCH 3/5] rename variables for consistency --- pytheranostics/dosimetry/base_dosimetry.py | 153 ++++++++++----------- 1 file changed, 73 insertions(+), 80 deletions(-) diff --git a/pytheranostics/dosimetry/base_dosimetry.py b/pytheranostics/dosimetry/base_dosimetry.py index 3380a44..2691c8c 100644 --- a/pytheranostics/dosimetry/base_dosimetry.py +++ b/pytheranostics/dosimetry/base_dosimetry.py @@ -122,9 +122,9 @@ def __init__( # DataFrame storing results self.results = self.initialize() - self.results_lesions = pandas.DataFrame() - self.results_salivaryglands = pandas.DataFrame() - self.df_ad = pandas.DataFrame() + self.results_dosimetry_lesions = pandas.DataFrame() + self.results_dosimetry_salivaryglands = pandas.DataFrame() + self.results_dosimetry_organs = pandas.DataFrame() # Sanity Checks: self.sanity_checks(metric="Volume_CT_mL") @@ -459,23 +459,7 @@ def compute_tia(self) -> None: def smart_fit_selection( self, region_data: pandas.Series, region: str ) -> lmfit.model.ModelResult: - """Select the best fit based on Akaike Information Criterion. - - If enabled in self.config, iterates through different valid fits orders and select best fit based on Akaike Information Criterion. - If a fit order is specified by the user, then the method will just perform fit following user's selected order and configuration. - - Parameters - ---------- - region_data : pandas.Series - Series containing Time and Activity. - region : str - Region of Interest - - Returns - ------- - lmfit.model.ModelResult - The best fit model based on Akaike Information Criterion. - """ + """Select the best fit based on Akaike Information Criterion.""" # If fit_order is defined by user: if self.config["rois"][region]["fit_order"] is not None: fit_results, _ = exponential_fit_lmfit( @@ -635,8 +619,8 @@ def calculate_bed(self, kinetic: str) -> None: RADIOBIOLOGY_DATA_FILE = Path(this_dir, "data", "radiobiology.json") with open(RADIOBIOLOGY_DATA_FILE) as f: self.radiobiology_dic = json.load(f) - bed_df = self.df_ad[ - self.df_ad.index.isin(list(self.radiobiology_dic.keys())) + bed_df = self.results_dosimetry_organs[ + self.results_dosimetry_organs.index.isin(list(self.radiobiology_dic.keys())) ] # only organs that we know the radiobiology parameters organs = numpy.array(bed_df.index.unique()) bed = {} @@ -645,7 +629,7 @@ def calculate_bed(self, kinetic: str) -> None: t_repair = self.radiobiology_dic[organ]["t_repair"] alpha_beta = self.radiobiology_dic[organ]["alpha_beta"] AD = ( - float(self.df_ad.loc[bed_df.index == organ]["AD[Gy/GBq]"].values[0]) + float(bed_df.loc[bed_df.index == organ]["AD_total[Gy/GBq]"].values[0]) * float(self.config["InjectedActivity"]) / 1000 ) # Gy @@ -701,7 +685,9 @@ def calculate_bed(self, kinetic: str) -> None: ) print(f"{organ}", bed[organ]) - self.df_ad["BED[Gy]"] = self.df_ad.index.map(bed) + self.results_dosimetry_organs["BED[Gy]"] = ( + self.results_dosimetry_organs.index.map(bed) + ) def save_images_and_masks_at(self, time_id: int) -> None: """Save CT, NM and masks for a specific time point. @@ -773,11 +759,13 @@ def write_json_data( cycle["InjectedActivity"] = self.config["InjectedActivity"] cycle["Weight_g"] = self.config["PatientWeight_g"] cycle["Level"] = self.config["Level"] - cycle["Method"] = self.config["Method"] - cycle["OutputFormat"] = self.config["OutputFormat"] - cycle["ScaleDoseByDensity"] = self.config.get( - "ScaleDoseByDensity", cycle.get("ScaleDoseByDensity", "NA") - ) + if cycle["Level"] == "Organ": + cycle["Method"] = self.config["OrganLevel"] + elif cycle["Level"] == "Voxel": + cycle["Method"] = self.config["VoxelLevel"] + cycle["ScaleDoseByDensity"] = self.config.get( + "ScaleDoseByDensity", cycle.get("ScaleDoseByDensity", "NA") + ) cycle["ReferenceTimePoint"] = self.config["ReferenceTimePoint"] cycle["TimePoints_h"] = self.results["Time_hr"][0] @@ -884,24 +872,24 @@ def write_json_data( if "Lesion" in organ or "TTB" in organ: cycle["rois"][organ]["density_gml"]["different_tps"] = "NA" cycle["rois"][organ]["density_gml"]["uncertainty"] = "NA" - cycle["rois"][organ]["density_gml"]["mean"] = self.results_lesions.loc[ - organ, "Density_g_per_mL" - ] + cycle["rois"][organ]["density_gml"]["mean"] = ( + self.results_dosimetry_lesions.loc[organ, "Density_g_per_mL"] + ) cycle["rois"][organ]["density_gml"]["mean_uncertainty"] = "NA" cycle["rois"][organ]["mass_g"]["different_tps"] = "NA" cycle["rois"][organ]["mass_g"]["uncertainty"] = "NA" - cycle["rois"][organ]["mass_g"]["mean"] = self.results_lesions.loc[ - organ, "Mass_g" - ] + cycle["rois"][organ]["mass_g"]["mean"] = ( + self.results_dosimetry_lesions.loc[organ, "Mass_g"] + ) cycle["rois"][organ]["mass_g"]["mean_uncertainty"] = "NA" - cycle["rois"][organ]["composition"] = self.results_lesions.loc[ - organ, "Composition" - ] - cycle["rois"][organ]["total_s_value"] = self.results_lesions.loc[ - organ, "Total_S_Value" - ] + cycle["rois"][organ]["composition"] = ( + self.results_dosimetry_lesions.loc[organ, "Composition"] + ) + cycle["rois"][organ]["total_s_value"] = ( + self.results_dosimetry_lesions.loc[organ, "Total_S_Value"] + ) cycle["rois"][organ]["total_s_value_uncertainty"] = "NA" - cycle["rois"][organ]["mean_AD_Gy"] = self.results_lesions.loc[ + cycle["rois"][organ]["mean_AD_Gy"] = self.results_dosimetry_lesions.loc[ organ, "AD_Gy" ] cycle["rois"][organ]["mean_AD_Gy_uncertainty"] = "NA" @@ -915,30 +903,30 @@ def write_json_data( cycle["rois"][organ]["density_gml"]["different_tps"] = "NA" cycle["rois"][organ]["density_gml"]["uncertainty"] = "NA" cycle["rois"][organ]["density_gml"]["mean"] = ( - self.results_salivaryglands.loc[organ, "Density_g_per_mL"] + self.results_dosimetry_salivaryglands.loc[organ, "Density_g_per_mL"] ) cycle["rois"][organ]["density_gml"]["mean_uncertainty"] = "NA" cycle["rois"][organ]["mass_g"]["different_tps"] = "NA" cycle["rois"][organ]["mass_g"]["uncertainty"] = "NA" cycle["rois"][organ]["mass_g"]["mean"] = ( - self.results_salivaryglands.loc[organ, "Mass_g"] + self.results_dosimetry_salivaryglands.loc[organ, "Mass_g"] ) cycle["rois"][organ]["mass_g"]["mean_uncertainty"] = "NA" - cycle["rois"][organ]["composition"] = self.results_salivaryglands.loc[ - organ, "Composition" - ] - cycle["rois"][organ]["total_s_value"] = self.results_salivaryglands.loc[ - organ, "Total_S_Value" - ] + cycle["rois"][organ]["composition"] = ( + self.results_dosimetry_salivaryglands.loc[organ, "Composition"] + ) + cycle["rois"][organ]["total_s_value"] = ( + self.results_dosimetry_salivaryglands.loc[organ, "Total_S_Value"] + ) cycle["rois"][organ]["total_s_value_uncertainty"] = "NA" - cycle["rois"][organ]["mean_AD_Gy"] = self.results_salivaryglands.loc[ - organ, "AD_Gy" - ] + cycle["rois"][organ]["mean_AD_Gy"] = ( + self.results_dosimetry_salivaryglands.loc[organ, "AD_Gy"] + ) cycle["rois"][organ]["mean_AD_Gy_uncertainty"] = "NA" if self.config["Level"] == "Organ": - for organ in self.df_ad.index: - if organ in self.df_ad.index: + for organ in self.results_dosimetry_organs.index: + if organ in self.results_dosimetry_organs.index: cycle["Organ-level_AD"][organ] = { "AD[Gy/GBq]": {}, "AD[Gy/GBq]_uncertianty": {}, @@ -947,19 +935,21 @@ def write_json_data( "BED[Gy]": {}, "BED[Gy]_uncertianty": {}, } - cycle["Organ-level_AD"][organ]["AD[Gy/GBq]"] = self.df_ad.loc[ - organ, "AD[Gy/GBq]" - ] + cycle["Organ-level_AD"][organ]["AD[Gy/GBq]"] = ( + self.results_dosimetry_organs.loc[organ, "AD_total[Gy/GBq]"] + ) cycle["Organ-level_AD"][organ]["AD[Gy/GBq]_uncertainty"] = "NA" - cycle["Organ-level_AD"][organ]["AD[Gy]"] = self.df_ad.loc[ - organ, "AD[Gy]" - ] + cycle["Organ-level_AD"][organ]["AD[Gy]"] = ( + self.results_dosimetry_organs.loc[organ, "AD_total[Gy]"] + ) cycle["Organ-level_AD"][organ]["AD[Gy]_uncertainty"] = "NA" - if "BED[Gy]" in self.df_ad.columns: + if "BED[Gy]" in self.results_dosimetry_organs.columns: cycle["Organ-level_AD"][organ]["BED[Gy]"] = ( - self.df_ad.loc[organ, "BED[Gy]"] - if pandas.notna(self.df_ad.loc[organ, "BED[Gy]"]) + self.results_dosimetry_organs.loc[organ, "BED[Gy]"] + if pandas.notna( + self.results_dosimetry_organs.loc[organ, "BED[Gy]"] + ) else "NA" ) else: @@ -967,7 +957,9 @@ def write_json_data( cycle["Organ-level_AD"][organ]["BED[Gy]_uncertianty"] = "NA" - if "Yes" in self.config["LesionDosimetry"]: + if "Yes" in self.config["OrganLevel"]["AdditionalOptions"].get( + "LesionDosimetry" + ): cycle["Organ-level_AD"]["TTB"] = { "mass_g": {}, "volumes_mL": {}, @@ -977,22 +969,23 @@ def write_json_data( "AD[Gy/GBq]": {}, "AD[Gy/GBq]_uncertianty": {}, } - cycle["Organ-level_AD"]["TTB"]["mass_g"] = self.results_lesions.loc[ - "TTB", "Mass_g" - ] - cycle["Organ-level_AD"]["TTB"]["volumes_mL"] = self.results_lesions.loc[ - "TTB", "Volume_CT_mL" - ] - cycle["Organ-level_AD"]["TTB"]["TIA_h"] = self.results_lesions.loc[ - "TTB", "TIA_h" - ] - cycle["Organ-level_AD"]["TTB"]["AD[Gy]"] = self.results_lesions.loc[ - "TTB", "AD_Gy" - ] + cycle["Organ-level_AD"]["TTB"]["mass_g"] = ( + self.results_dosimetry_lesions.loc["TTB", "Mass_g"] + ) + cycle["Organ-level_AD"]["TTB"]["volumes_mL"] = ( + self.results_dosimetry_lesions.loc["TTB", "Volume_CT_mL"] + ) + cycle["Organ-level_AD"]["TTB"]["TIA_h"] = ( + self.results_dosimetry_lesions.loc["TTB", "TIA_h"] + ) + cycle["Organ-level_AD"]["TTB"]["AD[Gy]"] = ( + self.results_dosimetry_lesions.loc["TTB", "AD_Gy"] + ) cycle["Organ-level_AD"]["TTB"]["AD[Gy]_uncertainty"] = "NA" - cycle["Organ-level_AD"]["TTB"]["AD[Gy/GBq]"] = self.results_lesions.loc[ - "TTB", "AD_Gy" - ] / (float(self.config["InjectedActivity"]) / 1000) + cycle["Organ-level_AD"]["TTB"]["AD[Gy/GBq]"] = ( + self.results_dosimetry_lesions.loc["TTB", "AD_Gy"] + / (float(self.config["InjectedActivity"]) / 1000) + ) cycle["Organ-level_AD"]["TTB"]["AD[Gy/GBq]_uncertianty"] = "NA" with open(file_path, "w") as file: From 6604d1b3fa1f3c2414c11271190a59eb1dab4a2e Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Wed, 5 Nov 2025 12:41:34 -0800 Subject: [PATCH 4/5] add condition for when there are no lesions --- pytheranostics/dosimetry/organ_s_dosimetry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 4088f7c..fc2bc37 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -250,6 +250,10 @@ def prepare_data(self) -> None: self.results_fitting_lesions = self.results[ self.results.index.str.contains("Lesion") ] + elif "No" in self.config["OrganLevel"]["AdditionalOptions"].get( + "LesionDosimetry" + ): + self.results_fitting_organs = self.results_fitting.copy() if "TotalTumorBurden" in self.results_fitting.index: self.results_fitting.drop("TotalTumorBurden", axis=0, inplace=True) From aac740356cbad1ad4fcdad3e43850beaa3b4c742 Mon Sep 17 00:00:00 2001 From: sarakurkowska Date: Thu, 13 Nov 2025 13:25:39 -0800 Subject: [PATCH 5/5] improve mass scaling for total body --- pytheranostics/dosimetry/organ_s_dosimetry.py | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index fc2bc37..b3515e1 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -561,28 +561,30 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: dose_df = s_values_subset.multiply(tia_series, axis=1) # ROB - print("Calculating Remainder of Body contributions...") dose_df["RemainderOfBody"] = 0 total_body_mass = self.organ_masses.loc["Total Body", "Mass_g"] - ROB_mass = tia_df.loc["RemainderOfBody", "Volume_CT_mL"] - + source_organs = tia_series.index if radiation_type == "beta": - organs = s_values_subset.index.difference( + target_organs = s_values_subset.index.difference( tia_series.index.difference(["Red Marrow", "Osteogenic Cells"]) ) + ROB_mass = tia_df.loc["RemainderOfBody", "Volume_CT_mL"] if radiation_type == "gamma": - organs = s_values_subset.index - self.contribution_from_sources_dict = {radiation_type: {}} - for target_organ in organs: - contribution_from_sources = 0 + target_organs = s_values_subset.index + total_mass_source_organs = self.organ_masses.loc[ + self.organ_masses.index.intersection(tia_series.index), "Mass_g" + ].sum() - self.contribution_from_sources_dict[radiation_type][ - target_organ - ] = {"sources": {}} + ROB_mass = total_body_mass - total_mass_source_organs - for source_organ in s_values.columns.difference(tia_series.index): + for target_organ in target_organs: + contribution_from_sources = 0 + + for source_organ in source_organs: if radiation_type == "beta": + if target_organ.split()[0] == source_organ.split()[0]: + continue if target_organ == "Red Marrow" and source_organ in [ "Trabecular Bone" ]: @@ -596,12 +598,10 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: if source_organ == "Total Body": continue - if target_organ.split()[0] == source_organ.split()[0]: - continue + if target_organ == "Total Body": continue - print(f" From source organ: {source_organ}") source_organ_mass = self.organ_masses.loc[ source_organ, "Mass_g" ] @@ -612,16 +612,18 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: contribution_from_sources += s_value_source_to_target * ( source_organ_mass / ROB_mass ) - self.contribution_from_sources_dict[radiation_type][ - target_organ - ]["sources"][ - f"contribution_from_{source_organ}" - ] = s_value_source_to_target - - s_value_ROB_to_target = ( - s_values.loc[target_organ, "Total Body"] - * (total_body_mass / ROB_mass) - ) - contribution_from_sources + + if target_organ == "Total Body": + s_value_ROB_to_target = ( + s_values.loc[target_organ, "Total Body"] + # * (total_body_mass / ROB_mass) since it is a target organ, the scaling will happen at mass scaling method, so not to have it twice its not performed here + ) - contribution_from_sources + else: + s_value_ROB_to_target = ( + s_values.loc[target_organ, "Total Body"] + * (total_body_mass / ROB_mass) + ) - contribution_from_sources + dose_df.at[target_organ, "RemainderOfBody"] = ( s_value_ROB_to_target * tia_df.loc["RemainderOfBody", "TIA_s"] ) @@ -645,10 +647,14 @@ def perform_mass_scaling( print("Performing mass scaling...") for organ in df["Target organ"]: - - if organ in model_masses_df.index and organ in self.results_fitting.index: - model_mass = model_masses_df.loc[organ, "Mass_g"] - patient_mass = self.results_fitting.loc[organ, "Volume_CT_mL"] + # Temporary mapping just for internal calculations + organ_internal = "RemainderOfBody" if organ == "Total Body" else organ + if ( + organ_internal in model_masses_df.index + and organ_internal in self.results_fitting.index + ): + model_mass = model_masses_df.loc[organ_internal, "Mass_g"] + patient_mass = self.results_fitting.loc[organ_internal, "Volume_CT_mL"] if ( pandas.notna(patient_mass)