diff --git a/sauna/analysis.py b/sauna/analysis.py index 32f747e..77c7411 100644 --- a/sauna/analysis.py +++ b/sauna/analysis.py @@ -1,22 +1,31 @@ +from __future__ import annotations + import numpy as np import pandas as pd import os import scipy.optimize from copy import deepcopy +from numpy.typing import NDArray +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .sensitivity import Sensitivity, Sensitivities + from .covariance import Covariance, Covariances + class Analysis(): """A static class responsible for the routines, related to sensitivity and uncertainty analysis. This class - governs the intercations among sensitivities and + governs the interactions among sensitivities and uncertainties. """ - def __new__(cls): + def __new__(cls) -> None: raise TypeError('A static class cannot be instantiated.') @classmethod - def cov_to_unc(cls, covariance): + def cov_to_unc(cls, covariance: float) -> float: """Calculate uncertainty from covariance with the preservation of the sign. @@ -40,7 +49,7 @@ def cov_to_unc(cls, covariance): return uncertainty @classmethod - def unc_to_cov(cls, uncertainty): + def unc_to_cov(cls, uncertainty: float) -> float: """Calculate covariance from uncertainty with the preservation of the sign. @@ -64,9 +73,16 @@ def unc_to_cov(cls, uncertainty): return covariance @classmethod - def get_std(cls, vec_1, std_1, cov_matrix, vec_2, std_2): + def get_std( + cls, + vec_1: NDArray[np.floating], + std_1: NDArray[np.floating], + cov_matrix: NDArray[np.floating], + vec_2: NDArray[np.floating], + std_2: NDArray[np.floating], + ) -> float: """Calculate statistical uncertainty of uncertainty based upon the - sensitivities and their statisticl uncertainty.W + sensitivities and their statistical uncertainty. Parameters ---------- @@ -74,7 +90,7 @@ def get_std(cls, vec_1, std_1, cov_matrix, vec_2, std_2): The first sensitivity vector std_1: numpy.ndarray Statistical uncertainty of the first sensitivity vector - cov_matrix: numpay.ndarray + cov_matrix: numpy.ndarray Covariance matrix vec_2: numpy.ndarray The second sensitivity vector @@ -100,9 +116,16 @@ def get_std(cls, vec_1, std_1, cov_matrix, vec_2, std_2): return std @classmethod - def get_individual_std(cls, sensitivity_1, std_1, covariance, sensitivity_2, std_2): + def get_individual_std( + cls, + sensitivity_1: float, + std_1: float, + covariance: float, + sensitivity_2: float, + std_2: float, + ) -> float: """Calculate statistical uncertainty of uncertainty based upon the - sensitivities and their statisticl uncertainty. + sensitivities and their statistical uncertainty. Parameters ---------- @@ -110,7 +133,7 @@ def get_individual_std(cls, sensitivity_1, std_1, covariance, sensitivity_2, std The first sensitivity value std_1: float Statistical uncertainty of the first sensitivity value - cov_matrix: float + covariance: float Covariance value sensitivity_2: float The second sensitivity value @@ -137,7 +160,12 @@ def get_individual_std(cls, sensitivity_1, std_1, covariance, sensitivity_2, std return std @classmethod - def sandwich(cls, sensitivity_1, covariance, sensitivity_2): + def sandwich( + cls, + sensitivity_1: "Sensitivity", + covariance: "Covariance", + sensitivity_2: "Sensitivity", + ) -> tuple[float, float]: """Propagate a functional uncertainty via the Sandwich rule. Parameters @@ -151,34 +179,46 @@ def sandwich(cls, sensitivity_1, covariance, sensitivity_2): Return ------ - float, float - Return a tuple of the functional uncertainty and its statistical uncertainty. + tuple of (float, float) + A tuple of the functional uncertainty and its statistical uncertainty. """ # Get sensitivity vectors and its uncertainties - vec_1, std_1 = sensitivity_1.sensitivity_vector, sensitivity_1.uncertainty_vector - vec_2, std_2 = sensitivity_2.sensitivity_vector, sensitivity_2.uncertainty_vector + vec_1: NDArray[np.floating] = sensitivity_1.sensitivity_vector + std_1: NDArray[np.floating] = sensitivity_1.uncertainty_vector + vec_2: NDArray[np.floating] = sensitivity_2.sensitivity_vector + std_2: NDArray[np.floating] = sensitivity_2.uncertainty_vector - cov_matrix = covariance.dataframe.to_numpy() + cov_matrix: NDArray[np.floating] = covariance.dataframe.to_numpy() - covariance = vec_1 @ cov_matrix @ vec_2 + covariance_val: float = vec_1 @ cov_matrix @ vec_2 - if covariance != 0: - std = cls.get_std(vec_1, std_1, cov_matrix, vec_2, std_2) + if covariance_val != 0: + std: float = cls.get_std(vec_1, std_1, cov_matrix, vec_2, std_2) else: std = 0 # Artificially set std no more than uncertainty - uncertainty = cls.cov_to_unc(covariance) + uncertainty: float = cls.cov_to_unc(covariance_val) if std > abs(uncertainty): std = abs(uncertainty) return uncertainty, std @classmethod - def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', functional_2 = 'Eigenvalue', type = 'E', covariances = None, reactions = None, save_to = None): - """Calculate similarity indices + def compare( + cls, + sensitivities_1: "Sensitivities", + sensitivities_2: "Sensitivities", + functional_1: str = 'Eigenvalue', + functional_2: str = 'Eigenvalue', + type: str = 'E', + covariances: "Covariances | None" = None, + reactions: list[int] | None = None, + save_to: str | None = None, + ) -> float: + """Calculate similarity indices. Parameters ---------- @@ -186,24 +226,25 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', Sensitivities instance for the model (application) sensitivities_2 : Sensitivities Sensitivities instance for another model (experiment) that - a comparision is conducted with + a comparison is conducted with functional_1 : str, optional - First functional name. The defualt values is 'Eigenvalue'. + First functional name. The default value is 'Eigenvalue'. functional_2 : str, optional - Second functional name. The defualt values is 'Eigenvalue'. + Second functional name. The default value is 'Eigenvalue'. type: str, optional Type of the similarity index to compare. It supports the - {'E', c_k', 'G'} indices. . The defualt values is 'E'. + {'E', 'c_k', 'G'} indices. The default value is 'E'. covariances: Covariances, optional - Covariances instance to use for similarity assessment, + Covariances instance to use for similarity assessment; it must be specified if only the c_k type of similarity is calculated, otherwise it is not used. - reactions: list, optional + reactions: list of int, optional List of MT numbers to take into account for similarity assessment. - None is default and accounts all the reactions. This argument dictates to - take only certain MT numbers. It is currently applicable only for G. + None is default and accounts for all the reactions. This argument + dictates to take only certain MT numbers. It is currently + applicable only for G. save_to : str, optional - Path to save the breakdown for c_k. The default value is 'c_k.xlsx' + Path to save the breakdown for c_k. The default value is None. Returns ------- @@ -222,9 +263,9 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', if type == "E": - numerator = 0 - denominator_a = 0 - denominator_e = 0 + numerator: float = 0 + denominator_a: float = 0 + denominator_e: float = 0 for sensitivity_a in sensitivities_1.get_by_functional(functional_1): sensitivity_e = sensitivities_2.get_by_params(functional_2, sensitivity_a.zam, sensitivity_a.reaction) @@ -237,12 +278,12 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', elif type == "G": numerator = 0 - denominator = 0 + denominator: float = 0 if reactions == None: for sensitivity_1 in sensitivities_1.get_by_functional(functional_1): - vec_a = sensitivity_1.sensitivity_vector - vec_e = sensitivities_2.get_by_params(functional_2, sensitivity_1.zam, sensitivity_1.reaction).sensitivity_vector + vec_a: NDArray[np.floating] = sensitivity_1.sensitivity_vector + vec_e: NDArray[np.floating] = sensitivities_2.get_by_params(functional_2, sensitivity_1.zam, sensitivity_1.reaction).sensitivity_vector for g in range(len(vec_a)): @@ -268,29 +309,29 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', elif (np.abs(vec_a[g]) >= np.abs(vec_e[g])): numerator += np.abs(vec_a[g] - vec_e[g]) - index = 1 - numerator/denominator + index: float = 1 - numerator/denominator return index elif type == "c_k": - # Create an uncertainty dataframe to populated + # Create an uncertainty dataframe to populate c_k_df = pd.DataFrame(columns = ['Nuclide 1', 'Reaction 1', 'Nuclide 2', 'Reaction 2', 'Individual c_k']) numerator = 0 denominator_a = 0 denominator_e = 0 - # Get the list of all zams containing in both the application and + # Get the list of all zams contained in both the application and # experimental Sensitivities instances - zams = sensitivities_1.zams + list(set(sensitivities_2.zams) - set(sensitivities_1.zams)) + zams: list[int] = sensitivities_1.zams + list(set(sensitivities_2.zams) - set(sensitivities_1.zams)) for zam in zams: covs = covariances.get_by_zam(zam) for cov in covs: - zam_1 = cov.zam_1 - zam_2 = cov.zam_2 - first_mt = cov.reaction_1 - second_mt = cov.reaction_2 + zam_1: int = cov.zam_1 + zam_2: int = cov.zam_2 + first_mt: int = cov.reaction_1 + second_mt: int = cov.reaction_2 if (first_mt != 1) & (second_mt != 1) & (first_mt != 455) & (first_mt != 456) & (second_mt != 455) & (second_mt != 456): sensitivity_a1 = sensitivities_1.get_by_params(functional_1, zam_1, first_mt) @@ -303,7 +344,7 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', cov_ae = cls.unc_to_cov(uncertainty_ae) numerator += cov_ae - individual_contribution = cov_ae + individual_contribution: float = cov_ae uncertainty_a, _ = cls.sandwich(sensitivity_a1, cov, sensitivity_a2) cov_a = cls.unc_to_cov(uncertainty_a) @@ -322,11 +363,11 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', denominator_e += cov_e # Create a new row to append it to the dataframe - new_row = {'Nuclide 1' : f'{sensitivity_a1.zam}', - 'Reaction 1' : f'MT{sensitivity_a1.reaction}', - 'Nuclide 2' : f'{sensitivity_a2.zam}', - 'Reaction 2' : f'MT{sensitivity_a2.reaction}', - 'Individual c_k' : individual_contribution} + new_row: dict = {'Nuclide 1' : f'{sensitivity_a1.zam}', + 'Reaction 1' : f'MT{sensitivity_a1.reaction}', + 'Nuclide 2' : f'{sensitivity_a2.zam}', + 'Reaction 2' : f'MT{sensitivity_a2.reaction}', + 'Individual c_k' : individual_contribution} c_k_df.loc[len(c_k_df)] = new_row @@ -334,9 +375,9 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', index = numerator/denominator # Add the total uncertainty row - total_c_k = np.sum(c_k_df['Individual c_k']) + total_c_k: float = np.sum(c_k_df['Individual c_k']) - total_row = {'Nuclide 1' : 'total', + total_row: dict = {'Nuclide 1' : 'total', 'Reaction 1' : 'total', 'Nuclide 2' : 'total', 'Reaction 2' : 'total', @@ -355,7 +396,7 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', c_k_df.to_excel(writer) else: with pd.ExcelWriter(name, engine='openpyxl', mode='w') as writer: - c_k_df.to_excel(writer) + c_k_df.to_excel(writer) print(f"{functional_1} uncertainty of the first model is {np.sqrt(denominator_a)*100:2f}%.") @@ -376,7 +417,13 @@ def compare(cls, sensitivities_1, sensitivities_2, functional_1 = 'Eigenvalue', raise NotImplementedError(f"The set method of comparison '{type}' is not implemented.") @classmethod - def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = False): + def get_breakdown( + cls, + sensitivities: "Sensitivities", + covariances: "Covariances", + save_to: str | None = None, + by_total: bool = False, + ) -> dict[str, pd.DataFrame]: """Propagate the uncertainties of the functionals in the Sensitivities instance via the Sandwich rule and produce the dataframes with the uncertainty breakdown by uncertainty sources. @@ -387,15 +434,15 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa Sensitivities instance covariances : Covariances Covariances instance - save_to : str, optioanl + save_to : str, optional Path to save the breakdown by_total: bool, optional Whether to compute the uncertainty by total cross section. - The default values is False. + The default value is False. Return ------ - dict + dict of {str : pandas.DataFrame} Return a dictionary in the form {str : pandas.DataFrame} where the key is the functional, and item is the uncertainty breakdown. @@ -407,16 +454,16 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa """ # Create a dictionary of dataframes for different functionals - dataframes = {} + dataframes: dict[str, pd.DataFrame] = {} # Populate the uncertainty dataframe for each nuclide and reaction - # Get necessary functionals from sensetivities + # Get necessary functionals from sensitivities for functional in sensitivities.functionals: - # Create an uncertainty dataframe to populated + # Create an uncertainty dataframe to populate uncertainty_df = pd.DataFrame(columns = ['Nuclide 1', 'Reaction 1', 'Nuclide 2', 'Reaction 2', 'Uncertainty [%]', 'Statistical Uncertainty [%]']) - # Get necessary zams from sensetivities + # Get necessary zams from sensitivities for zam in sensitivities.zams: # Get covs based upon the zams @@ -424,10 +471,10 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa # Get sensitivities for each cov and propagate the uncertainties for cov in covs: - zam_1 = cov.zam_1 - zam_2 = cov.zam_2 - first_mt = cov.reaction_1 - second_mt = cov.reaction_2 + zam_1: int = cov.zam_1 + zam_2: int = cov.zam_2 + first_mt: int = cov.reaction_1 + second_mt: int = cov.reaction_2 if (first_mt != 1) & (second_mt != 1) & (by_total == False): if (first_mt != 455) & (first_mt != 456) & (second_mt != 455) & (second_mt != 456): @@ -435,7 +482,7 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa sensitivity_1 = sensitivities.get_by_params(functional, zam_1, first_mt) sensitivity_2 = sensitivities.get_by_params(functional, zam_2, second_mt) - # Get the uncertainty intrduced by given reactions to a functional + # Get the uncertainty introduced by given reactions to a functional uncertainty, std = cls.sandwich(sensitivity_1, cov, sensitivity_2) # Account that cross-reaction covariance must be doubled @@ -443,7 +490,7 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa uncertainty, std = uncertainty*np.sqrt(2), std*np.sqrt(2) # Create a new row to append it to the dataframe - new_row = {'Nuclide 1' : f'{sensitivity_1.zam}', + new_row: dict = {'Nuclide 1' : f'{sensitivity_1.zam}', 'Reaction 1' : f'MT{sensitivity_1.reaction}', 'Nuclide 2' : f'{sensitivity_2.zam}', 'Reaction 2' : f'MT{sensitivity_2.reaction}', @@ -461,7 +508,7 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa sensitivity_1 = sensitivities.get_by_params(functional, zam_1, first_mt) sensitivity_2 = sensitivities.get_by_params(functional, zam_2, second_mt) - # Get the uncertainty intrduced by given reactions to a functional + # Get the uncertainty introduced by given reactions to a functional uncertainty, std = cls.sandwich(sensitivity_1, cov, sensitivity_2) # Account that cross-reaction covariance must be doubled @@ -469,7 +516,7 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa uncertainty, std = uncertainty*np.sqrt(2), std*np.sqrt(2) # Create a new row to append it to the dataframe - new_row = {'Nuclide 1' : f'{sensitivity_1.zam}', + new_row = {'Nuclide 1' : f'{sensitivity_1.zam}', 'Reaction 1' : f'MT{sensitivity_1.reaction}', 'Nuclide 2' : f'{sensitivity_2.zam}', 'Reaction 2' : f'MT{sensitivity_2.reaction}', @@ -485,7 +532,7 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa uncertainty, std = cls.sandwich(sensitivity_1, cov, sensitivity_2) # Create a new row to append it to the dataframe - new_row = {'Nuclide 1' : f'{sensitivity_1.zam}', + new_row = {'Nuclide 1' : f'{sensitivity_1.zam}', 'Reaction 1' : f'MT{sensitivity_1.reaction}', 'Nuclide 2' : f'{sensitivity_2.zam}', 'Reaction 2' : f'MT{sensitivity_2.reaction}', @@ -496,10 +543,10 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa uncertainty_df.loc[len(uncertainty_df)] = new_row # Add the total uncertainty row - total_uncertainty = np.sqrt(np.sum(i*i if i >=0 else -i*i for i in uncertainty_df['Uncertainty [%]'])) - stat_uncertainty = np.sqrt(np.sum(uncertainty_df['Statistical Uncertainty [%]'][i]**2 * uncertainty_df['Uncertainty [%]'][i]**2 / total_uncertainty**2 for i in range(len(uncertainty_df['Statistical Uncertainty [%]'])))) + total_uncertainty: float = np.sqrt(np.sum(i*i if i >=0 else -i*i for i in uncertainty_df['Uncertainty [%]'])) + stat_uncertainty: float = np.sqrt(np.sum(uncertainty_df['Statistical Uncertainty [%]'][i]**2 * uncertainty_df['Uncertainty [%]'][i]**2 / total_uncertainty**2 for i in range(len(uncertainty_df['Statistical Uncertainty [%]'])))) - total_row = {'Nuclide 1' : 'total', + total_row: dict = {'Nuclide 1' : 'total', 'Reaction 1' : 'total', 'Nuclide 2' : 'total', 'Reaction 2' : 'total', @@ -524,7 +571,13 @@ def get_breakdown(cls, sensitivities, covariances, save_to = None, by_total = Fa return dataframes @classmethod - def get_detailed(cls, sensitivities, covariances, save_to = None, by_total = False): + def get_detailed( + cls, + sensitivities: "Sensitivities", + covariances: "Covariances", + save_to: str | None = None, + by_total: bool = False, + ) -> dict[str, pd.DataFrame]: """Get detailed (groupwise) uncertainty breakdown. Parameters @@ -537,17 +590,17 @@ def get_detailed(cls, sensitivities, covariances, save_to = None, by_total = Fal Path to save the breakdown by_total: bool, optional Whether to compute the uncertainty by total cross section. - The default values is False. + The default value is False. Return ------ - dict + dict of {str : pandas.DataFrame} Return a dictionary in the form {str : pandas.DataFrame} where the key is the functional, and item is the uncertainty breakdown. Notes ----- - It works slow due to many lines when impoorting to Excel, which is the + It works slow due to many lines when importing to Excel, which is the bottleneck here. The method currently does not account the inherent decay constant uncertainty for lambda-eff — only impact from the data in MF31-35. @@ -555,12 +608,19 @@ def get_detailed(cls, sensitivities, covariances, save_to = None, by_total = Fal """ # Create a dictionary of dataframes for different functionals - dataframes = {} - group_number = len(sensitivities.group_structure) - 1 - indices = np.arange(group_number) - sqrt_two = np.sqrt(2) - def get_rows(functional, zam_1, zam_2, first_mt, second_mt): - """An auxiliary method to populate the dataframe + dataframes: dict[str, pd.DataFrame] = {} + group_number: int = len(sensitivities.group_structure) - 1 + indices: NDArray[np.intp] = np.arange(group_number) + sqrt_two: float = np.sqrt(2) + + def get_rows( + functional: str, + zam_1: int, + zam_2: int, + first_mt: int, + second_mt: int, + ) -> list[dict]: + """An auxiliary method to populate the dataframe. Parameters ---------- @@ -577,25 +637,25 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): Return ------ - dict - Return a dictionary to append them to the dataframe + list of dict + Return a list of row dictionaries to append to the dataframe """ # Get Sensitivity instances for the corresponding covariance matrix sensitivity_1 = sensitivities.get_by_params(functional, zam_1, first_mt) sensitivity_2 = sensitivities.get_by_params(functional, zam_2, second_mt) - is_mts_equal = (first_mt == second_mt) - vec_1 = sensitivity_1.sensitivity_vector - vec_2 = sensitivity_2.sensitivity_vector - std_1 = sensitivity_1.uncertainty_vector - std_2 = sensitivity_2.uncertainty_vector - cov_matrix = cov.dataframe.to_numpy() - - temp_rows = [] + is_mts_equal: bool = (first_mt == second_mt) + vec_1: NDArray[np.floating] = sensitivity_1.sensitivity_vector + vec_2: NDArray[np.floating] = sensitivity_2.sensitivity_vector + std_1: NDArray[np.floating] = sensitivity_1.uncertainty_vector + std_2: NDArray[np.floating] = sensitivity_2.uncertainty_vector + cov_matrix: NDArray[np.floating] = cov.dataframe.to_numpy() + + temp_rows: list[dict] = [] for i in indices: for j in indices: - uncertainty = cls.cov_to_unc(vec_1[i] * cov_matrix[i][j] * vec_2[j]) - std = cls.get_individual_std(vec_1[i], std_1[i], cov_matrix[i][j], vec_2[j], std_2[j]) + uncertainty: float = cls.cov_to_unc(vec_1[i] * cov_matrix[i][j] * vec_2[j]) + std: float = cls.get_individual_std(vec_1[i], std_1[i], cov_matrix[i][j], vec_2[j], std_2[j]) # Artificially set std not more than uncertainty if std > abs(uncertainty): @@ -606,7 +666,7 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): uncertainty, std = uncertainty * sqrt_two, std * sqrt_two # Create a new row to append it to the dataframe - new_row = {'Nuclide 1' : f'{zam_1}', + new_row: dict = {'Nuclide 1' : f'{zam_1}', 'Reaction 1' : f'MT{first_mt}', 'Group 1' : group_number - i, 'Nuclide 2' : f'{zam_2}', @@ -618,10 +678,10 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): return temp_rows # Populate the uncertainty dataframe for each nuclide and reaction - # Get necessary functionals from sensetivities + # Get necessary functionals from sensitivities if by_total == False: for functional in sensitivities.functionals: - rows = [] + rows: list[dict] = [] if ('beta-eff' == functional) | ('lambda-eff' == functional): for zam in sensitivities.zams: covs = covariances.get_by_zam(zam) @@ -632,7 +692,7 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): second_mt = cov.reaction_2 if (first_mt != 1) & (second_mt != 1): - is_nu_t = (first_mt != 455) & (first_mt != 456) & (second_mt != 455) & (second_mt != 456) + is_nu_t: bool = (first_mt != 455) & (first_mt != 456) & (second_mt != 455) & (second_mt != 456) if is_nu_t: rows.extend(get_rows(functional, zam_1, zam_2, first_mt, second_mt)) elif (first_mt == 455 | second_mt == 455) & (first_mt != 456 | second_mt != 456): @@ -649,7 +709,7 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): if (first_mt != 1) & (second_mt != 1): is_nu_t = (first_mt != 455) & (first_mt != 456) & (second_mt != 455) & (second_mt != 456) if is_nu_t: rows.extend(get_rows(functional, zam_1, zam_2, first_mt, second_mt)) - # Create and populated the final dataframe + # Create and populate the final dataframe uncertainty_df = pd.DataFrame(rows, columns = ['Nuclide 1', 'Reaction 1', 'Group 1', 'Nuclide 2', 'Reaction 2', 'Group 2', 'Uncertainty [%]', 'Statistical Uncertainty [%]']) # Add the total uncertainty row @@ -695,7 +755,7 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): if ((first_mt == 1) & (second_mt == 1)) | ((first_mt == 1018) & (second_mt == 1018)) | ((first_mt == 251) & (second_mt == 251)) | ((first_mt == 452) & (second_mt == 452)): rows.extend(get_rows(functional, zam_1, zam_2, first_mt, second_mt)) - # Create and populated the final dataframe + # Create and populate the final dataframe uncertainty_df = pd.DataFrame(rows, columns = ['Nuclide 1', 'Reaction 1', 'Group 1', 'Nuclide 2', 'Reaction 2', 'Group 2', 'Uncertainty [%]', 'Statistical Uncertainty [%]']) # Add the total uncertainty row @@ -713,7 +773,7 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): uncertainty_df.loc[len(uncertainty_df)] = total_row - # # Sort the uncertainties by its absolute values + # Sort the uncertainties by its absolute values uncertainty_df = uncertainty_df.reindex(uncertainty_df['Uncertainty [%]'].abs().sort_values(ascending=False).index).reset_index(drop=True) dataframes[functional] = uncertainty_df @@ -727,7 +787,16 @@ def get_rows(functional, zam_1, zam_2, first_mt, second_mt): return dataframes @classmethod - def get_concentration_uncertainty(cls, sensitivities, uncertainty, targets, background_zams, fraction, fraction_type = 'ao', uncertainty_type = 'normal'): + def get_concentration_uncertainty( + cls, + sensitivities: "Sensitivities", + uncertainty: float, + targets: NDArray[np.intp], + background_zams: list[int], + fraction: float, + fraction_type: str = 'ao', + uncertainty_type: str = 'normal', + ) -> dict[str, float]: """Propagate the uncertainties of the functionals in the Sensitivities instance based upon the idea that macroscopic XS are strictly defined as a product of the nuclide concentration and the corresponding total @@ -742,37 +811,37 @@ def get_concentration_uncertainty(cls, sensitivities, uncertainty, targets, back Uncertainty in the concentration: 68% of the confidence interval for the 'normal' distribution or 1/2 of the range of the 'uniform' and 'triangular' distributions. - targets: numpy.ndarray of ints - ZAM values for the nuclide, which uncertainty + targets: numpy.ndarray of int + ZAM values for the nuclide whose uncertainty is provided in the uncertainty parameter. - background_zams: numpy.ndarray - Nuclides to be considered in the calculation and exclude the target nuclide. + background_zams: list of int + Nuclides to be considered in the calculation, excluding the target nuclide. Make sure that correct nuclides (ZAMs) for the region, where the influence of uncertainties in concentrations is assessed. For example, ZAMs - for cladding must not be provided if the Pu239 fraction uncecrtainty + for cladding must not be provided if the Pu239 fraction uncertainty influence in the HM fuel composition is of interest. fraction: float Fraction of the target nuclide among the nuclides parameter. - fraction_type: string, optional + fraction_type: str, optional Type of the fraction which the uncertainty is calculated for. - It support only the atomic fraction 'ao', which is the same for - concentrations, and the weight fraction 'wo'. The default + It supports only the atomic fraction 'ao', which is the same for + concentrations, and the weight fraction 'wo'. The default value is 'ao'. - uncertainty_type: string, optional + uncertainty_type: str, optional Type of the uncertainty distribution. It is assumed as one of the following {'normal', 'uniform', 'triangular'}. The default value is 'normal'. Return ------ - dict + dict of {str : float} Return a dictionary in the form {str : float} where the key is the functional, and item is the uncertainty based upon constrained sensitivity. """ - nuclides = background_zams + nuclides: list[int] = background_zams # Make sure that target values are not included in the nuclides list for target in targets: @@ -780,22 +849,22 @@ def get_concentration_uncertainty(cls, sensitivities, uncertainty, targets, back nuclides.remove(target) print(f'{target} has been removed from the background_zams array.') - concentration_uncertainties = {} + concentration_uncertainties: dict[str, float] = {} for functional in sensitivities.functionals: - unconstrained_sensitivity = 0 + unconstrained_sensitivity: float = 0 for target in targets: unconstrained_sensitivity += sensitivities.get_by_params(functional, target, 1).sensitivity - constrainer = 0 + constrainer: float = 0 for nuclide in nuclides: constrainer += sensitivities.get_by_params(functional, nuclide, 1).sensitivity # Get constrained sensitivities: if fraction_type == 'ao': - constrained_sensitivity = unconstrained_sensitivity - fraction/(1-fraction)*constrainer - concentration_uncertainty = np.abs(constrained_sensitivity * uncertainty) + constrained_sensitivity: float = unconstrained_sensitivity - fraction/(1-fraction)*constrainer + concentration_uncertainty: float = np.abs(constrained_sensitivity * uncertainty) concentration_uncertainties[functional] = concentration_uncertainty elif fraction_type == 'wo': constrained_sensitivity = (unconstrained_sensitivity - constrainer) / (1 - fraction) @@ -818,7 +887,13 @@ def get_concentration_uncertainty(cls, sensitivities, uncertainty, targets, back return concentration_uncertainties @classmethod - def get_density_uncertainty(cls, sensitivities, uncertainty, targets, uncertainty_type = 'normal'): + def get_density_uncertainty( + cls, + sensitivities: "Sensitivities", + uncertainty: float, + targets: NDArray[np.intp], + uncertainty_type: str = 'normal', + ) -> dict[str, float]: """Propagate the uncertainties of the functionals in the Sensitivities instance based upon the idea that macroscopic XS are strictly defined as a product of the nuclide concentration and the corresponding total @@ -833,28 +908,28 @@ def get_density_uncertainty(cls, sensitivities, uncertainty, targets, uncertaint Uncertainty in the concentration: 68% of the confidence interval for the 'normal' distribution or 1/2 of the range of the 'uniform' or 'triangular' distributions - targets: numpy.ndarray of ints - ZAM values for the nuclide, which uncertainty + targets: numpy.ndarray of int + ZAM values for the nuclide whose uncertainty is provided in the uncertainty parameter. Make sure the target nuclides are located in the volume of interest - uncertainty_type: string, optional + uncertainty_type: str, optional Type of the uncertainty distribution. It is assumed as one of the following {'normal', 'uniform', 'triangular'}. The default value is 'normal'. Return ------ - dict + dict of {str : float} Return a dictionary in the form {str : float} where the key is the functional, and item is the uncertainty based upon total sensitivity. """ - uncertainties = {} + uncertainties: dict[str, float] = {} for functional in sensitivities.functionals: - sensitivity = 0 + sensitivity: float = 0 for target in targets: sensitivity += sensitivities.get_by_params(functional, target, 1).sensitivity @@ -875,13 +950,13 @@ def get_density_uncertainty(cls, sensitivities, uncertainty, targets, uncertaint return uncertainties @classmethod - def _define_cost(cls, zam, reaction, cost_type='A'): - """Provide the cost function coefficitne (lambda) based upon the + def _define_cost(cls, zam: int, reaction: int, cost_type: str = 'A') -> float: + """Provide the cost function coefficient (lambda) based upon the type according to WPEC/SG26. Parameters ---------- - zam : float + zam : int ZAM value for the nuclide of interest reaction : int MT number for the reaction of interest @@ -904,6 +979,8 @@ def _define_cost(cls, zam, reaction, cost_type='A'): if cost_type not in ('A', 'B', 'C'): raise NotImplementedError(f"The cost type '{cost_type}' is not implemented, only 'A', 'B', and 'C' are supported.") + cost_coefficient: float + if (zam in (922350, 922380, 942390)) & (reaction in (18, 102, 452)): if cost_type == 'A': cost_coefficient = 1 @@ -950,24 +1027,29 @@ def _define_cost(cls, zam, reaction, cost_type='A'): return cost_coefficient @classmethod - def _weight_function(cls, uncertainties_to_minimize, costs, base_uncertainties): - """An auxiliary function to calculates the sum of the weighted inverse + def _weight_function( + cls, + uncertainties_to_minimize: NDArray[np.floating], + costs: NDArray[np.floating], + base_uncertainties: NDArray[np.floating], + ) -> float: + """An auxiliary function to calculate the sum of the weighted inverse variances weight_i/uncertainties_i**2. Parameters ---------- uncertainties_to_minimize : numpy.ndarray - Values to propagate the uncertainities + Values to propagate the uncertainties costs : numpy.ndarray Cost coefficients corresponding to the uncertainty being minimized. base_uncertainties : numpy.ndarray Base values of the uncertainties being minimized to make sure that - zero value uncertainties are still. + zero value uncertainties are still handled correctly. Return ------ float - Return the sum + Return the sum of weighted inverse variances """ @@ -979,20 +1061,26 @@ def _weight_function(cls, uncertainties_to_minimize, costs, base_uncertainties): return np.sum(np.where(base_uncertainties == 0, 0, costs / (uncertainties_to_minimize * uncertainties_to_minimize))) @classmethod - def _weight_jacobian(cls, uncertainties_to_minimize, costs, base_uncertainties): - """An auxiliary function to calculates the Jacobian of + def _weight_jacobian( + cls, + uncertainties_to_minimize: NDArray[np.floating], + costs: NDArray[np.floating], + base_uncertainties: NDArray[np.floating], + ) -> NDArray[np.floating]: + """An auxiliary function to calculate the Jacobian of the sum of the weighted inverse variances, which is -2*weight_i/uncertainties_i**3. Parameters ---------- uncertainties_to_minimize : numpy.ndarray - Values to propagate the uncertainities + Values to propagate the uncertainties costs : numpy.ndarray Cost coefficients corresponding to the uncertainty being minimized. base_uncertainties : numpy.ndarray Base values of the uncertainties being minimized to make sure that - zero value uncertainties are still. + zero value uncertainties are still handled correctly. + Return ------ numpy.ndarray @@ -1009,56 +1097,68 @@ def _weight_jacobian(cls, uncertainties_to_minimize, costs, base_uncertainties): return np.where(base_uncertainties == 0, 0, (-2 * costs) / uncertainties_to_minimize**3) @classmethod - def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lower_boundary=0.005, method='trust-constr', cost_type='A', energy_costs = None, maxiter=1000, tol = 1e-5): - """Calculates target accuracy requirements based upon - a Sensitivities instance, an Uncertainties instance + def tars( + cls, + model_sensitivities: list["Sensitivities"], + covariances: "Covariances", + tars: dict[str, float], + number_of_reactions: int = 10, + lower_boundary: float = 0.005, + method: str = 'trust-constr', + cost_type: str = 'A', + energy_costs: list[float] | None = None, + maxiter: int = 1000, + tol: float = 1e-5, + ) -> "Covariances": + """Calculate target accuracy requirements based upon + a Sensitivities instance, a Covariances instance and TAR values. Parameters ---------- - model_sensitivities : list + model_sensitivities : list of Sensitivities List of Sensitivities instances covariances : Covariances Covariances instance - tars: dict + tars: dict of {str : float} Dictionary of target accuracies in the form of {functional : TAR, ...}, e.g. {'Eigenvalue' : 0.005}. number_of_reactions : int, optional Number of top symmetric covariances to take into account in the optimization process. The other uncertainties are assumed - to be constant. The actual number of the going to be tweaked - covariances is more since it the parameter does not include the + to be constant. The actual number of covariances tweaked + is more since the parameter does not include the cross-reaction covariances. Especially, the actual number can be - significantly increased if two or more functionals considered + significantly increased if two or more functionals are considered with different uncertainty sources (up to *len(functionals)). Default value is 10. lower_boundary : float, optional - Lower boundary for uncertainty. Each uncertainty cannnot + Lower boundary for uncertainty. Each uncertainty cannot be decreased lower than this value. The current implementation assumes that if the value is already lower than the set value, the value will not be changed. - The method does not handle lower boundary equal zero, to + The method does not handle lower boundary equal to zero; to avoid that, it is recommended to provide the lower boundary - reasonably higher than zero (i.e. is not going to be + reasonably higher than zero (i.e. not going to be reached), e.g. 1e-4. The default value is 0.005. - method : string, optional + method : str, optional Method to be applied by the optimizer. Only 'trust-constr' is currently implemented as a more reliable approach: https://github.com/scipy/scipy/issues/9640#issuecomment-451918238 . The default value is 'trust-constr'. - cost_type : string, optional + cost_type : str, optional Parameter identifying cost parameters {'A', 'B', 'C'}. The coefficients are defined according to the NEA/OECD WPEC/SG26 report. The cost for other parameters (not mentioned in the - report) is set to maximum, i.e. the (n,n') cost is set to (\chi)). + report) is set to maximum, i.e. the (n,n') cost is set to (chi). The default value is 'A', assuming there is no difference among reactions. - energy_cost : list, optional + energy_costs : list of float, optional List of energy weights identifying the cost at each energy group that - is multipled with cost_type to get the total cost in the TAR excercise. + is multiplied with cost_type to get the total cost in the TAR exercise. The default value is None, meaning there is no energy dependence of the cost function. - maxiter: float, optional + maxiter: int, optional Number of maximum iterations to be passed to the SciPy optimizer. The default value is 1000. tol: float, optional @@ -1086,19 +1186,19 @@ def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lo lower_boundary = 1e-5 # Variables for iteration - group_number = len(covariances.group_structure) - 1 - base_indices = np.arange(number_of_reactions) + group_number: int = len(covariances.group_structure) - 1 + base_indices: NDArray[np.intp] = np.arange(number_of_reactions) if energy_costs == None: energy_costs = [1]*group_number # Get dataframe for the uncertainty sources # Get MTs present in the dataframe to avoid accounting others - # Remove cross-correlations, cross-correations are fixed during next step - all_reactions = set() - dfs = [] + # Remove cross-correlations, cross-correlations are fixed during next step + all_reactions: set[int] = set() + dfs: list[dict[str, pd.DataFrame]] = [] for sensitivities in model_sensitivities: - model_dfs = {} + model_dfs: dict[str, pd.DataFrame] = {} for functional in tars: df = cls.get_breakdown(sensitivities, covariances)[functional] all_reactions.update(df['Reaction 1'].drop_duplicates().sort_values().str.replace('MT', '').drop(df[df['Nuclide 1']=='total'].index, inplace=False).astype(int)) @@ -1107,17 +1207,17 @@ def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lo dfs.append(model_dfs) # Populate list of uncertainties_to_minimize based upon the number of reactions - uncertainties = [] - zams = [] - reactions = [] - zam_covs = [] - zam_mt = [] - costs = [] + uncertainties: list[float] = [] + zams: list[int] = [] + reactions: list[int] = [] + zam_covs: list["Covariance"] = [] + zam_mt: list[tuple[int, int]] = [] + costs: list[float] = [] for s, sensitivities in enumerate(model_sensitivities): for functional in tars: for row in base_indices: - mt = int(dfs[s][functional]['Reaction 1'][row][2:]) - zam = int(dfs[s][functional]['Nuclide 1'][row]) + mt: int = int(dfs[s][functional]['Reaction 1'][row][2:]) + zam: int = int(dfs[s][functional]['Nuclide 1'][row]) if zam not in zams: zam_covs.extend(covariances.get_by_zam(zam)) @@ -1131,26 +1231,26 @@ def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lo cov = covariances.get_by_params(zam, zam, mt, mt) # Get the uncertainties from the covariance matrix - diag = np.diag(cov.dataframe.to_numpy()) + diag: NDArray[np.floating] = np.diag(cov.dataframe.to_numpy()) uncertainties.extend(np.sqrt(diag)) - uncertainties = np.array(uncertainties) + uncertainties_arr: NDArray[np.floating] = np.array(uncertainties) # Redefine energy costs - energy_costs = energy_costs * int(len(uncertainties)/group_number) - costs = np.multiply(costs, energy_costs) + energy_costs = energy_costs * int(len(uncertainties_arr)/group_number) + costs_arr: NDArray[np.floating] = np.multiply(costs, energy_costs) # Populate list of covs which are going to be tweaked # This list includes both cross-material and cross-reaction correlations zam_covs = [cov for cov in zam_covs if cov.reaction_1 in all_reactions] # Update indices for all the functionals present - indices = np.arange(len(zam_mt)) - uncertainty_indices = np.append(indices*group_number, [(indices[-1]+1)*group_number]) + indices: NDArray[np.intp] = np.arange(len(zam_mt)) + uncertainty_indices: NDArray[np.intp] = np.append(indices*group_number, [(indices[-1]+1)*group_number]) # Prepare covariance list which is going to be accessed during the # optimization - temp_covs = [] + temp_covs: list[list["Covariance"]] = [] for row in indices: mt = reactions[row] zam = zams[row] @@ -1158,9 +1258,9 @@ def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lo # Prepare sensitivity list which is going to be accessed during the # optimization - temp_senses = [] + temp_senses: list[dict] = [] for sensitivities in model_sensitivities: - model_senses = {} + model_senses: dict = {} for functional in tars: for row in indices: mt = reactions[row] @@ -1175,27 +1275,30 @@ def tars(cls, model_sensitivities, covariances, tars, number_of_reactions=10, lo temp_senses.append(model_senses) # Constraints for uncertainties: (unc_i)_min <= unc_i <= (unc_i)_0 - lower_boundaries = np.array([unc if unc < lower_boundary else lower_boundary for unc in uncertainties]) - upper_boundaries = uncertainties.copy() + lower_boundaries: NDArray[np.floating] = np.array([unc if unc < lower_boundary else lower_boundary for unc in uncertainties_arr]) + upper_boundaries: NDArray[np.floating] = uncertainties_arr.copy() ###################### # The second part ###################### # Sandwich formula functions to be constrained to TAR - def sandwich_constraint(uncertainties_to_minimize, functional, model_index): + def sandwich_constraint( + uncertainties_to_minimize: NDArray[np.floating], + functional: str, + model_index: int, + ) -> float: """An auxiliary function to propagate uncertainties from given uncertainties. Parameters ---------- uncertainties_to_minimize : numpy.ndarray - Values to propagate the uncertainities + Values to propagate the uncertainties functional : str Functional name to access the sensitivity for a chosen functional and propagate the uncertainties. model_index : int - Index for a model which the uncertainties are propagated - for. + Index for a model which the uncertainties are propagated for. Return ------ @@ -1203,17 +1306,17 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): Return the functional variance """ - delayed_used = ('beta' in functional) | ('lambda' in functional) + delayed_used: bool = ('beta' in functional) | ('lambda' in functional) - variance = 0 + variance: float = 0 for row in indices: mt = reactions[row] zam = zams[row] - base_diag = uncertainties[uncertainty_indices[row]:uncertainty_indices[row+1]] - new_diag = uncertainties_to_minimize[uncertainty_indices[row]:uncertainty_indices[row+1]] + base_diag: NDArray[np.floating] = uncertainties_arr[uncertainty_indices[row]:uncertainty_indices[row+1]] + new_diag: NDArray[np.floating] = uncertainties_to_minimize[uncertainty_indices[row]:uncertainty_indices[row+1]] for cov in temp_covs[row]: if ~delayed_used & ((cov.reaction_1 == 456) | (cov.reaction_2 == 456) | (cov.reaction_1 == 455) | (cov.reaction_2 == 455)): @@ -1221,7 +1324,7 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): elif delayed_used & ((cov.reaction_1 == 452) | (cov.reaction_2 == 452)): continue - symm_coef = 1 + symm_coef: int = 1 sens_1 = temp_senses[model_index][(functional, cov.zam_1, cov.reaction_1)] if (cov.zam_1 == zam) & (cov.reaction_1 == mt): @@ -1241,12 +1344,12 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): second_new = 1 symm_coef = 2 - matrix = cov.dataframe.to_numpy() - base_outer = np.outer(first_base, second_base) - cor = np.divide(matrix, base_outer, out = np.zeros_like(matrix), where = base_outer != 0 ) - new_outer = np.outer(first_new, second_new) - new_cov_mat = cor * new_outer - sandwich = sens_1.sensitivity_vector @ new_cov_mat @ sens_2.sensitivity_vector + matrix: NDArray[np.floating] = cov.dataframe.to_numpy() + base_outer: NDArray[np.floating] = np.outer(first_base, second_base) + cor: NDArray[np.floating] = np.divide(matrix, base_outer, out = np.zeros_like(matrix), where = base_outer != 0 ) + new_outer: NDArray[np.floating] = np.outer(first_new, second_new) + new_cov_mat: NDArray[np.floating] = cor * new_outer + sandwich: float = sens_1.sensitivity_vector @ new_cov_mat @ sens_2.sensitivity_vector variance += symm_coef * sandwich print(f'Model {model_index} {functional}:', np.sqrt(variance).real) @@ -1256,7 +1359,7 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): # The third part ###################### # Non-linear constraints for trust-constr - tar_constraints = [] + tar_constraints: list = [] for s, sensitivities in enumerate(model_sensitivities): for functional in tars: tar_constraint = scipy.optimize.NonlinearConstraint(lambda x, functional=functional, model_index=s: sandwich_constraint(x, functional, model_index), @@ -1269,10 +1372,10 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): bounds = scipy.optimize.Bounds(lower_boundaries, upper_boundaries) - res = scipy.optimize.minimize(lambda x, costs=costs, base_uncertainties=uncertainties: cls._weight_function(x, costs, uncertainties), - uncertainties, + res = scipy.optimize.minimize(lambda x, costs=costs_arr, base_uncertainties=uncertainties_arr: cls._weight_function(x, costs, uncertainties_arr), + uncertainties_arr, method = 'trust-constr', - jac = lambda x, costs=costs, base_uncertainties=uncertainties: cls._weight_jacobian(x, costs, uncertainties), + jac = lambda x, costs=costs_arr, base_uncertainties=uncertainties_arr: cls._weight_jacobian(x, costs, uncertainties_arr), hess = scipy.optimize.SR1(), constraints = tar_constraints, options = {'disp': True, 'maxiter': maxiter}, @@ -1284,7 +1387,7 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): ###################### # The fourth part ###################### - optimized_uncertainties = np.where(uncertainties == 0, 0, res.x) + optimized_uncertainties: NDArray[np.floating] = np.where(uncertainties_arr == 0, 0, res.x) print(optimized_uncertainties) # Completely copies the main covariances to avoid rewriting them @@ -1294,14 +1397,14 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): zam_covs = [] [zam_covs.extend(target_covariances.get_by_zam(zam)) for zam in set(zams)] - changed_covariances = [] + changed_covariances: list[tuple[int, int]] = [] for row in indices: mt = reactions[row] zam = zams[row] changed_covariances.append((zam,mt)) temp_covs_2 = [cov for cov in zam_covs if ((cov.zam_1 == zam) & (cov.reaction_1 == mt)) | ((cov.zam_2 == zam) & (cov.reaction_2 == mt))] - base_diag = uncertainties[uncertainty_indices[row]:uncertainty_indices[row+1]] + base_diag = uncertainties_arr[uncertainty_indices[row]:uncertainty_indices[row+1]] new_diag = optimized_uncertainties[uncertainty_indices[row]:uncertainty_indices[row+1]] for cov in temp_covs_2: @@ -1325,11 +1428,11 @@ def sandwich_constraint(uncertainties_to_minimize, functional, model_index): new_outer = np.outer(first_new, second_new) cov.dataframe[:] = cor * new_outer - base_cost = cls._weight_function(uncertainties, costs, uncertainties) - final_cost = cls._weight_function(optimized_uncertainties, costs, uncertainties) - cost = final_cost - base_cost + base_cost: float = cls._weight_function(uncertainties_arr, costs_arr, uncertainties_arr) + final_cost: float = cls._weight_function(optimized_uncertainties, costs_arr, uncertainties_arr) + cost: float = final_cost - base_cost - print('The total number of symmmetric covariances tweaked:', len(zam_mt)) + print('The total number of symmetric covariances tweaked:', len(zam_mt)) print('The tweaked symmetric covariances:', changed_covariances) print('The base cost function:', base_cost) print('The cost function:', final_cost) diff --git a/sauna/auxiliary.py b/sauna/auxiliary.py index 2a600c6..a9d39d3 100644 --- a/sauna/auxiliary.py +++ b/sauna/auxiliary.py @@ -1,13 +1,17 @@ +from __future__ import annotations + import pathlib import numpy as np +from numpy.typing import NDArray + -def get_text(file): +def get_text(file: str) -> str: """Extract the text from an ENDF-6 file and fix it Parameters ---------- file : str - The relative path to an ENDF-6 file to processn, e.g., + The relative path to an ENDF-6 file to process, e.g., '../NuclearData/BROND-3.1/n_5040_50-Sn-117.dat' Returns @@ -27,7 +31,8 @@ def get_text(file): return text -def fix_endf(text): + +def fix_endf(text: str) -> str: """Fix ENDF-6 files to allow pandas to parse them correctly. This does not interact with the original ENDF-6 files. They are fixed internally. @@ -56,7 +61,8 @@ def fix_endf(text): return text -def get_zam(tape): + +def get_zam(tape: "sandy.Endf6") -> int: # type: ignore[name-defined] """Get ZAM (ZA * 10 + META) of a nuclide from tape Parameters @@ -66,40 +72,43 @@ def get_zam(tape): Returns ------- - str + int ZAM of the nuclide Notes ----- - This method can is used to properly name files + This method is used to properly name files """ - # Get material the MAT number - mat = tape.mat[0] + # Get the MAT number + mat: int = tape.mat[0] # Get data from the tape - info = tape.read_section(mat, 1, 451) - meta = info["LISO"] - za = int(info["ZA"]) - zam = za * 10 + meta + info: dict = tape.read_section(mat, 1, 451) + meta: int = info["LISO"] + za: int = int(info["ZA"]) + zam: int = za * 10 + meta return zam -def process_file(tape, library, group_structure): - """Get ZAM (ZA * 10 + META) of a nuclide from tape + +def process_file( + tape: "sandy.Endf6", # type: ignore[name-defined] + library: str, + group_structure: list[float], +) -> "sandy.endf.errorr": # type: ignore[name-defined] + """Process an ENDF-6 tape through NJOY/ERRORR for a given library and group structure. Parameters ---------- tape: sandy.Endf6 An ENDF-6 tape to process - - library : str, optional + library : str The argument defines how the method interacts with peculiarities of different libraries. The special exceptions are provided for 'ENDF/B-VIII.0', 'JEFF-4T2.2', 'JEFF-4T3', and 'BROND-3.1'. There are no exceptions for other state-of-the-art libraries. - - group_structur : list, optional + group_structure : list of float Group structure to process an ENDF-6 file in eV Returns @@ -113,7 +122,7 @@ def process_file(tape, library, group_structure): """ - zam = get_zam(tape) + zam: int = get_zam(tape) # These conditions are specified otherwise NJOY does not allow processing the following files if library == 'ENDF/B-VIII.0': if zam in [922380, 922350]: @@ -178,9 +187,9 @@ def process_file(tape, library, group_structure): return errorr -def cov_to_corr(array_of_covariances): - """Generate a correlation matrix from a symmetric - covariance matrix + +def cov_to_corr(array_of_covariances: NDArray[np.floating]) -> NDArray[np.floating]: + """Generate a correlation matrix from a symmetric covariance matrix. Parameters ---------- @@ -190,55 +199,71 @@ def cov_to_corr(array_of_covariances): Return ------ numpy.ndarray - Correlation matrix from a covariance matrix + Correlation matrix derived from the covariance matrix """ - diag = np.sqrt(np.diag(array_of_covariances)) - outer = np.outer(diag, diag) - correlations = np.divide(array_of_covariances, outer, out=np.zeros_like(array_of_covariances), where=outer != 0) + diag: NDArray[np.floating] = np.sqrt(np.diag(array_of_covariances)) + outer: NDArray[np.floating] = np.outer(diag, diag) + correlations: NDArray[np.floating] = np.divide( + array_of_covariances, outer, + out=np.zeros_like(array_of_covariances), + where=outer != 0, + ) return correlations -def corr_to_cov(array_of_correlations, variances): - """Generate a covariance matrix from a symmetric - correlation matrix + +def corr_to_cov( + array_of_correlations: NDArray[np.floating], + variances: NDArray[np.floating], +) -> NDArray[np.floating]: + """Generate a covariance matrix from a symmetric correlation matrix. Parameters ---------- array_of_correlations : numpy.ndarray Correlation matrix - variances : numpy.ndarray Variances for the corresponding correlations Return ------ numpy.ndarray - Covariance matrix from a correlation matrix + Covariance matrix derived from the correlation matrix """ - diag = np.sqrt(variances) - outer_diag = np.outer(diag, diag) - covariances = array_of_correlations * outer_diag + diag: NDArray[np.floating] = np.sqrt(variances) + outer_diag: NDArray[np.floating] = np.outer(diag, diag) + covariances: NDArray[np.floating] = array_of_correlations * outer_diag return covariances -def fix_corrs(cov, eps=1e-5): + +def fix_corrs(cov: "Covariance", eps: float = 1e-5) -> NDArray[np.floating]: # type: ignore[name-defined] """Make sure that a symmetric covariance matrix does - not have non-mathematical correlations + not have non-mathematical correlations. Parameters ---------- + cov : Covariance + A symmetric Covariance instance (reaction_1 must equal reaction_2) + eps : float, optional + Tolerance for clipping correlations outside [-1, 1]. Default is 1e-5. Returns ------- numpy.ndarray - Returns fixed symmetric covariance matrix + Fixed symmetric covariance matrix as a numpy array + + Raises + ------ + ValueError + If a non-symmetric (cross-reaction) covariance is provided. """ if cov.reaction_1 == cov.reaction_2: - array_of_covs = cov.dataframe.to_numpy() + array_of_covs: NDArray[np.floating] = cov.dataframe.to_numpy() - corr = cov_to_corr(array_of_covs) + corr: NDArray[np.floating] = cov_to_corr(array_of_covs) corr = np.where(corr > 1+eps, 1, corr) corr = np.where(corr < -1-eps, -1, corr) @@ -248,13 +273,41 @@ def fix_corrs(cov, eps=1e-5): return cov else: raise ValueError(f'Non-symmetric matrix is provided.') - -def get_negative(list): + + +def get_negative(list: list[float]) -> int | None: + """Return the index of the first negative value in a list. + + Parameters + ---------- + list : list of float + Input list to search for a negative value + + Returns + ------- + int or None + Index of the first negative element, or None if none found + + """ for i in range(len(list)): if list[i] < 0: return i -def get_complex(list): + +def get_complex(list: list[complex]) -> int | None: + """Return the index of the first complex value in a list. + + Parameters + ---------- + list : list of complex + Input list to search for a complex value + + Returns + ------- + int or None + Index of the first complex element, or None if none found + + """ for i in range(len(list)): if np.iscomplex(list[i]): - return i + return i \ No newline at end of file diff --git a/sauna/covariance.py b/sauna/covariance.py index ac1f69c..d613523 100644 --- a/sauna/covariance.py +++ b/sauna/covariance.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sandy import os import pandas as pd @@ -5,6 +7,7 @@ import math import multiprocessing import pathlib +from numpy.typing import NDArray from .auxiliary import * @@ -54,28 +57,28 @@ class Covariances(): """ - def __init__(self, library=''): + def __init__(self, library: str = '') -> None: self.library = library self._group_structure = [] self._covariances = [] - def __repr__(self): + def __repr__(self) -> str: return (f"{self.__class__.__name__}({self.library!r}, {(len(self.group_structure)-1)!r})") @property - def library(self): + def library(self) -> str: return self._library @library.setter - def library(self, library): + def library(self, library: str) -> None: self._library = library @property - def group_structure(self): + def group_structure(self) -> NDArray[np.floating]: return self._group_structure @group_structure.setter - def group_structure(self, group_structure): + def group_structure(self, group_structure: list[float] | NDArray[np.floating]) -> None: group_number = len(group_structure) if group_number >=2 & group_number <= 1501: self._group_structure = np.array(group_structure) @@ -83,20 +86,20 @@ def group_structure(self, group_structure): raise ValueError(f'The number of group must be between 1 and 1500, but {group_number-1} is provided') @property - def covariances(self): + def covariances(self) -> list[Covariance]: return self._covariances @covariances.setter - def covariances(self, covariances): + def covariances(self, covariances: list[Covariance]) -> None: self._covariances = covariances - def append(self, covariance): + def append(self, covariance: Covariance) -> None: self._covariances.append(covariance) - def extend(self, covariances): + def extend(self, covariances: list[Covariance]) -> None: self._covariances.extend(covariances) - def from_endf(self, file): + def from_endf(self, file: str) -> list[Covariance]: """Generate covariance matrices from an ENDF-6 file. Parameters @@ -259,7 +262,7 @@ def from_endf(self, file): return covs_in_file - def from_endfs(self, folder, extension='.dat', parallel = True): + def from_endfs(self, folder: str, extension: str = '.dat', parallel: bool = True) -> None: """Generate covariance matrices from ENDF-6 files in a folder. Parameters @@ -295,7 +298,7 @@ def from_endfs(self, folder, extension='.dat', parallel = True): print(f'The {self.library} library has been processed.') print("-------------------------------") - def from_abbn(self, file): + def from_abbn(self, file: str) -> None: """Generate covariance matrices from an ABBN file. It assumes each file contains data about only one nuclide or element. @@ -460,7 +463,7 @@ def from_abbn(self, file): covariance.mf = 33 self.covariances.append(covariance) - def from_abbns(self, folder, extension='.TAB'): + def from_abbns(self, folder: str, extension: str = '.TAB') -> None: """Import covariance matrices from ABBN files in a folder to the Covariances instances. @@ -484,7 +487,7 @@ def from_abbns(self, folder, extension='.TAB'): print('The covariances have been imported successfully.') - def from_excel(self, file): + def from_excel(self, file: str) -> None: """Import a covariance matrix from an .xlsx file to the Covariances instance. @@ -519,7 +522,7 @@ def from_excel(self, file): self.covariances.append(covariance) - def from_excels(self, folder): + def from_excels(self, folder: str) -> None: """Import covariance matrices from .xlsx files in a folder to the Covariances instances. @@ -541,7 +544,7 @@ def from_excels(self, folder): print('The covariances have been imported successfully.') - def from_commara(self, file): + def from_commara(self, file: str) -> None: """Import covariance matrices from a COMMARA file to the Covariances instances. @@ -748,7 +751,7 @@ def from_commara(self, file): self.covariances.append(covariance) - def from_coverx(self, ampxcovconverter, file): + def from_coverx(self, ampxcovconverter: str, file: str) -> None: """Import covariance matrices from a COVERX (the format of AMPX of the SCALE code system) file to the Covariances instances. @@ -779,7 +782,7 @@ def from_coverx(self, ampxcovconverter, file): lines = open(f'{file}.toc', "r").readlines() - def scaleid_to_zam(scale_id): + def scaleid_to_zam(scale_id: int) -> int: """Translate a SCALE ID to the corresponding ZAM value. @@ -914,7 +917,7 @@ def scaleid_to_zam(scale_id): os.remove(f'{file}.new.toc') os.remove(f'{file}.toc') - def check_eigenvalues(self, eps=1e-5): + def check_eigenvalues(self, eps: float = 1e-5) -> None: """Check the mathermatical corectness of the covariance matrices whether it is positive semidefinite. The method provides an output as a number of incorrect covariances @@ -982,7 +985,7 @@ def check_eigenvalues(self, eps=1e-5): print('-------------------------------------------') - def check_corrs(self, eps = 1e-5): + def check_corrs(self, eps: float = 1e-5) -> None: """Check whether the correlation matrices are correct or not, i.e. the values are between -1 and 1. The method provides an output as a number of incorrect correlation @@ -1018,7 +1021,7 @@ def check_corrs(self, eps = 1e-5): print(f'The number of incorrect correlation matrices is {number_of_corr} of {number_of_symmetric} symmetric matrices among a total number of {len(self.covariances)} matrices') print('-------------------------------------------') - def export_corrs(self, save_to='./correlations/', fix_corr = False, eps = 1e-5): + def export_corrs(self, save_to: str = './correlations/', fix_corr: bool = False, eps: float = 1e-5) -> None: """Auxiliary method to export the correlation matrices based upon the covariance matrices. @@ -1053,7 +1056,7 @@ def export_corrs(self, save_to='./correlations/', fix_corr = False, eps = 1e-5): print('The correlations have been exported successfully.') print('-------------------------------------------------') - def limit_covs(self): + def limit_covs(self) -> None: """Limit variances and covariances to 100% to avoid unphysical or overestimated values. It assumes that there is no change in correlations while changing the variances @@ -1098,7 +1101,7 @@ def limit_covs(self): print(f'The number of matrices with over 100% values is {number_of_matrices} of a total number of {len(self.covariances)} matrices.') print('-------------------------------------------') - def get_by_zam(self, zam): + def get_by_zam(self, zam: int) -> NDArray: """Get a list of Covariance instances by a ZAM value from a Covariances instance. @@ -1125,7 +1128,7 @@ def get_by_zam(self, zam): return array - def get_by_reaction(self, mt): + def get_by_reaction(self, mt: int) -> NDArray: """Get a list of Covariance instances by a MT value from a Covariances instance. @@ -1149,7 +1152,7 @@ def get_by_reaction(self, mt): return array - def get_by_params(self, zam_1, zam_2, reaction_1, reaction_2): + def get_by_params(self, zam_1: int, zam_2: int, reaction_1: int, reaction_2: int) -> Covariance: """Get a Covariance instance by the ZAM, Reaction 1 MT number, and Reaction 2 MT numbers from a Covariances instance. @@ -1172,7 +1175,7 @@ def get_by_params(self, zam_1, zam_2, reaction_1, reaction_2): return next(cov for cov in self.covariances if (cov.zam_1 == zam_1) & (cov.zam_2 == zam_2) & (cov.reaction_1 == reaction_1) & (cov.reaction_2 == reaction_2)) - def to_excels(self, save_to='./covariances/', fix_corr = False, eps = 1e-5): + def to_excels(self, save_to: str = './covariances/', fix_corr: bool = False, eps: float = 1e-5) -> None: """Export covariances in the Coviariances instance to Excel files. @@ -1241,88 +1244,88 @@ class Covariance(): """ - def __init__(self): - self._library = None - self._mat = None - self._zam_1 = None - self._zam_2 = None - self._file = None - self._reaction_1 = None - self._reaction_2 = None - self._group_structure = None - self._dataframe = None - - def __repr__(self): + def __init__(self) -> None: + self._library: str | None = None + self._mat: int | None = None + self._zam_1: int | None = None + self._zam_2: int | None = None + self._file: str | None = None + self._reaction_1: int | None = None + self._reaction_2: int | None = None + self._group_structure: NDArray[np.floating] | None = None + self._dataframe: pd.DataFrame | None = None + + def __repr__(self) -> str: return (f"{self.__class__.__name__}({self.zam_1!r}-{self.zam_2!r}, {self.reaction_1!r}-{self.reaction_2!r}, {(len(self.group_structure)-1)!r})") @property - def library(self): + def library(self) -> str | None: return self._library @library.setter - def library(self, library): + def library(self, library: str) -> None: self._library = library @property - def mat(self): + def mat(self) -> int | None: return self._mat @mat.setter - def mat(self, mat): + def mat(self, mat: int) -> None: self._mat = mat @property - def zam_1(self): + def zam_1(self) -> int | None: return self._zam_1 @property - def zam_2(self): + def zam_2(self) -> int | None: return self._zam_2 @zam_1.setter - def zam_1(self, zam_1): + def zam_1(self, zam_1: int) -> None: self._zam_1 = zam_1 @zam_2.setter - def zam_2(self, zam_2): + def zam_2(self, zam_2: int) -> None: self._zam_2 = zam_2 @property - def mf(self): + def mf(self) -> int | None: return self._mf @mf.setter - def mf(self, mf): + def mf(self, mf: int) -> None: self._mf = mf @property - def reaction_1(self): + def reaction_1(self) -> int | None: return self._reaction_1 @reaction_1.setter - def reaction_1(self, reaction_1): + def reaction_1(self, reaction_1: int) -> None: self._reaction_1 = reaction_1 @property - def reaction_2(self): + def reaction_2(self) -> int | None: return self._reaction_2 @reaction_2.setter - def reaction_2(self, reaction_2): + def reaction_2(self, reaction_2: int) -> None: self._reaction_2 = reaction_2 @property - def group_structure(self): + def group_structure(self) -> NDArray[np.floating] | None: return self._group_structure @group_structure.setter - def group_structure(self, group_structure): + def group_structure(self, group_structure: list[float] | NDArray[np.floating]) -> None: self._group_structure = group_structure @property - def dataframe(self): + def dataframe(self) -> pd.DataFrame | None: return self._dataframe @dataframe.setter - def dataframe(self, dataframe): - self._dataframe = dataframe + def dataframe(self, dataframe: pd.DataFrame) -> None: + self._dataframe = dataframe \ No newline at end of file diff --git a/sauna/sensitivity.py b/sauna/sensitivity.py index c257101..397834a 100644 --- a/sauna/sensitivity.py +++ b/sauna/sensitivity.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import numpy as np import pandas as pd import os +from numpy.typing import NDArray from .bridge import Serpent, SCALE @@ -76,21 +79,21 @@ class Sensitivities(): """ - def __init__(self): - self._group_structure = [] - self._sensitivities = [] - self._functionals = [] - self._zams = [] + def __init__(self) -> None: + self._group_structure: list[float] = [] + self._sensitivities: list[Sensitivity] = [] + self._functionals: list[str] = [] + self._zams: list[int] = [] - def __repr__(self): + def __repr__(self) -> str: return (f"{self.__class__.__name__}({len(self.functionals)!r}, {len(self.zams)!r}, {len(self.sensitivities)!r}, {(len(self.group_structure)-1)!r})") @property - def group_structure(self): + def group_structure(self) -> list[float]: return self._group_structure @group_structure.setter - def group_structure(self, group_structure): + def group_structure(self, group_structure: list[float] | NDArray[np.floating]) -> None: group_number = len(group_structure) if group_number >=2 & group_number <= 1501: self._group_structure = group_structure @@ -99,29 +102,29 @@ def group_structure(self, group_structure): but {group_number-1} is provided') @property - def sensitivities(self): + def sensitivities(self) -> list[Sensitivity]: return self._sensitivities @sensitivities.setter - def sensitivities(self, sensitivities): + def sensitivities(self, sensitivities: list[Sensitivity]) -> None: self._sensitivities = sensitivities @property - def functionals(self): + def functionals(self) -> list[str]: return self._functionals @property - def zams(self): + def zams(self) -> list[int]: return self._zams - def append(self, sensitivity): + def append(self, sensitivity: Sensitivity) -> None: self._sensitivities.append(sensitivity) if sensitivity.functional not in self.functionals: self._functionals.append(sensitivity.functional) if sensitivity.zam not in self.zams: self._zams.append(sensitivity.zam) - def extend(self, sensitivities): + def extend(self, sensitivities: list[Sensitivity]) -> None: self._sensitivities.extend(sensitivities) for sens in sensitivities: if sens.functional not in self.functionals: @@ -130,7 +133,7 @@ def extend(self, sensitivities): self._zams.append(sens.zam) @classmethod - def reactivity_difference(cls, sensitivities_nom, sensitivities_pert, keff_nom, keff_pert, dkeff_nom = 0, dkeff_pert = 0, functional = 'Reactivity-difference'): + def reactivity_difference(cls, sensitivities_nom: Sensitivities, sensitivities_pert: Sensitivities, keff_nom: float, keff_pert: float, dkeff_nom: float = 0, dkeff_pert: float = 0, functional: str = 'Reactivity-difference') -> Sensitivities: """Calculate senitivity of a reactivity difference to quantify the influence of the inputs on the reactivity effects. @@ -257,7 +260,7 @@ def reactivity_difference(cls, sensitivities_nom, sensitivities_pert, keff_nom, return reactivity_sensitivities @classmethod - def beta_eff(cls, sensitivities_nom, sensitivities_prompt, keff_nom, keff_prompt, functional = 'beta-eff'): + def beta_eff(cls, sensitivities_nom: Sensitivities, sensitivities_prompt: Sensitivities, keff_nom: float, keff_prompt: float, functional: str = 'beta-eff') -> Sensitivities: """Calculate senitivity of an effective delayed neutron fraction via the nominal eigenvalue and prompt eigenvalue @@ -307,7 +310,7 @@ def beta_eff(cls, sensitivities_nom, sensitivities_prompt, keff_nom, keff_prompt return sensitivities_b @classmethod - def ratio(cls, sensitivities_num, sensitivities_denom, functional = 'Ratio'): + def ratio(cls, sensitivities_num: Sensitivities, sensitivities_denom: Sensitivities, functional: str = 'Ratio') -> Sensitivities: """Calculate senitivity of a ratio of two arbitrary sensitivities. For instnace, it can be used to calculate the effective neutron generation time via the effective life-time and the eigenvalue @@ -354,7 +357,7 @@ def ratio(cls, sensitivities_num, sensitivities_denom, functional = 'Ratio'): return sensitivities_r @classmethod - def promt_decay(cls, sensitivities_prompt, sensitivities_l, k_prompt, functional = 'alpha'): + def promt_decay(cls, sensitivities_prompt: Sensitivities, sensitivities_l: Sensitivities, k_prompt: float, functional: str = 'alpha') -> Sensitivities: """Calculate senitivity of a prompt decay constant also known as Rossi-alpha or prompt alpha eigenvalue via the sensitivities of the prompt eigenvalue and effective neutron life-time. @@ -404,7 +407,7 @@ def promt_decay(cls, sensitivities_prompt, sensitivities_l, k_prompt, functional return sensitivities_a @classmethod - def breeding_ratio(cls, sensitivities_gamma, sensitivities_fission, R_gamma, R_fission, discard_scattering = False, functional = 'Breeding Ratio'): + def breeding_ratio(cls, sensitivities_gamma: Sensitivities, sensitivities_fission: Sensitivities, R_gamma: float, R_fission: float, discard_scattering: bool = False, functional: str = 'Breeding Ratio') -> Sensitivities: """Calculate senitivity of a breeding ratio via the sensitivities of the fissile-gamma/fertile-gamma ratio and the fissile-fission/ fertile-gamma ratio. The method is intented to avoid the limitation @@ -465,7 +468,7 @@ def breeding_ratio(cls, sensitivities_gamma, sensitivities_fission, R_gamma, R_f return br_sensitivities - def from_serpent(self, file): + def from_serpent(self, file: str) -> None: """Import sensitivity data of all the reactions from a Serpent output file (_sens0.m) to the Sensitivities instance. @@ -490,7 +493,7 @@ def from_serpent(self, file): sensitivity.uncertainty = sens.uncertainty self.append(sensitivity) - def from_scale(self, file, type='B', functional = 'Eigenvalue'): + def from_scale(self, file: str, type: str = 'B', functional: str = 'Eigenvalue') -> None: """Import sensitivities from an SDF (Sensitivity Data File) file of SCALE to the Sensitivities instance. @@ -534,7 +537,7 @@ def from_scale(self, file, type='B', functional = 'Eigenvalue'): print('The data have been imported successfully.') - def get_by_functional(self, functional): + def get_by_functional(self, functional: str) -> NDArray: """Get a list of Sensitivity instances by a functional from a Sensetivities instance. @@ -551,7 +554,7 @@ def get_by_functional(self, functional): return np.array([sensitivity for sensitivity in self.sensitivities if sensitivity.functional == functional]) - def get_by_zam(self, zam): + def get_by_zam(self, zam: int) -> list[Sensitivity]: """Get a list of Sensitivity instances by a ZAM value from a Sensetivities instance. @@ -569,7 +572,7 @@ def get_by_zam(self, zam): return [sensitivity for sensitivity in self.sensitivities if sensitivity.zam == zam] - def get_by_reaction(self, reaction): + def get_by_reaction(self, reaction: int) -> list[Sensitivity]: """Get a list of Sensitivity instances by an MT number from a Sensetivities instance. @@ -587,7 +590,7 @@ def get_by_reaction(self, reaction): return [sensitivity for sensitivity in self.sensitivities if sensitivity.reaction == reaction] - def get_by_params(self, functional, zam, reaction): + def get_by_params(self, functional: str, zam: int, reaction: int) -> Sensitivity: """Get a list of Sensitivity instances by a functional, ZAM, reaction from a Sensetivities instance. @@ -623,7 +626,7 @@ def get_by_params(self, functional, zam, reaction): return sensitivity - def to_dataframe(self, functional, sort=True): + def to_dataframe(self, functional: str, sort: bool = True) -> pd.DataFrame: """Export sensitivity data of all the relative sensitivities from a Sensitivities instance to a dataframe. It generates diffirent sheet for given functional. @@ -659,7 +662,7 @@ def to_dataframe(self, functional, sort=True): return sensitivity_df - def to_excel(self, name='Sensitivity.xlsx', sort = True): + def to_excel(self, name: str = 'Sensitivity.xlsx', sort: bool = True) -> None: """Export sensitivity data of all the integral relative sensitivities from a Sensitivities instance. Generates diffirent sheet for each functional. @@ -712,65 +715,65 @@ class Sensitivity(): """ - def __init__(self): - self._functional = None - self._zam = None - self._reaction = None - self._sensitivity = None - self._uncertainty = None - self._group_structure = [] - self._sensitivity_vector = [] - self._uncertainties = None - - def __repr__(self): + def __init__(self) -> None: + self._functional: str | None = None + self._zam: int | None = None + self._reaction: int | None = None + self._sensitivity: float | None = None + self._uncertainty: float | None = None + self._group_structure: list[float] = [] + self._sensitivity_vector: NDArray[np.floating] = np.array([]) + self._uncertainties: NDArray[np.floating] | None = None + + def __repr__(self) -> str: return (f"{self.__class__.__name__}({self.functional!r}, {self.zam!r}, {self.reaction!r}, {(len(self.group_structure)-1)!r})") @property - def functional(self): + def functional(self) -> str | None: return self._functional @functional.setter - def functional(self, functional): + def functional(self, functional: str) -> None: self._functional = functional @property - def zam(self): + def zam(self) -> int | None: return self._zam @zam.setter - def zam(self, zam): + def zam(self, zam: int) -> None: self._zam = zam @property - def reaction(self): + def reaction(self) -> int | None: return self._reaction @reaction.setter - def reaction(self, reaction): + def reaction(self, reaction: int) -> None: self._reaction = reaction @property - def sensitivity(self): + def sensitivity(self) -> float | None: return self._sensitivity @sensitivity.setter - def sensitivity(self, sensitivity): + def sensitivity(self, sensitivity: float) -> None: self._sensitivity = sensitivity @property - def uncertainty(self): + def uncertainty(self) -> float | None: return self._uncertainty @uncertainty.setter - def uncertainty(self, uncertainty): + def uncertainty(self, uncertainty: float) -> None: self._uncertainty = uncertainty @property - def group_structure(self): + def group_structure(self) -> list[float]: return self._group_structure @group_structure.setter - def group_structure(self, group_structure): + def group_structure(self, group_structure: list[float] | NDArray[np.floating]) -> None: group_number = len(group_structure) if group_number >=2 & group_number <= 1501: self._group_structure = group_structure @@ -779,22 +782,22 @@ def group_structure(self, group_structure): but {group_number-1} is provided') @property - def sensitivity_vector(self): + def sensitivity_vector(self) -> NDArray[np.floating]: return self._sensitivity_vector @sensitivity_vector.setter - def sensitivity_vector(self, sensitivity_vector): + def sensitivity_vector(self, sensitivity_vector: list[float] | NDArray[np.floating]) -> None: self._sensitivity_vector = np.array(sensitivity_vector) @property - def uncertainty_vector(self): + def uncertainty_vector(self) -> NDArray[np.floating]: return self._uncertainty_vector @uncertainty_vector.setter - def uncertainty_vector(self, uncertainty_vector): + def uncertainty_vector(self, uncertainty_vector: list[float] | NDArray[np.floating]) -> None: self._uncertainty_vector = np.array(uncertainty_vector) - def from_serpent(self, file): + def from_serpent(self, file: str) -> Sensitivity: """Export sensitivity data of a single reaction from a Serpent output file (_sens0.m) to the Sensitivity instance. @@ -817,7 +820,7 @@ def from_serpent(self, file): return next(sensitivity for sensitivity in sensitivities if (sensitivity.functional == self.functional) & (sensitivity.zam == self.zam) & (sensitivity.reaction == self.reaction)) - def from_scale(self, file): + def from_scale(self, file: str) -> Sensitivity: """Export sensitivity data of a single reaction from a SCALE output file (.sdf) to the Sensitivity instance. @@ -838,4 +841,4 @@ def from_scale(self, file): sensitivities = Serpent.read(file) - return next(sensitivity for sensitivity in sensitivities if (sensitivity.functional == self.functional) & (sensitivity.zam == self.zam) & (sensitivity.reaction == self.reaction)) + return next(sensitivity for sensitivity in sensitivities if (sensitivity.functional == self.functional) & (sensitivity.zam == self.zam) & (sensitivity.reaction == self.reaction)) \ No newline at end of file