diff --git a/src/kinase_library/__init__.py b/src/kinase_library/__init__.py index 0e9a571..34b704c 100644 --- a/src/kinase_library/__init__.py +++ b/src/kinase_library/__init__.py @@ -25,7 +25,7 @@ #%% -__version__ = "1.7.0" +__version__ = "1.7.1" #%% Loading scored phosphoproteome one time per session diff --git a/src/kinase_library/modules/data.py b/src/kinase_library/modules/data.py index 8e4d372..3ea9513 100644 --- a/src/kinase_library/modules/data.py +++ b/src/kinase_library/modules/data.py @@ -570,6 +570,91 @@ def get_kinome_info(kin_type=None, columns=None, info_file='./../databases/kinas return(all_kin_info) +def update_kinome_info(rows, info_file='./../databases/kinase_data/kinome_information.tsv'): + """ + Add or update rows in kinome_information.tsv. + + For each row, if MATRIX_NAME already exists in the file the existing row + is replaced; otherwise the row is appended. Row order from the original + file is preserved for unchanged rows; new rows are inserted alphabetically + by MATRIX_NAME. Line endings (\\r\\n) and boolean format (TRUE/FALSE) + match the existing file convention. + + Parameters + ---------- + rows : pd.DataFrame or dict + If pd.DataFrame: one row per kinase, must contain MATRIX_NAME column. + If dict: mapping of {matrix_name: {column: value, ...}}. + info_file : str, optional + Path to information file. The default is the package's + kinome_information.tsv. + + Returns + ------- + None. + """ + if not os.path.isfile(info_file): + current_dir = os.path.dirname(__file__) + info_file = os.path.join(current_dir, info_file) + + if isinstance(rows, dict): + df_rows = pd.DataFrame.from_dict(rows, orient='index') + df_rows.index.name = 'MATRIX_NAME' + df_rows = df_rows.reset_index() + else: + df_rows = rows.copy() + + if 'MATRIX_NAME' not in df_rows.columns: + raise ValueError('rows must contain a MATRIX_NAME column.') + + # na_filter=False keeps every cell as a string so we don't accidentally + # rewrite the literal 'nan' entries some legacy rows have for missing PDB IDs + all_info = pd.read_csv(info_file, sep='\t', dtype=str, na_filter=False) + + new_names = df_rows['MATRIX_NAME'].astype(str).tolist() + new_names_set = set(new_names) + existing_names = all_info['MATRIX_NAME'].astype(str).tolist() + + # Replace existing rows in place to preserve original row order; append + # truly new rows at the end (callers may sort if they prefer) + name_to_row = {r['MATRIX_NAME']: r for _, r in df_rows.iterrows()} + kept_records = [] + for _, existing_row in all_info.iterrows(): + matrix_name = str(existing_row['MATRIX_NAME']) + if matrix_name in new_names_set: + # Replace with the new row (preserving column order from all_info) + new_row = name_to_row[matrix_name] + merged = {col: new_row.get(col, existing_row[col]) for col in all_info.columns} + kept_records.append(merged) + else: + kept_records.append(existing_row.to_dict()) + + # Append rows for kinases not previously in the file + truly_new = [n for n in new_names if n not in set(existing_names)] + for name in truly_new: + new_row = name_to_row[name] + merged = {col: new_row.get(col, '') for col in all_info.columns} + kept_records.append(merged) + + out_df = pd.DataFrame(kept_records, columns=all_info.columns) + + # Match the existing file format: TRUE/FALSE booleans and CRLF line endings + if 'DUAL_SPECIFICITY' in out_df.columns: + out_df['DUAL_SPECIFICITY'] = out_df['DUAL_SPECIFICITY'].apply( + lambda v: 'TRUE' if v in (True, 'TRUE', 'True', 'true') else + ('FALSE' if v in (False, 'FALSE', 'False', 'false') else v) + ) + + # Write with CRLF to match the existing file convention. Strip the + # trailing line terminator that pandas adds, since the original file + # ends with the last row's line ending only. + text = out_df.to_csv(sep='\t', index=False, lineterminator='\r\n') + if text.endswith('\r\n'): + text = text[:-2] + with open(info_file, 'wb') as f: + f.write(text.encode('utf-8')) + + def get_kinase_info(kinase, name_type='matrix', info_file='./../databases/kinase_data/kinome_information.tsv'): """ Get kinase information. diff --git a/src/kinase_library/utils/matrix_processing.py b/src/kinase_library/utils/matrix_processing.py index fc4b77a..097bd34 100644 --- a/src/kinase_library/utils/matrix_processing.py +++ b/src/kinase_library/utils/matrix_processing.py @@ -240,11 +240,13 @@ def build_combined_matrices(kin_type, k_mod=False, round_digits=4): positions = _get_positions(kin_type) combined_matrix_columns = [str(x) + str(y) for x in positions for y in full_aa_labels] - densitometry_dir = os.path.join(mat_dir, 'densitometry') + # Enumerate from raw/ — that's the canonical source for the combined + # matrices (every kinase in the database has its raw matrix written there) + raw_dir = os.path.join(mat_dir, 'raw') all_kinases = np.sort([ - '_'.join(name.split('.')[0].split('_')[:-1]) - for name in os.listdir(densitometry_dir) - if not name.startswith(".") + name.split('.')[0] + for name in os.listdir(raw_dir) + if not name.startswith('.') and name.endswith('.tsv') ]) full_matrix_raw = pd.DataFrame(index=all_kinases, columns=combined_matrix_columns) @@ -422,32 +424,216 @@ def process_kinases(kin_type, lib_type, kinase_names=None, dual_spec=False, return results -def save_matrices_to_database(processed_results, kin_type, round_digits=4, - output_dir=None, rebuild_combined=True, - update_phosphoproteome=False): +_KINOME_INFO_COLUMNS = [ + 'MATRIX_NAME', 'KINASE', 'GENE_NAME', 'TYPE', 'SUBTYPE', 'FAMILY', + 'UNIPROT_ID', 'UNIPROT_ENTRY_NAME', 'PDB_ID', 'KL_LIBRARY', + 'DUAL_SPECIFICITY', 'DISPLAY_NAME' +] + + +def _write_individual_matrices(processed_results, output_dir, round_digits): + """Write raw/norm/log2 TSV files for each kinase under output_dir.""" + raw_dir = os.path.join(output_dir, 'raw') + norm_dir = os.path.join(output_dir, 'norm') + log2_dir = os.path.join(output_dir, 'log2') + + os.makedirs(raw_dir, exist_ok=True) + os.makedirs(norm_dir, exist_ok=True) + os.makedirs(log2_dir, exist_ok=True) + + for kinase, result in processed_results.items(): + result['raw'].round(round_digits).to_csv( + os.path.join(raw_dir, kinase + '.tsv'), header=True, index=True, sep='\t' + ) + result['norm'].round(round_digits).to_csv( + os.path.join(norm_dir, kinase + '.tsv'), sep='\t' + ) + result['log2'].round(round_digits).to_csv( + os.path.join(log2_dir, kinase + '.tsv'), sep='\t' + ) + + print(f'Saved {len(processed_results)} kinase matrices to {output_dir}') + + +def _write_combined_matrices(kin_type, base_dir, round_digits): + """Rebuild and write the combined *_all_*.tsv files for the package database.""" + print('Rebuilding combined matrices...') + combined = build_combined_matrices(kin_type, round_digits=round_digits) + combined['raw'].to_csv( + os.path.join(base_dir, kin_type + '_all_raw_matrices.tsv'), + header=True, index=True, sep='\t' + ) + combined['norm'].to_csv( + os.path.join(base_dir, kin_type + '_all_norm_matrices.tsv'), + header=True, index=True, sep='\t' + ) + combined['log2'].to_csv( + os.path.join(base_dir, kin_type + '_all_log2_matrices.tsv'), + header=True, index=True, sep='\t' + ) + if kin_type == 'ser_thr': + combined['st_favorability'].to_csv( + os.path.join(base_dir, 'st_favorability.tsv'), + header=True, index=True, sep='\t' + ) + print('Combined matrices saved.') + + +def _confirm(message): + """Prompt y/n. Returns True on yes, False on no.""" + while True: + ans = input(f'{message} (y/n): ').strip().lower() + if ans in ('y', 'yes'): + return True + if ans in ('n', 'no'): + return False + print("Please enter 'y' or 'n'.") + + +def _kinase_already_in_kinome_info(name): + """Return True if the kinase has a row in kinome_information.tsv.""" + try: + all_info = data.get_kinome_info() + return name in all_info['MATRIX_NAME'].astype(str).tolist() + except Exception: + return False + + +def _prompt_field(label, default=None): + """Prompt for a field; pressing enter accepts the default if given.""" + if default is not None: + prompt = f' {label} [{default}]: ' + else: + prompt = f' {label}: ' + val = input(prompt).strip() + if val == '' and default is not None: + return default + return val + + +def _prompt_kinome_info_for_kinase(name, kin_type, lib_type, dual_spec, family=None): """ - Save processed matrices to the database. + Prompt the user for kinome_information columns for a single kinase. + + Returns a dict of column->value, or None if the user declined to update + an existing row. + """ + if _kinase_already_in_kinome_info(name): + if not _confirm(f"Kinase '{name}' already exists in kinome_information.tsv. Update its row?"): + return None + + print(f"Filling kinome_information.tsv row for '{name}':") + + kinase_val = _prompt_field('KINASE', default=name) + gene_val = _prompt_field('GENE_NAME', default=name) + subtype_val = _prompt_field('SUBTYPE (e.g. STK, RTK, ncTK)') + family_val = _prompt_field('FAMILY', default=family) if family else _prompt_field('FAMILY') + uniprot_id = _prompt_field('UNIPROT_ID') + uniprot_entry = _prompt_field('UNIPROT_ENTRY_NAME') + pdb_id = _prompt_field('PDB_ID (optional, blank to skip)', default='') + display_name = _prompt_field('DISPLAY_NAME', default=name) + + return { + 'MATRIX_NAME': name, + 'KINASE': kinase_val, + 'GENE_NAME': gene_val, + 'TYPE': kin_type, + 'SUBTYPE': subtype_val, + 'FAMILY': family_val, + 'UNIPROT_ID': uniprot_id, + 'UNIPROT_ENTRY_NAME': uniprot_entry, + 'PDB_ID': pdb_id, + 'KL_LIBRARY': lib_type, + 'DUAL_SPECIFICITY': bool(dual_spec), + 'DISPLAY_NAME': display_name, + } - Writes individual raw, normalized, and log2 matrix files, optionally - rebuilds combined matrix files, and optionally updates the scored - phosphoproteomes. + +def _resolve_kinome_info_rows(kinome_info, kinase_metadata, confirm): + """ + Resolve the kinome_info rows to apply. + + Parameters + ---------- + kinome_info : str, pd.DataFrame, dict, or None + - str: path to a TSV/CSV file with one row per kinase + - pd.DataFrame: rows already prepared + - dict: {matrix_name: {column: value, ...}} + - None: prompt interactively (requires confirm=True) + kinase_metadata : dict + {name: (kin_type, lib_type, dual_spec, family)} for each kinase being saved. + confirm : bool + If False and kinome_info is None, raise an error. + + Returns + ------- + pd.DataFrame or None + DataFrame ready for data.update_kinome_info, or None if the user + declined all updates. + """ + if kinome_info is None: + if not confirm: + raise ValueError( + 'kinome_info must be provided when confirm=False and ' + 'update_database=True (no interactive prompts available).' + ) + rows = [] + for name, (kin_type, lib_type, dual_spec, family) in kinase_metadata.items(): + row = _prompt_kinome_info_for_kinase(name, kin_type, lib_type, dual_spec, family) + if row is not None: + rows.append(row) + if not rows: + return None + return pd.DataFrame(rows) + + if isinstance(kinome_info, str): + sep = '\t' if kinome_info.endswith('.tsv') else ',' + return pd.read_csv(kinome_info, sep=sep) + if isinstance(kinome_info, dict): + df = pd.DataFrame.from_dict(kinome_info, orient='index') + df.index.name = 'MATRIX_NAME' + return df.reset_index() + if isinstance(kinome_info, pd.DataFrame): + return kinome_info.copy() + raise TypeError(f'Unsupported type for kinome_info: {type(kinome_info)}') + + +def save_matrices(processed_results, kin_type, output_dir=None, + update_database=False, confirm=True, kinome_info=None, + kinase_metadata=None, round_digits=4): + """ + Save processed matrices to a directory and/or update the package database. + + Two independent save targets: + * `output_dir` - write matrices to this directory only + * `update_database` - perform full package update (matrices + combined + + kinome_information.tsv + phosphoproteomes), with confirmation prompt + + Both can be set in the same call. Parameters ---------- processed_results : dict - Dictionary mapping kinase names to result dicts (output of - process_kinase or process_kinases). + {kinase_name: result_dict} from process_kinase / process_kinases. kin_type : str Kinase type ('ser_thr' or 'tyrosine'). + output_dir : str, optional + Directory to write matrices to. The default is None. + update_database : bool, optional + If True, perform the full package update. The default is False. + confirm : bool, optional + If True, prompt before updating the package database. The default is True. + kinome_info : str, dict, or pd.DataFrame, optional + Path to a TSV/CSV file, dict, or DataFrame with kinome_information rows + for the kinases being saved. Only used when update_database is True. + If None and confirm=True, fields are prompted interactively. If None and + confirm=False, an error is raised. The default is None. + kinase_metadata : dict, optional + {name: (kin_type, lib_type, dual_spec, family)} for each kinase. Used + to drive the kinome_info prompts. Defaults inferred from kin_type and + ('ser_thr', False, None) if not provided. round_digits : int, optional Number of decimal digits for rounding. The default is 4. - output_dir : str, optional - Custom output directory. If None, writes to the package's internal - database. The default is None. - rebuild_combined : bool, optional - Rebuild combined matrix files after saving. The default is True. - update_phosphoproteome : bool, optional - Update scored phosphoproteomes after saving. The default is False. Returns ------- @@ -455,87 +641,73 @@ def save_matrices_to_database(processed_results, kin_type, round_digits=4, """ exceptions.check_kin_type(kin_type) - if output_dir is None: - current_dir = os.path.dirname(__file__) - base_dir = os.path.join(current_dir, _global_vars.mat_dir, kin_type) - else: - base_dir = output_dir + if not output_dir and not update_database: + print('Nothing to do. Pass output_dir or update_database=True.') + return - raw_dir = os.path.join(base_dir, 'raw') - norm_dir = os.path.join(base_dir, 'norm') - log2_dir = os.path.join(base_dir, 'log2') + # Path-only save (independent of update_database) + if output_dir: + _write_individual_matrices(processed_results, output_dir, round_digits) - os.makedirs(raw_dir, exist_ok=True) - os.makedirs(norm_dir, exist_ok=True) - os.makedirs(log2_dir, exist_ok=True) + if not update_database: + return - for kinase, result in processed_results.items(): - result['raw'].round(round_digits).to_csv( - os.path.join(raw_dir, kinase + '.tsv'), header=True, index=True, sep='\t' - ) - result['norm'].round(round_digits).to_csv( - os.path.join(norm_dir, kinase + '.tsv'), sep='\t' - ) - result['log2'].round(round_digits).to_csv( - os.path.join(log2_dir, kinase + '.tsv'), sep='\t' - ) + # Confirmation prompt for the package update + if confirm: + names = list(processed_results.keys()) + if len(names) <= 5: + label = ', '.join(f"'{n}'" for n in names) + else: + label = f"{len(names)} kinases ({', '.join(names[:3])}, ...)" + if not _confirm(f'Update package database with {label}?'): + print('Database update cancelled.') + return - print(f'Saved {len(processed_results)} kinase matrices to {base_dir}') + # Resolve kinome_info rows BEFORE writing anything to the database + if kinase_metadata is None: + kinase_metadata = {n: (kin_type, kin_type, False, None) for n in processed_results} + kinome_rows = _resolve_kinome_info_rows(kinome_info, kinase_metadata, confirm) - if rebuild_combined: - if output_dir is not None: - print('Skipping combined matrix rebuild (only supported for package database).') - else: - print('Rebuilding combined matrices...') - combined = build_combined_matrices(kin_type, round_digits=round_digits) - combined['raw'].to_csv( - os.path.join(base_dir, kin_type + '_all_raw_matrices.tsv'), - header=True, index=True, sep='\t' - ) - combined['norm'].to_csv( - os.path.join(base_dir, kin_type + '_all_norm_matrices.tsv'), - header=True, index=True, sep='\t' - ) - combined['log2'].to_csv( - os.path.join(base_dir, kin_type + '_all_log2_matrices.tsv'), - header=True, index=True, sep='\t' + # Write to package + current_dir = os.path.dirname(__file__) + base_dir = os.path.join(current_dir, _global_vars.mat_dir, kin_type) + _write_individual_matrices(processed_results, base_dir, round_digits) + + # Save densitometry source files to the package, where available, so the + # kinase can be re-processed in the future from the package alone. + densitometry_dir = os.path.join(base_dir, 'densitometry') + os.makedirs(densitometry_dir, exist_ok=True) + for kinase, result in processed_results.items(): + if 'densitometry' in result and result['densitometry'] is not None: + result['densitometry'].to_csv( + os.path.join(densitometry_dir, kinase + '_densitometry.txt'), + sep='\t', header=True, index=True ) - if kin_type == 'ser_thr': - combined['st_favorability'].to_csv( - os.path.join(base_dir, 'st_favorability.tsv'), - header=True, index=True, sep='\t' - ) - print('Combined matrices saved.') - if update_phosphoproteome: - print('Updating scored phosphoproteomes...') - data.update_scored_phosphoproteome() + _write_combined_matrices(kin_type, base_dir, round_digits) - _print_database_update_reminder(update_phosphoproteome=update_phosphoproteome) + if kinome_rows is not None and len(kinome_rows) > 0: + print('Updating kinome_information.tsv...') + data.update_kinome_info(kinome_rows) + print('Updating scored phosphoproteomes...') + data.update_scored_phosphoproteome() -def _print_database_update_reminder(update_phosphoproteome=False): - """ - Print a prominent reminder of manual steps after updating the database. - """ + _print_database_update_reminder() + + +def _print_database_update_reminder(): + """Print a prominent reminder of remaining manual steps.""" banner = '#' * 70 lines = [ '', banner, '# REMEMBER: manual steps to complete the database update'.ljust(69) + '#', banner, - ] - step = 1 - if not update_phosphoproteome: - lines.append(f'# {step}. Update scored phosphoproteome:'.ljust(69) + '#') - lines.append('# >>> kl.update_scored_phosphoproteome()'.ljust(69) + '#') - step += 1 - lines += [ - f'# {step}. Update databases/kinase_data/kinome_information.tsv'.ljust(69) + '#', - f'# {step + 1}. Update test files in src/tests/ (if applicable)'.ljust(69) + '#', - f'# {step + 2}. Update CHANGELOG.md'.ljust(69) + '#', - f'# {step + 3}. Update README.md (kinase counts, new features)'.ljust(69) + '#', - f'# {step + 4}. Bump __version__ in src/kinase_library/__init__.py'.ljust(69) + '#', + '# 1. Update CHANGELOG.md'.ljust(69) + '#', + '# 2. Update README.md (kinase counts, new features)'.ljust(69) + '#', + '# 3. Bump __version__ in src/kinase_library/__init__.py'.ljust(69) + '#', + '# 4. Update test files in src/tests/ (if applicable)'.ljust(69) + '#', banner, '', ] @@ -549,20 +721,42 @@ def _print_database_update_reminder(update_phosphoproteome=False): ############################ """ +def _kinase_to_results_dict(kinase_obj): + """Build a processed_results-style dict from a Kinase object's attributes.""" + if not hasattr(kinase_obj, 'raw_matrix'): + raise AttributeError( + 'Kinase object has no raw_matrix attribute. ' + 'Use kinase_from_densitometry() to create a savable Kinase object.' + ) + + # Matrices are saved with positions as rows and amino acids as columns + # (transpose back from Kinase convention of amino acids as rows) + result = { + 'raw': kinase_obj.raw_matrix.transpose(), + 'norm': kinase_obj.norm_matrix.transpose(), + 'log2': kinase_obj.log2_matrix.transpose(), + } + if kinase_obj.kin_type == 'ser_thr' and hasattr(kinase_obj, 'st_fav_series'): + result['st_favorability'] = kinase_obj.st_fav_series + if hasattr(kinase_obj, '_densitometry'): + result['densitometry'] = kinase_obj._densitometry + + return {kinase_obj.name: result} + + def kinase_from_densitometry(densitometry, name, kin_type, lib_type, dual_spec=False, k_mod=False, family='undefined', round_digits=4, - update_database=False, - rebuild_combined=True, - update_phosphoproteome=False): + output_dir=None, update_database=False, + confirm=True, kinome_info=None): """ Read a densitometry matrix into a kl.Kinase object. Processes the densitometry through the full pipeline (raw, norm, log2) and returns a kl.Kinase object with all standard attributes and methods (plot_data, seq_logo, heatmap, score, percentile, etc.). The raw matrix - is attached as an extra attribute, and a save_to_database() method is - bound to the instance for optional persistence. + is attached as an extra attribute, and a save() method is bound to the + instance for optional persistence. Parameters ---------- @@ -582,26 +776,33 @@ def kinase_from_densitometry(densitometry, name, kin_type, lib_type, Kinase family. The default is 'undefined'. round_digits : int, optional Number of decimal digits for rounding. The default is 4. + output_dir : str, optional + Save matrices to this directory. The default is None (no save). update_database : bool, optional - If True, save the processed matrices to the package database - immediately after creation. The default is False. - rebuild_combined : bool, optional - When update_database is True, rebuild combined matrix files. - The default is True. - update_phosphoproteome : bool, optional - When update_database is True, re-score phosphoproteomes. - The default is False. + If True, perform the full package update. The default is False. + confirm : bool, optional + If True, prompt before updating the package database. The default is True. + kinome_info : str, dict, or pd.DataFrame, optional + Pre-built kinome_information row(s). Only used when update_database is + True. If None and confirm=True, fields are prompted interactively. Returns ------- kin : kl.Kinase - Kinase object with raw_matrix attached and save_to_database() bound. + Kinase object with raw_matrix attached and save() bound. """ exceptions.check_kin_type(kin_type) exceptions.check_lib_type(lib_type) + # Resolve densitometry to a DataFrame so we can stash it on the Kinase + # for later persistence to the package's densitometry/ folder. + if isinstance(densitometry, str): + densitometry_df = pd.read_csv(densitometry, sep='\t', index_col=0) + else: + densitometry_df = densitometry.copy() + result = process_kinase( - densitometry, kinase_name=name, kin_type=kin_type, lib_type=lib_type, + densitometry_df, kinase_name=name, kin_type=kin_type, lib_type=lib_type, dual_spec=dual_spec, k_mod=k_mod, round_digits=round_digits ) @@ -628,76 +829,123 @@ def kinase_from_densitometry(densitometry, name, kin_type, lib_type, if kin_type == 'ser_thr': kin.st_fav_series = result['st_favorability'] - # Store processing context for later re-save + # Store processing context for later save + kin._densitometry = densitometry_df kin._lib_type = lib_type kin._dual_spec = dual_spec kin._round_digits = round_digits - # Bind save_to_database method to the instance - def save_to_database(self, rebuild_combined=True, update_phosphoproteome=False, - output_dir=None): - save_kinase_to_database( - self, rebuild_combined=rebuild_combined, - update_phosphoproteome=update_phosphoproteome, - output_dir=output_dir, round_digits=self._round_digits + # Bind unified save method to the instance + def save(self, output_dir=None, update_database=False, confirm=True, + kinome_info=None): + save_matrices( + _kinase_to_results_dict(self), + kin_type=self.kin_type, + output_dir=output_dir, + update_database=update_database, + confirm=confirm, + kinome_info=kinome_info, + kinase_metadata={self.name: (self.kin_type, self._lib_type, + self._dual_spec, self.family)}, + round_digits=self._round_digits, ) - kin.save_to_database = types.MethodType(save_to_database, kin) - if update_database: - kin.save_to_database( - rebuild_combined=rebuild_combined, - update_phosphoproteome=update_phosphoproteome - ) + kin.save = types.MethodType(save, kin) + + # If save flags were passed at creation, run the save now + if output_dir is not None or update_database: + kin.save(output_dir=output_dir, update_database=update_database, + confirm=confirm, kinome_info=kinome_info) return kin -def save_kinase_to_database(kinase_obj, rebuild_combined=True, - update_phosphoproteome=False, - output_dir=None, round_digits=4): +def kinases_from_densitometry_dir(densitometry_dir, kin_type, lib_type, + kinase_names=None, dual_spec=False, + k_mod=False, family='undefined', + round_digits=4, output_dir=None, + update_database=False, confirm=True, + kinome_info=None): """ - Save a Kinase object's matrices to the package database. + Process all densitometry files in a directory into kl.Kinase objects. + + Mirrors the original matrix_processing.py command-line behavior. Parameters ---------- - kinase_obj : kl.Kinase - Kinase object (must have raw_matrix attribute, as created by - kinase_from_densitometry). - rebuild_combined : bool, optional - Rebuild combined matrix files after saving. The default is True. - update_phosphoproteome : bool, optional - Re-score phosphoproteomes after saving. The default is False. + densitometry_dir : str + Directory containing `{KINASE}_densitometry.txt` files. + kin_type : str + Kinase type ('ser_thr' or 'tyrosine'). + lib_type : str + Library type ('ser_thr', 'tyr', or 'ser_thr_tyr'). + kinase_names : list, optional + Specific kinases to process. The default is None (all files in the dir). + dual_spec, k_mod, family, round_digits : see process_kinases. output_dir : str, optional - Custom output directory. If None, writes to the package database. - The default is None. - round_digits : int, optional - Number of decimal digits for rounding. The default is 4. + Save matrices to this directory. The default is None. + update_database : bool, optional + Perform full package update. The default is False. + confirm : bool, optional + Prompt before updating the package database. The default is True. + kinome_info : str, dict, or pd.DataFrame, optional + Pre-built kinome_information rows for all kinases. Required when + update_database=True and confirm=False. Returns ------- - None. + kinases : dict + {kinase_name: kl.Kinase} dictionary. """ - if not hasattr(kinase_obj, 'raw_matrix'): - raise AttributeError( - 'Kinase object has no raw_matrix attribute. ' - 'Use kinase_from_densitometry() to create a savable Kinase object.' - ) + exceptions.check_kin_type(kin_type) + exceptions.check_lib_type(lib_type) - # Matrices are saved with positions as rows and amino acids as columns - # (transpose back from Kinase convention) - result = { - 'raw': kinase_obj.raw_matrix.transpose(), - 'norm': kinase_obj.norm_matrix.transpose(), - 'log2': kinase_obj.log2_matrix.transpose(), - } - if kinase_obj.kin_type == 'ser_thr' and hasattr(kinase_obj, 'st_fav_series'): - result['st_favorability'] = kinase_obj.st_fav_series + if not densitometry_dir.endswith('/'): + densitometry_dir = densitometry_dir + '/' + + available = [ + '_'.join(name.split('.')[0].split('_')[:-1]) + for name in os.listdir(densitometry_dir) + if not name.startswith('.') + ] + + if kinase_names is None: + kinase_names = sorted(available) - save_matrices_to_database( - {kinase_obj.name: result}, - kin_type=kinase_obj.kin_type, - round_digits=round_digits, - output_dir=output_dir, - rebuild_combined=rebuild_combined, - update_phosphoproteome=update_phosphoproteome + print(f'Processing {len(kinase_names)} kinase(s) from {densitometry_dir}') + kinases = {} + for kinase in tqdm(kinase_names): + if kinase not in available: + print(f"ERROR: kinase '{kinase}' not found in {densitometry_dir}") + continue + kinase_file = [ + x for x in os.listdir(densitometry_dir) + if '_'.join(x.split('.')[0].split('_')[:-1]) == kinase + ][0] + kinases[kinase] = kinase_from_densitometry( + densitometry_dir + kinase_file, + name=kinase, kin_type=kin_type, lib_type=lib_type, + dual_spec=dual_spec, k_mod=k_mod, family=family, + round_digits=round_digits, + # don't save individually — we'll save the batch below + ) + + if output_dir is None and not update_database: + return kinases + + # Build processed_results dict and run a single batch save (one combined + # rebuild, one phosphoproteome update at the end) + processed_results = {} + kinase_metadata = {} + for name, kin in kinases.items(): + processed_results[name] = _kinase_to_results_dict(kin)[name] + kinase_metadata[name] = (kin.kin_type, kin._lib_type, kin._dual_spec, kin.family) + + save_matrices( + processed_results, kin_type=kin_type, + output_dir=output_dir, update_database=update_database, + confirm=confirm, kinome_info=kinome_info, + kinase_metadata=kinase_metadata, round_digits=round_digits ) + + return kinases