diff --git a/pytheranostics/dosimetry/base_dosimetry.py b/pytheranostics/dosimetry/base_dosimetry.py index f599f86..4358836 100644 --- a/pytheranostics/dosimetry/base_dosimetry.py +++ b/pytheranostics/dosimetry/base_dosimetry.py @@ -123,9 +123,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") @@ -460,23 +460,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( @@ -636,8 +620,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 = {} @@ -646,7 +630,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 @@ -702,7 +686,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. @@ -775,11 +761,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] @@ -886,24 +874,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" @@ -917,30 +905,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": {}, @@ -949,19 +937,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: @@ -969,7 +959,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": {}, @@ -979,22 +971,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: diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 062cbec..d27bab5 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -123,22 +123,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: """ @@ -219,6 +224,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[ @@ -237,9 +243,13 @@ 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") + ] + 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) @@ -247,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", @@ -323,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( @@ -338,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 +369,7 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: svalues_gamma = self.load_svalues(gamma_path) 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 +401,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 +413,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,12 +489,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 = self._load_human_mass_table() - gender = self.config.get("Gender", "Male") - if gender not in masses.columns: - raise ValueError(f"Unknown gender '{gender}' for mass table.") - self.organ_masses = masses[[gender]].rename(columns={gender: "Mass_g"}) + 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: @@ -548,37 +561,46 @@ 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"] - ROB_mass = tia_df.loc["RemainderOfBody", "Volume_CT_mL"] + total_body_mass = self.organ_masses.loc["Total Body", "Mass_g"] + 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 - # adjust source organs so that they are different for gamma and beta radiation (in beta it is total - source organs + bone + skeleton) + 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() - for target_organ in organs: + ROB_mass = total_body_mass - total_mass_source_organs + + for target_organ in target_organs: contribution_from_sources = 0 - for source_organ in s_values.columns.difference(tia_series.index): - 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", - ]: - continue - if target_organ == "Red Marrow" and source_organ in [ - "Trabecular Bone" - ]: + 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" + ]: + continue + if target_organ == "Osteogenic Cells" and source_organ in [ + "Cortical Bone", + "Trabecular Bone", + "Red Marrow", + ]: + continue + + if source_organ == "Total Body": continue + if target_organ == "Total Body": continue - if source_organ == "Total Body": - continue source_organ_mass = self.organ_masses.loc[ source_organ, "Mass_g" @@ -591,10 +613,17 @@ def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: source_organ_mass / ROB_mass ) - 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"] ) @@ -606,21 +635,26 @@ 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 = self._load_human_mass_table() - if gender not in masses.columns: - raise ValueError(f"Unknown gender '{gender}' for mass table.") - model_masses_df = masses[[gender]].rename(columns={gender: "Mass_g"}) + 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...") 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) @@ -651,20 +685,12 @@ def perform_mass_scaling( def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: """Create .cas file that can be exported to Olinda/EXM.""" - if self.config["Gender"] == "Male": - template_file = "adult_male.cas" - elif self.config["Gender"] == "Female": - template_file = "adult_female.cas" - else: - print( - "Ensure that you correctly wrote patient gender in config file. Olinda supports: Male and Female." - ) - return + this_dir = path.dirname(__file__) + TEMPLATE_PATH = path.join(this_dir, "olindaTemplates") - with resource_path( - "pytheranostics.data", f"olinda/templates/human/{template_file}" - ) as template_path: - template = pandas.read_csv(template_path) + 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"]) @@ -674,13 +700,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]] = ( @@ -763,7 +789,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.""" 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([])