diff --git a/CPAC/utils/create_flame_model_files.py b/CPAC/utils/create_flame_model_files.py index d77ebaaf5e..58f5be3b2b 100644 --- a/CPAC/utils/create_flame_model_files.py +++ b/CPAC/utils/create_flame_model_files.py @@ -1,3 +1,24 @@ +# Copyright (C) 2016-2024 C-PAC Developers + +# This file is part of C-PAC. + +# C-PAC is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. + +# C-PAC is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with C-PAC. If not, see . +from CPAC.utils.monitoring.custom_logging import getLogger + +logger = getLogger("nipype.workflow") + + def create_dummy_string(length): ppstring = "" for i in range(0, length): @@ -126,6 +147,8 @@ def create_fts_file(ftest_list, con_names, model_name, current_output, out_dir): import numpy as np + logger.info("\nFound f-tests in your model, writing f-tests file (.fts)..\n") + try: out_file = os.path.join(out_dir, model_name + ".fts") @@ -202,7 +225,8 @@ def create_con_ftst_file( evs = evs.rstrip("\r\n").split(",") if evs[0].strip().lower() != "contrasts": - raise Exception + msg = "first cell in contrasts file should contain 'Contrasts'" + raise ValueError(msg) # remove "Contrasts" label and replace it with "Intercept" # evs[0] = "Intercept" @@ -217,7 +241,8 @@ def create_con_ftst_file( try: contrasts_data = np.genfromtxt(con_file, names=True, delimiter=",", dtype=None) except: - raise Exception + msg = f"Could not successfully read in contrast file: {con_file}" + raise OSError(msg) lst = contrasts_data.tolist() # lst = list of rows of the contrast matrix (each row represents a @@ -291,27 +316,31 @@ def create_con_ftst_file( fts_n = fts_columns.T if len(column_names) != (num_EVs_in_con_file): - "\n\n[!] CPAC says: The number of EVs in your model " "design matrix (found in the %s.mat file) does not " "match the number of EVs (columns) in your custom " "contrasts matrix CSV file.\n\nCustom contrasts matrix " "file: %s\n\nNumber of EVs in design matrix: %d\n" "Number of EVs in contrasts file: %d\n\nThe column " "labels in the design matrix should match those in " "your contrasts .CSV file.\nColumn labels in design " "matrix:\n%s" % ( + logger.error( + "\n\n[!] CPAC says: The number of EVs in your model design matrix (found" + " in the %s.mat file) does not match the number of EVs (columns) in your" + " custom contrasts matrix CSV file.\n\nCustom contrasts matrix file:" + " %s\n\nNumber of EVs in design matrix: %d\nNumber of EVs in contrasts" + " file: %d\n\nThe column labels in the design matrix should match those in" + "your contrasts .CSV file.\nColumn labels in design matrix:\n%s", model_name, con_file, len(column_names), num_EVs_in_con_file, str(column_names), ) - - # raise Exception(err_string) return None, None for design_mat_col, con_csv_col in zip(column_names, evs[1:]): if con_csv_col not in design_mat_col: - errmsg = ( - "\n\n[!] CPAC says: The names of the EVs in your " - "custom contrasts .csv file do not match the names or " - "order of the EVs in the design matrix. Please make " - "sure these are consistent.\nDesign matrix EV columns: " - "%s\nYour contrasts matrix columns: %s\n\n" % (column_names, evs[1:]) + logger.error( + "\n\n[!] CPAC says: The names of the EVs in your custom contrasts .csv" + " file do not match the names or order of the EVs in the design" + " matrix. Please make sure these are consistent.\nDesign matrix EV" + " columns: %s\nYour contrasts matrix columns: %s\n\n", + column_names, + evs[1:], ) - return None, None out_file = os.path.join(output_dir, model_name + ".con") @@ -344,6 +373,7 @@ def create_con_ftst_file( ftest_out_file = None if fTest: + logger.info("\nFound f-tests in your model, writing f-tests file (.fts)..\n") ftest_out_file = os.path.join(output_dir, model_name + ".fts") with open(ftest_out_file, "wt") as f: diff --git a/CPAC/utils/create_fsl_flame_preset.py b/CPAC/utils/create_fsl_flame_preset.py index de99d3b3f1..4ac8474c88 100644 --- a/CPAC/utils/create_fsl_flame_preset.py +++ b/CPAC/utils/create_fsl_flame_preset.py @@ -1,3 +1,23 @@ +# Copyright (C) 2018-2024 C-PAC Developers + +# This file is part of C-PAC. + +# C-PAC is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. + +# C-PAC is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with C-PAC. If not, see . +from CPAC.utils.monitoring.custom_logging import getLogger + +logger = getLogger("nipype.workflow") + # TODO: create a function that can help easily map raw pheno files that do not # TODO: have the participant_session id that CPAC uses @@ -56,7 +76,7 @@ def write_group_list_text_file(group_list, out_file=None): f.write(f"{part_id}\n") if os.path.exists(out_file): - pass + logger.info("Group-level analysis participant list written:\n%s\n", out_file) return out_file @@ -83,7 +103,7 @@ def write_dataframe_to_csv(matrix_df, out_file=None): matrix_df.to_csv(out_file, index=False) if os.path.exists(out_file): - pass + logger.info("CSV file written:\n%s\n", out_file) def write_config_dct_to_yaml(config_dct, out_file=None): @@ -170,7 +190,9 @@ def write_config_dct_to_yaml(config_dct, out_file=None): ) if os.path.exists(out_file): - pass + logger.info( + "Group-level analysis configuration YAML file written:\n%s\n", out_file + ) def create_design_matrix_df( diff --git a/CPAC/utils/create_fsl_model.py b/CPAC/utils/create_fsl_model.py index 9faff76984..f11b456e58 100644 --- a/CPAC/utils/create_fsl_model.py +++ b/CPAC/utils/create_fsl_model.py @@ -1,3 +1,24 @@ +# Copyright (C) 2012-2024 C-PAC Developers + +# This file is part of C-PAC. + +# C-PAC is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. + +# C-PAC is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with C-PAC. If not, see . +from CPAC.utils.monitoring.custom_logging import getLogger + +logger = getLogger("nipype.workflow") + + def load_pheno_file(pheno_file): import os @@ -362,7 +383,13 @@ def model_group_var_separately( grouping_var, formula, pheno_data_dict, ev_selections, coding_scheme ): if grouping_var is None or grouping_var not in formula: - raise Exception + msg = ( + "\n\n[!] CPAC says: Model group variances separately is enabled, but the" + " grouping variable set is either set to None, or was not included in the" + f" model as one of the EVs.\n\nDesign formula: {formula}\nGrouping" + f" variable: {grouping_var}\n\n" + ) + raise ValueError(msg) # do this a little early for the grouping variable so that it doesn't # get in the way of doing this for the other EVs once they have the @@ -471,18 +498,33 @@ def model_group_var_separately( def check_multicollinearity(matrix): import numpy as np + logger.info("\nChecking for multicollinearity in the model..") + U, s, V = np.linalg.svd(matrix) max_singular = np.max(s) min_singular = np.min(s) + logger.info( + "Max singular: %s\nMin singular: %s\nRank: %s\n\n", + max_singular, + min_singular, + np.linalg.matrix_rank(matrix), + ) + + _warning = ( + "[!] CPAC warns: Detected multicollinearity in the computed group-level" + " analysis model. Please double-check your model design.\n\n" + ) + if min_singular == 0: - pass + logger.warning(_warning) else: condition_number = float(max_singular) / float(min_singular) + logger.info("Condition number: %f\n\n", condition_number) if condition_number > 30: - pass + logger.warning(_warning) def write_mat_file( @@ -805,8 +847,17 @@ def create_design_matrix( try: dmatrix = patsy.dmatrix(formula, pheno_data_dict, NA_action="raise") - except: - raise Exception + except Exception as e: + msg = ( + "\n\n[!] CPAC says: Design matrix creation wasn't successful - do the" + " terms in your formula correctly correspond to the EVs listed in your" + " phenotype file?\nPhenotype file provided: %s\n\nPhenotypic data" + " columns (regressors): %s\nFormula: %s\n\n", + pheno_file, + list(pheno_data_dict.keys()), + formula, + ) + raise RuntimeError(msg) from e # check the model for multicollinearity - Patsy takes care of this, but # just in case @@ -976,6 +1027,8 @@ def create_dummy_string(length): def create_con_file(con_dict, col_names, file_name, current_output, out_dir): import os + logger.info("col names: %s", col_names) + with open(os.path.join(out_dir, file_name) + ".con", "w+") as f: # write header num = 1 @@ -1012,6 +1065,7 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, out_dir): import numpy as np try: + logger.info("\nFound f-tests in your model, writing f-tests file (.fts)..\n") with open(os.path.join(out_dir, model_name + ".fts"), "w") as f: print("/NumWaves\t", len(con_dict), file=f) print("/NumContrasts\t", len(ftest_list), file=f) @@ -1089,6 +1143,7 @@ def create_con_ftst_file( # evs[0] = "Intercept" fTest = False + logger.info("evs: %s", evs) for ev in evs: if "f_test" in ev: count_ftests += 1 @@ -1099,8 +1154,9 @@ def create_con_ftst_file( try: data = np.genfromtxt(con_file, names=True, delimiter=",", dtype=None) - except: - raise Exception + except Exception as e: + msg = f"Error: Could not successfully read in contrast file: {con_file}" + raise OSError(msg) from e lst = data.tolist() @@ -1218,6 +1274,7 @@ def create_con_ftst_file( np.savetxt(f, contrasts, fmt="%1.5e", delimiter="\t") if fTest: + logger.info("\nFound f-tests in your model, writing f-tests file (.fts)..\n") ftest_out_dir = os.path.join(output_dir, model_name + ".fts") with open(ftest_out_dir, "wt") as f: @@ -1361,8 +1418,13 @@ def run( try: if not os.path.exists(output_dir): os.makedirs(output_dir) - except: - raise Exception + except Exception as e: + msg = ( + "\n\n[!] CPAC says: Could not successfully create the group analysis" + f" output directory:\n{output_dir}\n\nMake sure you have write access" + " in this file structure.\n\n\n" + ) + raise OSError(msg) from e measure_dict = {} @@ -1551,6 +1613,10 @@ def run( or (custom_contrasts == "") or ("None" in custom_contrasts) ): + logger.info( + "Writing contrasts file (.con) based on contrasts provided using the group" + " analysis model builder's contrasts editor.." + ) create_con_file( contrasts_dict, regressor_names, model_name, current_output, model_out_dir ) @@ -1561,6 +1627,11 @@ def run( ) else: + logger.info( + "\nWriting contrasts file (.con) based on contrasts provided with a custom" + " contrasts matrix CSV file..\n" + ) + create_con_ftst_file( custom_contrasts, model_name,