def iso_combo_plot(bet_results, mask_results, save_file=True): """Creates an image displaying the relative pressure range with minimum error and the BET isotherm on the same plot. The point where n/nm = 1 is is the point where the BET monolayer loading occurs. Parameters ---------- bet_results : named tuple The bet_results.iso_df element is used to create a plot of isotherm data. mask_results : named tuple The mask_results.mask element is used to mask the BET results so that only valid results are displayed. save_file : boolean When save_file = True a png of the figure is created in the working directory. Returns ------- """ mask = mask_results.mask if mask.all(): logging.warning( "No valid relative pressure ranges. BET isotherm combo plot not created." ) return df = bet_results.iso_df nm = np.ma.array(bet_results.nm, mask=mask) c = np.ma.array(bet_results.c, mask=mask) err = np.ma.array(bet_results.err, mask=mask) err_max, err_max_idx, err_min, err_min_idx = util.max_min(err) c_min_err = c[err_min_idx[0], err_min_idx[1]] nnm_min = nm[err_min_idx[0], err_min_idx[1]] ppo = np.arange(0, 0.9001, 0.001) synth_min = 1 / (1 - ppo) - 1 / (1 + (c_min_err - 1) * ppo) expnnm_min = df.n / nnm_min err_min_i = int(err_min_idx[0] + 1) err_min_j = int(err_min_idx[1]) expnnm_min_used = expnnm_min[err_min_j:err_min_i] ppo_expnnm_min_used = df.relp[err_min_j:err_min_i] f, ax1 = plt.subplots(1, 1, figsize=(10, 10)) ax1.set_title("BET Isotherm and Experimental data") ax1.set_ylim(0, synth_min[-2] + 1) ax1.set_xlim(0, 1) ax1.set_ylabel("n/nm") ax1.set_xlabel("P/Po") ax1.grid(b=True, which="major", color="gray", linestyle="-") ax1.plot( ppo, synth_min, linestyle="-", linewidth=1, c="black", label="Theoretical isotherm", marker="", ) ax1.plot( ppo_expnnm_min_used, expnnm_min_used, c="gray", label="Experimental isotherm - used data", marker="o", linewidth=0, ) ax1.plot( df.relp, expnnm_min, c="grey", fillstyle="none", label="Experimental isotherm", marker="o", linewidth=0, ) ax1.plot([0, 1], [1, 1], c="grey", linestyle="--", linewidth=1, marker="") ax1.legend(loc="upper left", framealpha=1) if save_file is True: f.savefig("isothermcomp_%s.png" % (bet_results.info), bbox_inches="tight") logging.info( "Experimental and theoretical isotherm plot saved as:\ isothermcomp_%s.png" % (bet_results.info) ) return
def ssa_heatmap(bet_results, mask_results, save_file=True, gradient="Greens"): """Creates a heatmap of specific surface areas. Shading corresponds to specific surface area, normalized for the minimum and maximum specific sa values. Parameters ---------- bet_results : namedtuple The bet_results.ssa element is used to create a heatmap of specific surface area answers. mask_results : namedtuple The mask_results.mask element is used to mask the specific surface area heatmap so that only valid results are displayed. save_file : boolean When save_file = True a png of the figure is created in the working directory. gradient : string Color gradient for heatmap, must be a vaild color gradient name in the seaborn package. Returns ------- """ mask = mask_results.mask if mask.all(): logging.warning( "No valid relative pressure ranges. Specific surface area" " heatmap not created." ) return df = bet_results.iso_df # creating a masked array of ssa values ssa = np.ma.array(bet_results.ssa, mask=mask) # finding max and min sa to normalize heatmap colours ssamax, ssa_max_idx, ssamin, ssa_min_idx = util.max_min(ssa) hm_labels = round(df.relp * 100, 1) fig, (ax) = plt.subplots(1, 1, figsize=(13, 13)) sns.heatmap( ssa, vmin=ssamin, vmax=ssamax, square=True, cmap=gradient, mask=(ssa == 0), xticklabels=hm_labels, yticklabels=hm_labels, linewidths=1, linecolor="w", cbar_kws={"shrink": 0.78, "aspect": len(df.relp)}, ) ax.invert_yaxis() ax.set_title( "Specific Surface Area m^2/g" ) plt.xticks(rotation=45, horizontalalignment="right") plt.xlabel("Start Relative Pressure") plt.yticks(rotation=45, horizontalalignment="right") plt.ylabel("End Relative Pressure") if save_file is True: fig.savefig("ssa_heatmap_%s.png" % (bet_results.info), bbox_inches="tight") logging.info( "Specific surface area heatmap saved as: ssa_heatmap_%s.png" % (bet_results.info) ) return
def bet_combo_plot(bet_results, mask_results, save_file=True): """Creates a BET plots for the minimum and maxium error data sets. Only datapoints in the minimum and maximum error data sets are plotted. Equation for best fit line and corresponding R value are annotated on plot. Parameters ---------- bet_results : namedtuple Namedtuple where the bet_results.iso_df element is used to create a plot of isotherm BET values. mask_results : namedtuple The mask_results.mask element is used to mask the BET results so that only valid results are displayed. save_file : boolean When save_file = True a png of the figure is created in the working directory. Returns ------- """ mask = mask_results.mask if mask.all(): logging.warning( "No valid relative pressure ranges. BET combo plot not created." ) return df = bet_results.iso_df err = np.ma.array(bet_results.err, mask=mask) err_max, err_max_idx, err_min, err_min_idx = util.max_min(err) min_start = int(err_min_idx[1]) min_stop = int(err_min_idx[0]) max_start = int(err_max_idx[1]) max_stop = int(err_max_idx[0]) slope, intercept, r_val, p_value, std_err = sp.stats.linregress( df.relp[min_start : min_stop + 1], df.bet[min_start : min_stop + 1] ) min_liney = np.zeros(2) min_liney[0] = slope * (df.relp[min_start] - 0.01) + intercept min_liney[1] = slope * (df.relp[min_stop] + 0.01) + intercept min_linex = np.zeros(2) min_linex[0] = df.relp[min_start] - 0.01 min_linex[1] = df.relp[min_stop] + 0.01 ( slope_max, intercept_max, r_value_max, p_value_max, std_err_max, ) = sp.stats.linregress( df.relp[max_start : max_stop + 1], df.bet[max_start : max_stop + 1] ) max_liney = np.zeros(2) max_liney[0] = slope_max * (df.relp[max_start] - 0.01) + intercept_max max_liney[1] = slope_max * (df.relp[max_stop] + 0.01) + intercept_max max_linex = np.zeros(2) max_linex[0] = df.relp[max_start] - 0.01 max_linex[1] = df.relp[max_stop] + 0.01 figure, ax1 = plt.subplots(1, figsize=(10, 10)) ax1.set_title("BET Plot") ax1.set_xlim(0, max(min_linex[1], max_linex[1]) * 1.1) ax1.set_ylabel("1/[n(P/Po-1)]") ax1.set_ylim(0, max(min_liney[1] * 1.1, max_liney[1] * 1.1)) ax1.set_xlabel("P/Po") ax1.grid(b=True, which="major", color="gray", linestyle="-") ax1.plot( df.relp[min_start : min_stop + 1], df.bet[min_start : min_stop + 1], label="min error (exp. data)", c="grey", marker="o", linewidth=0, fillstyle="none", ) ax1.plot(min_linex, min_liney, color="black", label="min error (linear regression)") ax1.plot( df.relp[max_start : max_stop + 1], df.bet[max_start : max_stop + 1], label="Max Error Experimental Data", c="grey", marker="x", linewidth=0, ) ax1.plot( max_linex, max_liney, color="black", linestyle="--", label="max error (linear regression)", ) ax1.legend(loc="upper left", framealpha=1) ax1.annotate( "min error (linear regression): \nm = %.3f \nb = %.3f \nR = \ %.3f \n\nmax error (linear regression): \nm = %.3f \nb = %.3f \ \nR = %.3f" % (slope, intercept, r_val, slope_max, intercept_max, r_value_max), bbox=dict(boxstyle="round", fc="white", ec="gray", alpha=1), textcoords="axes fraction", xytext=(0.695, 0.017), xy=(df.relp[min_stop], df.bet[min_start]), size=11, ) if save_file is True: figure.savefig("betplot_%s.png" % (bet_results.info), bbox_inches="tight") logging.info("BET plot saved as: betplot_%s.png" % (bet_results.info)) return
def err_heatmap(bet_results, mask_results, save_file=True, gradient="Greys"): """Creates a heatmap of error values. Shading corresponds to average error between experimental data and the the theoretical BET isotherm, normalized so that, with default shading, 0 is displayed as white and the maximum error value is black. Parameters ---------- bet_results : namedtuple The bet_results.err element is used to create a heatmap of error values. mask_results : namedtuple The mask_results.mask element is used to mask the error heatmap so that only valid results are displayed. save_file : boolean When save_file = True a png of the figure is created in the working directory. gradient : string Color gradient for heatmap, must be a vaild color gradient name in the seaborn package, default is grey. Returns ------- """ mask = mask_results.mask if mask.all(): logging.warning( "No valid relative pressure ranges. Error heat map not created." ) return df = bet_results.iso_df # creating a masked array of error values err = np.ma.array(bet_results.err, mask=mask) errormax, error_max_idx, errormin, error_min_idx = util.max_min(err) hm_labels = round(df.relp * 100, 1) fig, (ax) = plt.subplots(1, 1, figsize=(13, 13)) sns.heatmap( err, vmin=0, vmax=errormax, square=True, cmap=gradient, mask=(err == 0), xticklabels=hm_labels, yticklabels=hm_labels, linewidths=1, linecolor="w", cbar_kws={"shrink": 0.78, "aspect": len(df.relp)}, ) ax.invert_yaxis() ax.set_title( "Average Error per Point Between Experimental and" " Theoretical Isotherms" ) plt.xticks(rotation=45, horizontalalignment="right") plt.xlabel("Start Relative Pressure") plt.yticks(rotation=45, horizontalalignment="right") plt.ylabel("End Relative Pressure") if save_file is True: fig.savefig("error_heatmap_%s.png" % (bet_results.info), bbox_inches="tight") logging.info("Error heatmap saved as: error_heatmap_%s.png" % (bet_results.info)) return
def ascii_tables(bet_results, mask_results): """Creates and prints ASCII formatted tables of BET results. Parameters ---------- bet_results : namedtuple Contains elements that result from BET analysis. Relevant fields are: - ``bet_results.iso_df`` (DataFrame) : experimental isotherm data. - ``bet_results.ssa`` (ndarray) : specific surface areas for all relp ranges. - ``bet_results.c`` (ndarray) : BET constants for all relp ranges. - ``bet_results.err`` (ndarray) : error values for all relp ranges. mask_results : namedtuple Contains the results of applying the Rouquerol criteria to BET results. Relevant fields are: - ``mask_results.mask`` (MaskedArray) : object where invalid BET results are masked. Returns ------- table : prettytable Summary of BET results, highlighting the high, low, and average values of specific surface area. ASCII formatted table. table2 : prettytable Summary of BET results, highlighting the high, low, and average values of the BET constant. ASCII formatted table. ssa_std : float Atandard deviation of valid specific surface area values. c_std : float Standard deviation of valid BET constant values. """ mask = mask_results.mask if mask.all(): msg = "No valid relative pressure ranges. ASCII tables not created." logging.warning(msg) return df = bet_results.iso_df ssa = np.ma.array(bet_results.ssa, mask=mask) c = np.ma.array(bet_results.c, mask=mask) err = np.ma.array(bet_results.err, mask=mask) ssamax, ssa_max_idx, ssamin, ssa_min_idx = util.max_min(ssa) cmax, c_max_idx, cmin, c_min_idx = util.max_min(c) ssamean = np.ma.mean(ssa) ssamedian = np.ma.median(ssa) cmean = np.ma.mean(c) cmedian = np.ma.median(c) ssa_std = np.nan_to_num(ssa)[ssa != 0].std() c_std = np.nan_to_num(c)[c != 0].std() err_max, err_max_idx, err_min, err_min_idx = util.max_min(err) cmax_err = float(c[err_max_idx[0], err_max_idx[1]]) cmin_err = float(c[err_min_idx[0], err_min_idx[1]]) # these are just variables to print in tables ssa_min = round(ssamin, 3) ssa_min_c = round(float(c[ssa_min_idx[0], ssa_min_idx[1]]), 3) ssa_min_start_ppo = round(float(df.relp[ssa_min_idx[1]]), 3) ssa_min_end_ppo = round(float(df.relp[ssa_min_idx[0]]), 3) ssa_max = round(ssamax, 3) ssa_max_c = round(float(c[ssa_max_idx[0], ssa_max_idx[1]]), 3) ssa_max_start_ppo = round(float(df.relp[ssa_max_idx[1]]), 3) ssa_max_end_ppo = round(float(df.relp[ssa_max_idx[0]]), 3) ssa_mean = round(ssamean, 3) ssa_median = round(ssamedian, 3) c_min = round(cmin, 3) c_min_sa = round(float(ssa[c_min_idx[0], c_min_idx[1]]), 3) c_min_start_ppo = round(float(df.relp[c_min_idx[1]]), 3) c_min_end_ppo = round(float(df.relp[c_min_idx[0]]), 3) c_min_err = round(float(err[c_min_idx[0], c_min_idx[1]]), 3) c_max = round(cmax, 3) c_max_sa = round(float(ssa[c_max_idx[0], c_max_idx[1]]), 3) c_max_start_ppo = round(float(df.relp[c_max_idx[1]]), 3) c_max_end_ppo = round(float(df.relp[c_max_idx[0]]), 3) c_max_err = round(float(err[c_max_idx[0], c_max_idx[1]]), 3) c_mean = round(cmean, 3) c_median = round(cmedian, 3) cmin_err = round(cmin_err, 3) c_min_err_sa = round(float(ssa[err_min_idx[0], err_min_idx[1]]), 3) c_min_err_start_ppo = round(float(df.relp[err_min_idx[1]]), 3) c_min_err_end_ppo = round(float(df.relp[err_min_idx[0]]), 3) err_min = round(err_min, 3) cmax_err = round(cmax_err, 3) c_max_err_sa = round(float(ssa[err_max_idx[0], err_max_idx[1]]), 3) c_max_err_start_ppo = round(float(df.relp[err_max_idx[1]]), 3) c_max_err_end_ppo = round(float(df.relp[err_max_idx[0]]), 3) err_max = round(err_max, 3) table = PrettyTable() table.field_names = ["", "Spec SA m2/g", "C", "Start P/Po", "End P/Po"] table.add_row([ "Min Spec SA", ssa_min, ssa_min_c, ssa_min_start_ppo, ssa_min_end_ppo ]) table.add_row([ "Max Spec SA", ssa_max, ssa_max_c, ssa_max_start_ppo, ssa_max_end_ppo ]) table.add_row(["Mean Spec SA", ssa_mean, "n/a", "n/a", "n/a"]) table.add_row(["Median Spec SA", ssa_median, "n/a", "n/a", "n/a"]) logging.info(table) logging.info("Standard deviation of specific surface area = %.3f" % (ssa_std)) table2 = PrettyTable() table2.field_names = [ "", "C, BET Constant", "Spec SA", "Start P/Po", "End P/Po", "Error", ] table2.add_row( ["Min C", c_min, c_min_sa, c_min_start_ppo, c_min_end_ppo, c_min_err]) table2.add_row( ["Max C", c_max, c_max_sa, c_max_start_ppo, c_max_end_ppo, c_max_err]) table2.add_row(["Mean C", c_mean, "n/a", "n/a", "n/a", "n/a"]) table2.add_row(["Median C", c_median, "n/a", "n/a", "n/a", "n/a"]) table2.add_row([ "Min Error C", cmin_err, c_min_err_sa, c_min_err_start_ppo, c_min_err_end_ppo, err_min, ]) table2.add_row([ "Max Error C", cmax_err, c_max_err_sa, c_max_err_start_ppo, c_max_err_end_ppo, err_max, ]) logging.info(table2) logging.info("Standard deviation of BET constant (C) = %.5f" % (c_std)) return table, table2, ssa_std, c_std
def dataframe_tables(bet_results, mask_results): """Creates and populates pandas dataframes summarizing BET results. Parameters ---------- bet_results : namedtuple Contains elements that result from BET analysis. Relevant fields are: - ``bet_results.iso_df`` (DataFrame) : experimental isotherm data. - ``bet_results.ssa`` (ndarray) : specific surface areas for all relp ranges. - ``bet_results.c`` (ndarray) : BET constants for all relp ranges. - ``bet_results.err`` (ndarray) : error values for all relp ranges. mask_results : namedtuple Contains the results of applying the Rouquerol criteria to BET results. Relevant fields are: - ``mask_results.mask`` (MaskedArray) : object where invalid BET results are masked. Returns ------- ssa_table : DataFrame Summary of BET results, highlighting the high, low, and average values of specific surface area. c_table : DataFrame Summary of BET results, highlighting the high, low, and average values of the BET constant. ssa_std : float Atandard deviation of valid specific surface area values. c_std : float Standard deviation of valid BET constant values. """ mask = mask_results.mask if mask.all(): logging.warning( "No valid relative pressure ranges. Tables not created.") ssa_dict = { " ": ["Min Spec SA", "Max Spec SA", "Mean Spec SA", "Median Spec SA"], "Spec SA m2/g": ["n/a", "n/a", "n/a", "n/a"], "C": ["n/a", "n/a", "n/a", "n/a"], "Start P/Po": ["n/a", "n/a", "n/a", "n/a"], "End P/Po": ["n/a", "n/a", "n/a", "n/a"], } ssa_table = pd.DataFrame(data=ssa_dict) c_dict = { " ": [ "Min C", "Max C", "Mean C", "Median C", "Min Error C", "Max Error C" ], "C": ["n/a", "n/a", "n/a", "n/a", "n/a", "n/a"], "Spec SA": ["n/a", "n/a", "n/a", "n/a", "n/a", "n/a"], "Start P/Po": ["n/a", "n/a", "n/a", "n/a", "n/a", "n/a"], "End P/Po": ["n/a", "n/a", "n/a", "n/a", "n/a", "n/a"], "Error": ["n/a", "n/a", "n/a", "n/a", "n/a", "n/a"], } msg = "No valid relative pressure ranges. Standard deviations not calculated." logging.warning(msg) c_table = pd.DataFrame(data=c_dict) ssa_sdev = 0 c_sdev = 0 return ssa_table, c_table, ssa_sdev, c_sdev df = bet_results.iso_df ssa = np.ma.array(bet_results.ssa, mask=mask) c = np.ma.array(bet_results.c, mask=mask) err = np.ma.array(bet_results.err, mask=mask) c = np.nan_to_num(c) ssamax, ssa_max_idx, ssamin, ssa_min_idx = util.max_min(ssa) cmax, c_max_idx, cmin, c_min_idx = util.max_min(c) ssamean = np.ma.mean(ssa) ssamedian = np.ma.median(ssa) cmean = np.ma.mean(c) cmedian = np.ma.median(c) ssa_std = np.nan_to_num(ssa)[ssa != 0].std() c_std = np.nan_to_num(c)[c != 0].std() err_max, err_max_idx, err_min, err_min_idx = util.max_min(err) cmax_err = float(c[err_max_idx[0], err_max_idx[1]]) cmin_err = float(c[err_min_idx[0], err_min_idx[1]]) # these are just variables to print in tables ssa_min = round(ssamin, 3) ssa_min_c = round(float(c[ssa_min_idx[0], ssa_min_idx[1]]), 3) ssa_min_start_ppo = round(float(df.relp[ssa_min_idx[1]]), 3) ssa_min_end_ppo = round(float(df.relp[ssa_min_idx[0]]), 3) ssa_max = round(ssamax, 3) ssa_max_c = round(float(c[ssa_max_idx[0], ssa_max_idx[1]]), 3) ssa_max_start_ppo = round(float(df.relp[ssa_max_idx[1]]), 3) ssa_max_end_ppo = round(float(df.relp[ssa_max_idx[0]]), 3) ssa_mean = round(ssamean, 3) ssa_median = round(ssamedian, 3) c_min = round(cmin, 3) c_min_sa = round(float(ssa[c_min_idx[0], c_min_idx[1]]), 3) c_min_start_ppo = round(float(df.relp[c_min_idx[1]]), 3) c_min_end_ppo = round(float(df.relp[c_min_idx[0]]), 3) c_min_err = round(float(err[c_min_idx[0], c_min_idx[1]]), 3) c_max = round(cmax, 3) c_max_sa = round(float(ssa[c_max_idx[0], c_max_idx[1]]), 3) c_max_start_ppo = round(float(df.relp[c_max_idx[1]]), 3) c_max_end_ppo = round(float(df.relp[c_max_idx[0]]), 3) c_max_err = round(float(err[c_max_idx[0], c_max_idx[1]]), 3) c_mean = round(cmean, 3) c_median = round(cmedian, 3) cmin_err = round(cmin_err, 3) c_min_err_sa = round(float(ssa[err_min_idx[0], err_min_idx[1]]), 3) c_min_err_start_ppo = round(float(df.relp[err_min_idx[1]]), 3) c_min_err_end_ppo = round(float(df.relp[err_min_idx[0]]), 3) err_min = round(err_min, 3) cmax_err = round(cmax_err, 3) c_max_err_sa = round(float(ssa[err_max_idx[0], err_max_idx[1]]), 3) c_max_err_start_ppo = round(float(df.relp[err_max_idx[1]]), 3) c_max_err_end_ppo = round(float(df.relp[err_max_idx[0]]), 3) err_max = round(err_max, 3) ssa_dict = { " ": ["Min Spec SA", "Max Spec SA", "Mean Spec SA", "Median Spec SA"], "Spec SA m2/g": [ssa_min, ssa_max, ssa_mean, ssa_median], "C": [ssa_min_c, ssa_max_c, "n/a", "n/a"], "Start P/Po": [ssa_min_start_ppo, ssa_max_start_ppo, "n/a", "n/a"], "End P/Po": [ssa_min_end_ppo, ssa_max_end_ppo, "n/a", "n/a"], } ssa_table = pd.DataFrame(data=ssa_dict) c_dict = { " ": ["Min C", "Max C", "Mean C", "Median C", "Min Error C", "Max Error C"], "C": [c_min, c_max, c_mean, c_median, cmin_err, cmax_err], "Spec SA": [c_min_sa, c_max_sa, "n/a", "n/a", c_min_err_sa, c_max_err_sa], "Start P/Po": [ c_min_start_ppo, c_max_start_ppo, "n/a", "n/a", c_min_err_start_ppo, c_max_err_start_ppo, ], "End P/Po": [ c_min_end_ppo, c_max_end_ppo, "n/a", "n/a", c_min_err_end_ppo, c_max_err_end_ppo, ], "Error": [c_min_err, c_max_err, "n/a", "n/a", err_min, err_max], } c_table = pd.DataFrame(data=c_dict) return ssa_table, c_table, ssa_std, c_std
def ssa_answer(bet_results, mask_results, criterion="error"): """ Logs a single specific surface area answer from the valid relative pressure range with the lowest error, most number of points, maximum specific surface area, or minimum specific surface area. Parameters ---------- bet_results : named tuple ``bet_results.ssa`` contains the array of specific surface values. rouq_mask : named tuple ``rouq_mask.mask`` contains the mask used to remove invaid specific surface area values from consideration. criterion : str Used to specify the criterion for a final specific surface area answer, either 'error', 'points', 'max', or 'min. Defaults to 'error'. Returns ------- ssa_ans : float Specific surface answer corresponding to user defined criteria. """ mask = mask_results.mask if mask.all(): msg = "No valid relative pressure ranges. Specific surface area not calculated." raise ValueError(msg) ssa = np.ma.array(bet_results.ssa, mask=mask) if criterion == "points": pts = np.ma.array(bet_results.num_pts, mask=mask) max_pts = np.max(pts) ssa_ans_array = np.ma.masked_where(pts < max_pts, ssa) try: ssa_ans = float(ssa_ans_array.compressed()) except ValueError: raise Exception( "Error, so single specific surface area answer. Multiple" + "relative pressure ranges with the maximum number of points.") return 0 logging.info( "The specific surface area value, based on %s is %.2f m2/g." % (criterion, ssa_ans)) return ssa_ans if criterion == "error": err = np.ma.array(bet_results.err, mask=mask) errormax, error_max_idx, errormin, error_min_idx = util.max_min(err) ssa_ans = ssa[int(error_min_idx[0]), int(error_min_idx[1])] logging.info( "The specific surface area value, based on %s is %.2f m2/g." % (criterion, ssa_ans)) return ssa_ans if criterion == "max": ssa_ans = np.max(ssa) logging.info( "The specific surface area value, based on %s is %.2f m2/g." % (criterion, ssa_ans)) return ssa_ans if criterion == "min": ssa_ans = np.min(ssa) logging.info( "The specific surface area value, based on %s is %.2f m2/g." % (criterion, ssa_ans)) return ssa_ans else: raise ValueError( "Invalid criterion, must be points, error, min, or max.")