From 9b2c69e2150a80fabcc28a610f573bc4f6e35e7c Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:59:37 +0100 Subject: [PATCH 01/17] Post-1.4.2 audit fixes: descriptor correctness, validation, safe CSV output Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 +++ DESCRIPTORS.md | 4 +- docs/descriptors.rst | 2 +- docs/usage.rst | 2 +- protpy/_constants.py | 34 ++++++ protpy/autocorrelation.py | 214 +++++++++++----------------------- protpy/composition.py | 135 ++++++++++++--------- protpy/conjoint_triad.py | 6 +- protpy/ctd.py | 15 ++- protpy/encode.py | 91 ++++++++------- protpy/sequence_order.py | 68 ++++------- protpy/utils.py | 6 +- tests/test_autocorrelation.py | 2 +- tests/test_composition.py | 47 +++++--- tests/test_conjoint_triad.py | 2 +- tests/test_regressions.py | 84 +++++++++++++ 16 files changed, 410 insertions(+), 320 deletions(-) create mode 100644 tests/test_regressions.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 073604f..3632f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- **Autocorrelation crash on fresh installs**: `aaindex1[code]` can return a new record on each access, so `.values.pop('-')` was lost and all three autocorrelation descriptors (and `all_descriptors`/`encode_dataset`) raised `IndexError`. Property values are now copied and filtered to the 20 canonical amino acids; aaindex records are no longer mutated. +- **Moran autocorrelation formula**: multiplied by the property variance instead of dividing by it. +- **CTD groups**: `polarity` groups 2/3 were copied from polarizability (A, D, G, S, T unassigned; 8 residues duplicated) — now `LIFWCMVY / PAGST / HQRKNED`. `normalized_vdwv` group 1 was missing C. +- **Overlapping counts**: dipeptide, tripeptide and conjoint triad used `str.count`, which skips overlapping matches (e.g. `"AA"` in `"AAA"`). +- **Short/repetitive sequences**: APAAComp raised `ZeroDivisionError` for sequences <= 30 residues; dipeptide composition, CTD transition, Moran and Geary crashed or returned NaN for single-residue or homopolymer sequences; lags with no residue pairs now return 0.0. +- **`all_descriptors(params={"ctd_": {"all_ctd": False}})`** raised `TypeError`. +- **Motif `dileucine_sorting`** never matched (literal `xxx` and stray `$`). +- **Boman index** now uses the published Radzicka-Wolfenden solubility scale and `BI = -sum/N`. +- **Isoelectric point** uses bisection over pH 0-14 (previously capped at 12.0 for very basic proteins). +- **PseAAC with AAIndex properties** included the `'-'` entry in normalization. +- **Glycine molecular weight** 75.032 -> 75.067. +- **`aggregation_propensity`**: `charge_threshold` is now an inclusive maximum, as documented; default changed 1 -> 0 so default results are unchanged. +- **`hydrophobic_moment`** registry entry now reports its `1 x 2` output shape. - **MoreauBroto Autocorrelation (Bug 1)**: Two intertwined bugs in `moreaubroto_autocorrelation` — the lag index always read `sequence[j+1]` (hardcoded lag=1) regardless of the current lag `i`, and the accumulator `temp` was not reset between lags, causing values to accumulate across all lags. Both corrected: `sequence[j+1]` → `sequence[j+i]`, and `temp = 0` moved inside the lag loop. - **PseAAC normalization denominator (Bug 2)**: `pseudo_amino_acid_composition` used `1 + weight + Σθ_k` where the Chou (2001) formula requires `1 + weight * Σθ_k`. Fixed operator `+` → `*`. Removed the redundant first normalization loop and unified output computation into a single pass. - **APAAComp properties parameter ignored (Bug 3)**: `amphiphilic_pseudo_amino_acid_composition` accepted a `properties` parameter but the underlying helpers `amphiphilic_correllation_function` and `amphiphilic_sequence_order_correlation_factor` hardcoded `hydrophobicity_` and `hydrophilicity_`, silently ignoring any user-supplied properties. Fixed by threading `properties` through all three functions and pre-normalizing once per call. - **`JSONDecodeError` constructor misuse (Bug 5)**: `sequence_order_coupling_number_` raised `json.JSONDecodeError(msg)` which fails at runtime with `TypeError` because `JSONDecodeError` requires `(msg, doc, pos)` positional arguments. Changed to `raise ValueError(...)`. ### Performance +- `kmer_composition`, dipeptide/tripeptide/pair composition count k-mers in one pass; QSO computes each SOCN once; PseAAC normalizes properties once per gap instead of per residue pair. - **`sequence_order_coupling_number_` disk I/O (Issue 6)**: Added `@functools.lru_cache(maxsize=2)` loader `_load_distance_matrix` in `sequence_order.py`. The distance matrix JSON was read from disk on every call; for a sequence with `lag=30`, that was 30 file reads per descriptor invocation. Now both matrices are cached in memory after the first load. - **`normalize_property` O(n²) (Issue 7)**: Refactored to compute `mean` and `std` once before the loop, reducing from O(n²) to O(n) for the 20-amino-acid property dictionaries used in PseAAC and APAAComp descriptors. ### Changed +- Invalid `lag`, `lamda`, `k`, `window` (non-integer or negative) and `weight` (non-numeric or negative) now raise `ValueError` instead of being silently replaced by defaults. `lag`/`lamda` larger than the sequence still fall back to 30. +- `all_descriptors` no longer writes `descriptors.csv` by default (`output_file=None`). +- `all_descriptors` no longer includes `amino_acid_pair_composition` (it duplicated dipeptide composition's 400 values). +- `encode_dataset`/`all_descriptors` refuse to overwrite an existing output file with different columns, append rows instead of rewriting the whole file per sequence, and `encode_dataset` replaces rows for re-encoded IDs instead of duplicating them. - **`tripeptide_composition` returns percentages (Gap 8)**: Output values are now fractional percentages (`count / (N-2) * 100`, rounded to 3 dp) consistent with `dipeptide_composition`, instead of raw integer counts. Column dtype is now `float64` instead of `int64`. - **`encode_dataset` verbose parameter (Gap 11)**: Added `verbose: bool = True` parameter to `encode_dataset`. When `False`, all per-sequence stdout status lines are suppressed. Default `True` preserves existing behaviour. diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index 64567ef..b2ea515 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -295,7 +295,7 @@ Estimates aggregation-prone regions via a sliding-window approach combining Kyte |-----------|------|---------|-------------| | `window` | int | `5` | Sliding window size | | `hydrophobicity_threshold` | float | `2.0` | Minimum mean hydrophobicity | -| `charge_threshold` | int | `1` | Maximum charged residues per window | +| `charge_threshold` | int | `0` | Maximum charged residues per window | ```python result = protpy.aggregation_propensity(protein_seq) @@ -713,7 +713,7 @@ Compute all available descriptors for a single protein sequence and return them | `sequence` | str | — | Protein sequence | | `params` | dict or None | `None` | Per-descriptor keyword arguments keyed by descriptor name | | `show_progress` | bool | `False` | Display a `tqdm` progress bar naming each descriptor as it is computed | -| `output_file` | str or None | `"descriptors.csv"` | Path to save results; `None` disables file output | +| `output_file` | str or None | `None` | Path to save results; `None` disables file output | | `save_each_sequence` | bool | `True` | Append this row to the output CSV immediately after computation; useful when calling in a loop | ```python diff --git a/docs/descriptors.rst b/docs/descriptors.rst index 3792478..4fc1463 100644 --- a/docs/descriptors.rst +++ b/docs/descriptors.rst @@ -433,7 +433,7 @@ windows and the fraction of the sequence covered. - Minimum mean hydrophobicity * - ``charge_threshold`` - int - - ``1`` + - ``0`` - Maximum charged residues per window .. code-block:: python diff --git a/docs/usage.rst b/docs/usage.rst index 2f1565f..bd40a16 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -470,7 +470,7 @@ The descriptors that accept custom parameters and their available keyword argume * - ``motif_composition`` - ``motifs`` (dict[str, str] | None, default None) * - ``aggregation_propensity`` - - ``window`` (int, default 5), ``hydrophobicity_threshold`` (float, default 2.0), ``charge_threshold`` (int, default 1) + - ``window`` (int, default 5), ``hydrophobicity_threshold`` (float, default 2.0), ``charge_threshold`` (int, default 0) * - ``hydrophobic_moment`` - ``window`` (int, default 11), ``angle`` (float, default 100) * - ``pseudo_amino_acid_composition`` diff --git a/protpy/_constants.py b/protpy/_constants.py index 9b8118a..0f02cb3 100644 --- a/protpy/_constants.py +++ b/protpy/_constants.py @@ -43,3 +43,37 @@ def _validate_sequence(sequence: str) -> str: f"Invalid amino acid found in protein sequence: {aa}." ) return sequence + + +def _validate_int(name: str, value, minimum: int = 0) -> int: + """ + Parse ``value`` as an int and check it is >= ``minimum``. + + Raises + ====== + ValueError + if ``value`` cannot be parsed as an int or is below ``minimum``. + """ + if isinstance(value, bool): + raise ValueError(f"Invalid {name} value input: {value}.") + try: + value = int(value) + except (ValueError, TypeError): + raise ValueError(f"Invalid {name} value input: {value}.") + if value < minimum: + raise ValueError(f"{name} must be >= {minimum}, got {value}.") + return value + + +def _validate_weight(weight) -> float: + """ + Check that ``weight`` is a non-negative number. + + Raises + ====== + ValueError + if ``weight`` is not a number or is negative. + """ + if isinstance(weight, bool) or not isinstance(weight, (int, float)) or weight < 0: + raise ValueError(f"weight must be a non-negative number, got {weight}.") + return weight diff --git a/protpy/autocorrelation.py b/protpy/autocorrelation.py index 60e189d..e959aa1 100644 --- a/protpy/autocorrelation.py +++ b/protpy/autocorrelation.py @@ -4,7 +4,6 @@ from __future__ import annotations -import numpy as np import pandas as pd import math from aaindex import aaindex1 @@ -25,7 +24,7 @@ """ #list of amino acids -from ._constants import amino_acids, _validate_sequence +from ._constants import amino_acids, _validate_sequence, _validate_int ############################### MoreauBroto Autocorrelation ############################### @@ -82,49 +81,13 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, #validate input protein sequence sequence = _validate_sequence(sequence) - #validate lag, set default lag if invalid value input - if (lag>=len(sequence) or (lag<0) or not (isinstance(lag, int))): + #validate lag; fall back to the default of 30 when lag exceeds the sequence length + lag = _validate_int("lag", lag) + if lag >= len(sequence): lag = 30 - #validate that at least 1 property input to function - if (properties == "" or properties == []): - raise ValueError('At least one property must be passed into function.') - - #if 1 property passed into function, wrap it to an iterable list - if (isinstance(properties, str)): - properties = [properties] - - #initialise dicts to store AAI properties and values - aai_properties = {} - for prop in properties: - #raise value error if accession number not found in aaindex1 - if not (prop in aaindex1.record_codes()): - raise ValueError(f"Property {prop} not found in list of available properties in the aaindex1:\n{properties}.") - aai_properties[prop] = {} - - #iterate through list of properties, getting property values for accession numbers from AAIndex1 - for prop in list(aai_properties.keys()): - #get property values from AAIndex - aaindex1[prop].values.pop('-', None) - prop_amino_acid_values = aaindex1[prop].values - - #normalise property values if applicable, calculate mean and std dev - aai_property_vals = {} - if (normalize): - for i, j in prop_amino_acid_values.items(): - aai_property_vals[i] = (j - (sum(prop_amino_acid_values.values()) / - len(prop_amino_acid_values.values()))) / _std(prop_amino_acid_values.values(), ddof=0) - else: - #use raw (unnormalized) property values - for i, j in prop_amino_acid_values.items(): - aai_property_vals[i] = j - aa_counter = 0 - - #assign property and associated amino acid values to aai_property_vals array - for i, j in aai_property_vals.items(): - aai_property_vals[amino_acids[aa_counter]] = aai_property_vals[i] - aa_counter+=1 - aai_properties[prop] = aai_property_vals + #get (optionally normalized) property values for each AAIndex accession number + aai_properties = _get_properties(properties, normalize) #store resultant autocorrelation values moreaubroto_autocorrelation = {} @@ -135,12 +98,9 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, temp = 0 # reset per lag for j in range(len(sequence)-i): temp = temp + aai_properties[key][sequence[j]] * aai_properties[key][sequence[j+i]] - if (len(sequence) - i == 0): - moreaubroto_autocorrelation["MBAuto_" + key + "_" + str(i)] = round( - temp / (len(sequence)), 3) - else: - moreaubroto_autocorrelation["MBAuto_" + key + "_" + str(i)] = round( - temp / (len(sequence) - i), 3) + #lags with no residue pairs (lag >= sequence length) are 0 + moreaubroto_autocorrelation["MBAuto_" + key + "_" + str(i)] = ( + round(temp / (len(sequence) - i), 3) if len(sequence) > i else 0.0) #transform values and columns to DataFrame moreaubroto_autocorrelation_df = pd.DataFrame([list(moreaubroto_autocorrelation.values())], @@ -180,50 +140,13 @@ def moran_autocorrelation(sequence: str, lag: int = 30, #validate input protein sequence sequence = _validate_sequence(sequence) - #validate lag, set default lag if invalid value input - if (lag>=len(sequence) or (lag<0) or not (isinstance(lag, int))): + #validate lag; fall back to the default of 30 when lag exceeds the sequence length + lag = _validate_int("lag", lag) + if lag >= len(sequence): lag = 30 - #validate at least 1 property input to function - if (properties == "" or properties == []): - raise ValueError('At least one property must be passed into function.') - - #if 1 property passed into function, wrap it to an iterable list - if (isinstance(properties, str)): - properties = [properties] - - #initialise dicts to store AAI properties and values - aai_properties = {} - for prop in properties: - #raise value error if accession number not found in aaindex1 - if not (prop in aaindex1.record_codes()): - raise ValueError(f"Property {prop} not found in list of available properties in the aaindex1:\n{properties}.") - aai_properties[prop] = {} - - #iterate through list of properties, getting property values for accession numbers from AAIndex1 - for prop in list(aai_properties.keys()): - #get property values from AAIndex - aaindex1[prop].values.pop('-', None) - prop_aminoacid_values = aaindex1[prop].values - - aai_property_vals = {} - - #normalise property values, calculate mean and std dev - if (normalize): - for i, j in prop_aminoacid_values.items(): - aai_property_vals[i] = (j - (sum(prop_aminoacid_values.values()) / - len(prop_aminoacid_values.values()))) / _std(prop_aminoacid_values.values(), ddof=0) - else: - #use raw (unnormalized) property values - for i, j in prop_aminoacid_values.items(): - aai_property_vals[i] = j - aa_counter = 0 - - #assign property and associated amino acid values to aai_property_vals array - for i, j in aai_property_vals.items(): - aai_property_vals[amino_acids[aa_counter]] = aai_property_vals[i] - aa_counter+=1 - aai_properties[prop] = aai_property_vals + #get (optionally normalized) property values for each AAIndex accession number + aai_properties = _get_properties(properties, normalize) #store resultant autocorrelation values moran_autocorrelation = {} @@ -243,12 +166,9 @@ def moran_autocorrelation(sequence: str, lag: int = 30, for j in range(len(sequence) - i): temp = temp + (aai_properties[key][sequence[j]] - prop_mean) * ( aai_properties[key][sequence[j+i]] - prop_mean) - if len(sequence) - i == 0: - moran_autocorrelation["MAuto_" + key + "_" + str(i)] = round( - temp / ((len(sequence)) / k), 5) - else: - moran_autocorrelation["MAuto_" + key + "_" + str(i)] = round( - temp / ((len(sequence) - i) / k), 5) + #Moran: (1/(N-d) * sum) / variance; 0 when there are no pairs or no variance + moran_autocorrelation["MAuto_" + key + "_" + str(i)] = ( + round(temp / (len(sequence) - i) / k, 5) if (len(sequence) > i and k > 0) else 0.0) #transform values and columns to DataFrame moran_autocorrelation_df = pd.DataFrame([list(moran_autocorrelation.values())], @@ -288,50 +208,13 @@ def geary_autocorrelation(sequence: str, lag: int = 30, #validate input protein sequence sequence = _validate_sequence(sequence) - #validate lag, set default lag if invalid value input - if (lag>=len(sequence) or (lag<0) or not (isinstance(lag, int))): + #validate lag; fall back to the default of 30 when lag exceeds the sequence length + lag = _validate_int("lag", lag) + if lag >= len(sequence): lag = 30 - #validate at least 1 property input to function - if (properties == "" or properties == []): - raise ValueError('At least one property must be passed into function.') - - #if 1 property passed into function, wrap it to an iterable list - if (isinstance(properties, str)): - properties = [properties] - - #initialise dicts to store AAI properties and values - aai_properties = {} - for prop in properties: - #raise value error if accession number not found in aaindex1 - if not (prop in aaindex1.record_codes()): - raise ValueError(f"Property {prop} not found in list of available properties in the aaindex1:\n{properties}.") - aai_properties[prop] = {} - - #iterate through list of properties, getting property values for accession numbers from AAIndex1 - for prop in list(aai_properties.keys()): - #get property values from AAIndex & reshape - aaindex1[prop].values.pop('-', None) - prop_aminoacid_values = aaindex1[prop].values - - aai_property_vals = {} - - #normalise property values, calculate mean and std dev - if (normalize): - for i, j in prop_aminoacid_values.items(): - aai_property_vals[i] = (j - (sum(prop_aminoacid_values.values()) / - len(prop_aminoacid_values.values()))) / _std(prop_aminoacid_values.values(), ddof=0) - else: - #use raw (unnormalized) property values - for i, j in prop_aminoacid_values.items(): - aai_property_vals[i] = j - aa_counter = 0 - - #assign property and associated amino acid values to aai_property_vals array - for i, j in aai_property_vals.items(): - aai_property_vals[amino_acids[aa_counter]] = aai_property_vals[i] - aa_counter+=1 - aai_properties[prop] = aai_property_vals + #get (optionally normalized) property values for each AAIndex accession number + aai_properties = _get_properties(properties, normalize) #store resultant autocorrelation values geary_autocorrelation = {} @@ -341,18 +224,15 @@ def geary_autocorrelation(sequence: str, lag: int = 30, cc = [] for aa in sequence: cc.append(aai_properties[key][aa]) - k = ((np.std(cc, ddof=0)) ** 2) * len(sequence) / (len(sequence) - 1) + k = ((_std(cc, ddof=0)) ** 2) * len(sequence) / (len(sequence) - 1) if len(sequence) > 1 else 0.0 for i in range(1, lag+1): temp = 0 for j in range(len(sequence) - i): temp = (temp + ( aai_properties[key][sequence[j]] - aai_properties[key][sequence[j+i]]) ** 2) - if len(sequence) - i == 0: - geary_autocorrelation["GAuto_" + key + "_" + str(i)] = round( - temp / (2* (len(sequence))) / k, 3) - else: - geary_autocorrelation["GAuto_" + key + "_" + str(i)] = round( - temp / (2* (len(sequence) -i)) / k, 3) + #0 when there are no pairs or no variance + geary_autocorrelation["GAuto_" + key + "_" + str(i)] = ( + round(temp / (2* (len(sequence) -i)) / k, 3) if (len(sequence) > i and k > 0) else 0.0) #transform values and columns to DataFrame geary_autocorrelation_df = pd.DataFrame([list(geary_autocorrelation.values())], @@ -360,6 +240,48 @@ def geary_autocorrelation(sequence: str, lag: int = 30, return geary_autocorrelation_df +def _get_properties(properties: list[str] | str, normalize: bool) -> dict[str, dict[str, float]]: + """ + Get the values of the 20 canonical amino acids for each AAIndex1 accession number, + optionally normalized to zero mean and unit standard deviation. Values are copied + so the aaindex records themselves are never modified. + + Parameters + ========== + :properties: list[str] | str + AAIndex1 accession number(s). + :normalize: bool + normalize the property values. + + Returns + ======= + :aai_properties: dict + {accession_number: {amino_acid: value}}. + """ + #validate that at least 1 property input to function + if (properties == "" or properties == [] or properties is None): + raise ValueError('At least one property must be passed into function.') + + #if 1 property passed into function, wrap it to an iterable list + if (isinstance(properties, str)): + properties = [properties] + + record_codes = aaindex1.record_codes() + aai_properties = {} + for prop in properties: + #raise value error if accession number not found in aaindex1 + if prop not in record_codes: + raise ValueError(f"Property {prop} not found in list of available properties in the aaindex1:\n{properties}.") + #keep only the 20 canonical amino acids (drops the '-' gap entry) + vals = {aa: v for aa, v in aaindex1[prop].values.items() if aa in amino_acids} + if normalize: + mean = sum(vals.values()) / len(vals) + std = _std(list(vals.values()), ddof=0) + vals = {aa: (v - mean) / std if std else 0.0 for aa, v in vals.items()} + aai_properties[prop] = vals + + return aai_properties + def _std(array: list[float], ddof: int = 1) -> float: """ Calculate the standard deviation of the array data, with associated means Delta Degrees diff --git a/protpy/composition.py b/protpy/composition.py index 827fed5..678c888 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -6,6 +6,7 @@ import re import itertools +from collections import Counter import pandas as pd import math from aaindex import aaindex1 @@ -63,7 +64,7 @@ class prediction: new methods of statistical classification. Proc Int Conf 299, 371-374. https://doi.org/10.1038/299371a0 """ #list of amino acids -from ._constants import amino_acids, _validate_sequence +from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight #physicochemical values for all amino acids for hydrophobicity, hydrophilicty and residue mass #values taken from http://www.csbio.sjtu.edu.cn/bioinf/PseAAC/ParaValue.htm @@ -166,12 +167,16 @@ def dipeptide_composition(sequence: str) -> pd.DataFrame: dipeptide_composition = {} + #count overlapping dipeptides with a sliding window (str.count misses overlaps, e.g. "AA" in "AAA") + total_dipeptides = len(sequence) - 1 + counts = Counter(sequence[i:i+2] for i in range(total_dipeptides)) + #iterate through each amino acid, calculating each dipeptide combination's composition for i in amino_acids: for j in amino_acids: dipep = i + j dipeptide_composition[dipep] = round( - float(sequence.count(dipep)) / (len(sequence)-1) * 100, 2) + counts[dipep] / total_dipeptides * 100, 2) if total_dipeptides > 0 else 0.0 #transform values and columns to DataFrame dipeptide_composition_df = pd.DataFrame([list(dipeptide_composition.values())], @@ -224,8 +229,9 @@ def tripeptide_composition(sequence: str) -> pd.DataFrame: #get fractional frequency of each tripeptide (percentage), consistent with dipeptide_composition total_tripeptides = len(sequence) - 2 + counts = Counter(sequence[i:i+3] for i in range(max(total_tripeptides, 0))) for i in tripeptides: - tripeptide_composition[i] = round(sequence.count(i) / total_tripeptides * 100, 3) if total_tripeptides > 0 else 0.0 + tripeptide_composition[i] = round(counts[i] / total_tripeptides * 100, 3) if total_tripeptides > 0 else 0.0 #transform values and columns to DataFrame tripeptide_composition_df = pd.DataFrame([list(tripeptide_composition.values())], @@ -422,7 +428,7 @@ def isoelectric_point(sequence: str) -> pd.DataFrame: """ Calculate the theoretical Isoelectric Point (pI) of a protein sequence. pI is the pH at which the net charge of the protein is zero. It is computed - by iteratively adjusting pH until positive and negative charges balance [11]. + by bisecting the pH range 0-14 until positive and negative charges balance [11]. Parameters ========== @@ -440,9 +446,7 @@ def isoelectric_point(sequence: str) -> pd.DataFrame: #count ionisable residues counts = {aa: sequence.count(aa) for aa in "DEHCYKR"} - #iterative charge-balance: adjust pH by +/- 0.001 until net charge < 1e-4 - ph = 7.0 - for _ in range(5000): + def _net_charge(ph: float) -> float: pos = (counts["K"] / (1 + 10 ** (ph - pka_values_["K"])) + counts["R"] / (1 + 10 ** (ph - pka_values_["R"])) + counts["H"] / (1 + 10 ** (ph - pka_values_["H"])) + @@ -452,10 +456,17 @@ def isoelectric_point(sequence: str) -> pd.DataFrame: counts["C"] / (1 + 10 ** (pka_values_["C"] - ph)) + counts["Y"] / (1 + 10 ** (pka_values_["Y"] - ph)) + 1.0 / (1 + 10 ** (pka_values_["C_term"] - ph))) - net_charge = pos - neg - if abs(net_charge) < 1e-4: - break - ph += 0.001 if net_charge > 0 else -0.001 + return pos - neg + + #net charge decreases monotonically with pH, so bisect over the full 0-14 range + low, high = 0.0, 14.0 + for _ in range(50): + mid = (low + high) / 2 + if _net_charge(mid) > 0: + low = mid + else: + high = mid + ph = (low + high) / 2 #transform to DataFrame isoelectric_point_df = pd.DataFrame([[round(ph, 3)]], columns=["IsoelectricPoint"]) @@ -467,7 +478,7 @@ def isoelectric_point(sequence: str) -> pd.DataFrame: #average residue molecular weights (Da) for each of the 20 standard amino acids [12] residue_molecular_weight_ = { "A": 89.094, "C": 121.154, "D": 133.104, "E": 147.130, "F": 165.192, - "G": 75.032, "H": 155.156, "I": 131.174, "K": 146.189, "L": 131.174, + "G": 75.067, "H": 155.156, "I": 131.174, "K": 146.189, "L": 131.174, "M": 149.208, "N": 132.119, "P": 115.132, "Q": 146.146, "R": 174.203, "S": 105.093, "T": 119.119, "V": 117.148, "W": 204.228, "Y": 181.191 } @@ -656,9 +667,8 @@ def kmer_composition(sequence: str, k: int = 2) -> pd.DataFrame: #validate input protein sequence sequence = _validate_sequence(sequence) - #validate k; default to 2 if invalid; raise error for impractically large k - if not isinstance(k, int) or k < 1: - k = 2 + #validate k; raise error for impractically large k + k = _validate_int("k", k, 1) if k > 4: raise ValueError( f"k={k} would generate {20**k:,} columns (20^k). Maximum supported k is 4." @@ -668,11 +678,11 @@ def kmer_composition(sequence: str, k: int = 2) -> pd.DataFrame: all_kmers = ["".join(p) for p in itertools.product(amino_acids, repeat=k)] total = len(sequence) - k + 1 + #count every (overlapping) k-mer in one pass, divide by total k-mer count + counts = Counter(sequence[i:i+k] for i in range(max(total, 0))) kmer_comp = {} - #count each k-mer occurrence, divide by total k-mer count for kmer in all_kmers: - cnt = sum(1 for i in range(total) if sequence[i:i+k] == kmer) - kmer_comp[kmer] = round(cnt / total * 100, 3) if total > 0 else 0.0 + kmer_comp[kmer] = round(counts[kmer] / total * 100, 3) if total > 0 else 0.0 kmer_composition_df = pd.DataFrame([list(kmer_comp.values())], columns=list(kmer_comp.keys())) @@ -748,7 +758,7 @@ def reduced_alphabet_composition(sequence: str, alphabet_size: int = 6) -> pd.Da "CxxC_zinc_finger": r"C.{2}C", "CAAX_prenylation": r"C[ACFILMVWY]{2}[ACEILMQV]$", "cAMP_PKA": r"RR.S", - "dileucine_sorting": r"[DE]xxxL[LI]$", + "dileucine_sorting": r"[DE]...L[LI]", "PEST_region": r"P.{1,10}[ESD].{1,10}T" } @@ -824,13 +834,13 @@ def _aa_class(aa): return "Charged" total = len(sequence) - 1 + counts = Counter(sequence[k:k+2] for k in range(total)) pair_comp = {} #build the 400 annotated columns and compute frequencies for i in amino_acids: for j in amino_acids: label = f"{i}{j}_{_aa_class(i)}-{_aa_class(j)}" - count = sum(1 for k in range(total) if sequence[k] == i and sequence[k+1] == j) - pair_comp[label] = round(count / total * 100, 3) if total > 0 else 0.0 + pair_comp[label] = round(counts[i + j] / total * 100, 3) if total > 0 else 0.0 pair_composition_df = pd.DataFrame([list(pair_comp.values())], columns=list(pair_comp.keys())) @@ -921,21 +931,21 @@ def extinction_coefficient(sequence: str) -> pd.DataFrame: ################################ Boman Index ############################################## -#solubility scale from Boman (2003) [18] +#side-chain solubility values (Radzicka & Wolfenden) used by Boman (2003) [18] boman_scale_ = { - "A": 2.1, "C": -0.7, "D": -3.5, "E": -3.5, "F": 2.8, "G": 2.1, - "H": -3.2, "I": 4.5, "K": -3.9, "L": 4.5, "M": 1.9, "N": -4.8, - "P": -1.6, "Q": -3.5, "R": -0.8, "S": -0.8, "T": -0.7, "V": 4.2, - "W": -0.9, "Y": -1.3 + "A": 1.81, "C": 1.28, "D": -8.72, "E": -6.81, "F": 2.98, "G": 0.94, + "H": -4.66, "I": 4.92, "K": -5.55, "L": 4.92, "M": 2.35, "N": -6.64, + "P": 0.00, "Q": -5.54, "R": -14.92, "S": -3.40, "T": -2.57, "V": 4.04, + "W": 2.33, "Y": -0.14 } def boman_index(sequence: str) -> pd.DataFrame: """ Calculate the Boman Index (potential protein interaction index) for a protein sequence. The index is the sum of the solubility values of - all residues divided by the sequence length [18]: + all residues divided by the sequence length, negated [18]: - BI = sum(solubility(aa_i)) / N + BI = -sum(solubility(aa_i)) / N Higher values indicate a greater tendency to bind other proteins or cell membranes (e.g. antimicrobial peptides). Values >2.48 suggest @@ -954,8 +964,8 @@ def boman_index(sequence: str) -> pd.DataFrame: #validate input protein sequence sequence = _validate_sequence(sequence) - #sum solubility values and divide by sequence length - bi = round(sum(boman_scale_[aa] for aa in sequence) / len(sequence), 3) + #negated mean of the solubility values + bi = round(-sum(boman_scale_[aa] for aa in sequence) / len(sequence), 3) boman_index_df = pd.DataFrame([[bi]], columns=["BomanIndex"]) @@ -963,13 +973,13 @@ def boman_index(sequence: str) -> pd.DataFrame: ############################### Aggregation Propensity #################################### -def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_threshold: float = 2.0, charge_threshold: int = 1) -> pd.DataFrame: +def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_threshold: float = 2.0, charge_threshold: int = 0) -> pd.DataFrame: """ Calculate the Aggregation Propensity of a protein sequence. Aggregation- prone regions (APRs) are identified using a sliding window: a window is classified as an APR when its mean Kyte-Doolittle hydrophobicity exceeds ``hydrophobicity_threshold`` and the number of charged residues (D, E, K, - R) is below ``charge_threshold`` [8]. + R) is at most ``charge_threshold`` [8]. Parameters ========== @@ -979,7 +989,7 @@ def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_thresh sliding window length in residues. :hydrophobicity_threshold: float (default=2.0) minimum mean Kyte-Doolittle hydrophobicity for an APR window. - :charge_threshold: int (default=1) + :charge_threshold: int (default=0) maximum number of charged residues (D, E, K, R) allowed in an APR. Returns @@ -992,6 +1002,8 @@ def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_thresh #validate input protein sequence sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + n = len(sequence) charged = frozenset("DEKR") #track which residue positions are covered by at least one APR @@ -1003,7 +1015,7 @@ def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_thresh seg = sequence[i:i + window] avg_hydro = sum(kyte_doolittle_[aa] for aa in seg) / window n_charged = sum(1 for aa in seg if aa in charged) - if avg_hydro >= hydrophobicity_threshold and n_charged < charge_threshold: + if avg_hydro >= hydrophobicity_threshold and n_charged <= charge_threshold: apr_count += 1 for j in range(i, i + window): covered[j] = True @@ -1099,6 +1111,8 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> p #validate input protein sequence sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + n = len(sequence) #use full sequence as single window when shorter than default window effective_window = min(window, n) @@ -1159,13 +1173,11 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float #validate input protein sequence sequence = _validate_sequence(sequence) - #set lamda to its default value of 30 if <0, or > sequence len or not an int - if ((lamda < 0) or (lamda > len(sequence)) or not isinstance(lamda, int)): - lamda = 30 - - #validate weight, set default weight of 0.05 if invalid value input - if ((weight < 0) or not (isinstance(weight, (float, int)))): - weight = 0.05 + #validate lamda & weight; lamda falls back to its default of 30 when > sequence length + lamda = _validate_int("lamda", lamda) + if lamda > len(sequence): + lamda = 30 + weight = _validate_weight(weight) #normalize None to empty list before further processing if properties is None: @@ -1185,7 +1197,9 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float for prop in properties: if (prop not in aaindex1.record_codes()): raise ValueError(f"Accession number not found in aaindex1: {prop}.") - property_values.append(aaindex1[prop].values) + #keep only the 20 canonical amino acids (drops the '-' gap entry) + property_values.append( + {aa: v for aa, v in aaindex1[prop].values.items() if aa in amino_acids}) psuedo_aac = {} @@ -1235,15 +1249,23 @@ def sequence_order_correlation_factor(sequence: str, k: int = 1, properties: lis # default to empty list if not provided (avoid mutable default argument) if properties is None: properties = [] + + # If there are no valid residue pairs for the requested gap, return 0.0. + if len(sequence) <= k: + return 0.0 + + #normalize each property once rather than for every residue pair + normalized_props = [normalize_property(prop) for prop in properties] correlation_factor = [] - #iterate through sequence using correlation function to calculate sequence order - #between its amino acids with gap=k + #iterate through sequence calculating the correlation (as in correlation_function) + #between its amino acids with gap=k for i in range(len(sequence) - k): aa1 = sequence[i] aa2 = sequence[i + k] - correlation_factor.append(correlation_function(aa1, aa2, properties)) - + theta = sum(math.pow(norm[aa1] - norm[aa2], 2) for norm in normalized_props) + correlation_factor.append(round(theta / len(normalized_props), 3)) + #round array of correlation factors correlation_factor_ = round(sum(correlation_factor) / (len(sequence) - k), 3) @@ -1300,8 +1322,13 @@ def normalize_property(properties: dict[str, float]) -> dict[str, float]: dict of normalized property values. """ vals = list(properties.values()) + if not vals: + return {} + mean = sum(vals) / len(vals) std = _std(vals, mean, ddof=0) + if std == 0: + return {aa: 0.0 for aa, _ in properties.items()} return {aa: (v - mean) / std for aa, v in properties.items()} def _std(prop: list[float], mean: float, ddof: int = 1) -> float: @@ -1367,13 +1394,11 @@ def amphiphilic_pseudo_amino_acid_composition(sequence: str, lamda: int = 30, we #validate input protein sequence sequence = _validate_sequence(sequence) - #set lamda to its default value of 30 if <0, or > sequence len or not an int - if ((lamda < 0) or (lamda > len(sequence)) or not isinstance(lamda, int)): + #validate lamda & weight; lamda falls back to its default of 30 when > sequence length + lamda = _validate_int("lamda", lamda) + if lamda > len(sequence): lamda = 30 - - #validate weight, set default weight of 0.5 if invalid value input - if ((weight < 0) or not (isinstance(weight, (float, int)))): - weight = 0.5 + weight = _validate_weight(weight) #cast properties to list if not isinstance(properties, list): @@ -1434,6 +1459,11 @@ def amphiphilic_sequence_order_correlation_factor(sequence: str, k: int = 1, if properties is None: properties = [hydrophobicity_, hydrophilicity_] + #no residue pairs for the requested gap: every correlation factor is 0 + denom = len(sequence) - k + if denom <= 0: + return [0.0] * len(properties) + #pre-normalize all properties once rather than inside every pair evaluation normalized_props = [normalize_property(prop) for prop in properties] n_props = len(properties) @@ -1445,7 +1475,6 @@ def amphiphilic_sequence_order_correlation_factor(sequence: str, k: int = 1, for p_idx, norm in enumerate(normalized_props): accumulators[p_idx].append(round(norm[aa1] * norm[aa2], 3)) - denom = len(sequence) - k return [round(sum(acc) / denom, 3) for acc in accumulators] def amphiphilic_correllation_function(Ri: str = "S", Rj: str = "D", diff --git a/protpy/conjoint_triad.py b/protpy/conjoint_triad.py index a91b2c6..c8ca2b1 100644 --- a/protpy/conjoint_triad.py +++ b/protpy/conjoint_triad.py @@ -4,6 +4,7 @@ from __future__ import annotations +from collections import Counter import pandas as pd """ @@ -63,12 +64,15 @@ def conjoint_triad(sequence: str) -> pd.DataFrame: for i in _aa_triads: protein_num = protein_num.replace(i, str(_aa_triads[i])) + #count overlapping triads with a sliding window (str.count misses overlaps, e.g. "111" in "11111") + triad_counts = Counter(protein_num[i:i+3] for i in range(len(protein_num) - 2)) + #calculate 7 triads for amino acids for i in range(1, 8): for j in range(1, 8): for k in range(1, 8): temp = str(i) + str(j) + str(k) - conjoint_triad["conj_triad_" + temp] = protein_num.count(temp) + conjoint_triad["conj_triad_" + temp] = triad_counts[temp] #transform descriptor values to dataframe conjoint_triad_df = pd.DataFrame([list(conjoint_triad.values())], columns=list(conjoint_triad.keys())) diff --git a/protpy/ctd.py b/protpy/ctd.py index 7dd07c9..91587ec 100644 --- a/protpy/ctd.py +++ b/protpy/ctd.py @@ -28,10 +28,10 @@ hydrophobicity = {"name": "hydrophobicity", "1": "RKEDQN", "2": "GASTPHY", "3": "CLVIMFW"} # '1' -> Polar; '2' -> Neutral, '3' -> Hydrophobicity -normalized_vdwv = {"name": "normalized_vdwv", "1": "GASTPD", "2": "NVEQIL", "3": "MHKFRYW"} +normalized_vdwv = {"name": "normalized_vdwv", "1": "GASTPDC", "2": "NVEQIL", "3": "MHKFRYW"} # '1' -> (0-2.78); '2' -> (2.95-4.0), '3' -> (4.03-8.08) -polarity = {"name": "polarity", "1": "LIFWCMVY", "2": "CPNVEQIL", "3": "KMHFRYW"} +polarity = {"name": "polarity", "1": "LIFWCMVY", "2": "PAGST", "3": "HQRKNED"} # '1' -> (4.9-6.2); '2' -> (8.0-9.2), '3' -> (10.4-13.0) charge = {"name": "charge", "1": "KR", "2": "ANCQGHILMFPSTWYV", "3": "DE"} @@ -179,13 +179,18 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFr ctd_transition_ = {} + #a single residue has no transitions + n_pairs = len(sequence) - 1 + if n_pairs == 0: + n_pairs = 1 + #calculate descriptor values, append to ctd_transition_ dict ctd_transition_["CTD_T_12_" + prop["name"]] = round( - float(seq.count("12") + seq.count("21")) / (len(sequence)-1), 3) + float(seq.count("12") + seq.count("21")) / n_pairs, 3) ctd_transition_["CTD_T_13_" + prop["name"]] = round( - float(seq.count("13") + seq.count("31")) / (len(sequence)-1), 3) + float(seq.count("13") + seq.count("31")) / n_pairs, 3) ctd_transition_["CTD_T_23_" + prop["name"]] = round( - float(seq.count("23") + seq.count("32")) / (len(sequence)-1), 3) + float(seq.count("23") + seq.count("32")) / n_pairs, 3) #transform values and columns to DataFrame ctd_transition_df = pd.DataFrame([list(ctd_transition_.values())], columns=list(ctd_transition_.keys())) diff --git a/protpy/encode.py b/protpy/encode.py index de8a326..cdc961a 100644 --- a/protpy/encode.py +++ b/protpy/encode.py @@ -5,6 +5,7 @@ from __future__ import annotations import os +import csv import pandas as pd from tqdm import tqdm as _tqdm @@ -23,7 +24,6 @@ kmer_composition, reduced_alphabet_composition, motif_composition, - amino_acid_pair_composition, aliphatic_index, extinction_coefficient, boman_index, @@ -69,7 +69,7 @@ def all_descriptors( sequence: str, params: dict[str, dict] | None = None, show_progress: bool = False, - output_file: str | None = "descriptors.csv", + output_file: str | None = None, save_each_sequence: bool = True, _show_descriptor_list: bool = True, ) -> pd.DataFrame: @@ -131,11 +131,13 @@ def all_descriptors( When ``True``, display a ``tqdm`` progress bar that names the descriptor currently being computed and tracks overall progress. - :output_file: str | None (default="descriptors.csv") - Path to the CSV file where results are written. Pass ``None`` to - disable all file output. When calling this function in a loop over + :output_file: str | None (default=None) + Path to the CSV file where results are written. ``None`` (default) + disables all file output. When calling this function in a loop over multiple sequences the rows are accumulated in the file across calls; - each call appends one row to any previously saved sequences. + each call appends one row to any previously saved sequences. A + ``ValueError`` is raised, and the file left untouched, if an existing + file has different columns. :save_each_sequence: bool (default=True) When ``True``, the output CSV is updated once after all descriptors @@ -210,7 +212,6 @@ def _p(name: str) -> dict: if _kmer_k not in (2, 3) else []), ("[Composition] Reduced alphabet composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))), ("[Composition] Motif composition", lambda: motif_composition(sequence, **_p("motif_composition"))), - ("[Composition] Amino acid pair composition", lambda: amino_acid_pair_composition(sequence)), ("[Composition] Aliphatic index", lambda: aliphatic_index(sequence)), ("[Composition] Extinction coefficient", lambda: extinction_coefficient(sequence)), ("[Composition] Boman index", lambda: boman_index(sequence)), @@ -226,7 +227,7 @@ def _p(name: str) -> dict: # ── Conjoint Triad ───────────────────────────────────────────────────── ("[Conjoint Triad] Conjoint triad", lambda: _conjoint_triad(sequence)), # ── CTD ──────────────────────────────────────────────────────────────── - ("[CTD] Composition, transition & distribution",lambda: ctd_(sequence, all_ctd=True, **_p("ctd_"))), + ("[CTD] Composition, transition & distribution",lambda: ctd_(sequence, **{"all_ctd": True, **_p("ctd_")})), # ── Sequence Order ───────────────────────────────────────────────────── ("[Sequence Order] Sequence order coupling number", lambda: sequence_order_coupling_number_all(sequence, **_p("sequence_order_coupling_number_all"))), ("[Sequence Order] Quasi sequence order", lambda: quasi_sequence_order_all(sequence, **_p("quasi_sequence_order_all"))), @@ -236,20 +237,6 @@ def _p(name: str) -> dict: if _show_descriptor_list: print("Calculating descriptors: " + ", ".join(descriptor_names)) - # Load any rows already saved from previous sequences in this run so they - # are preserved when we rewrite the file after each checkpoint. - existing_df: pd.DataFrame | None = None - if output_file is not None and os.path.isfile(output_file): - try: - existing_df = pd.read_csv(output_file) - except Exception: - existing_df = None - - def _checkpoint(frames_so_far: list[pd.DataFrame]) -> None: - partial = pd.concat(frames_so_far, axis=1) - out = pd.concat([existing_df, partial], ignore_index=True) if existing_df is not None else partial - out.to_csv(output_file, index=False) - frames: list[pd.DataFrame] = [] if show_progress: @@ -272,7 +259,8 @@ def _checkpoint(frames_so_far: list[pd.DataFrame]) -> None: result = pd.concat(frames, axis=1) if output_file is not None and save_each_sequence: - _checkpoint(frames) + _prepare_output_file(output_file, list(result.columns)) + result.to_csv(output_file, mode="a", header=False, index=False) return result @@ -318,10 +306,13 @@ def encode_dataset( :output_file: str | None (default="descriptors.csv") Path to the CSV file where results are written. Each completed row - (identifier prepended) is flushed immediately after that sequence + (identifier prepended) is appended immediately after that sequence finishes, so progress is preserved if the process is interrupted. - Rows already present in the file from a previous run are retained. - Pass ``None`` to disable all file output. + Rows already present in the file from a previous run are retained, + except rows whose identifier is encoded again in this run, which are + replaced. A ``ValueError`` is raised, and the file left untouched, if + an existing file has different columns. Pass ``None`` to disable all + file output. :save_each_sequence: bool (default=True) When ``True``, the CSV is updated after each sequence completes. @@ -394,14 +385,6 @@ def encode_dataset( f"sequences must be a list[str] or pd.DataFrame, got {type(sequences)}." ) - # Snapshot any previously saved rows so they are preserved on each flush. - initial_df: pd.DataFrame | None = None - if output_file is not None and os.path.isfile(output_file): - try: - initial_df = pd.read_csv(output_file) - except Exception: - pass - completed: list[pd.DataFrame] = [] total = len(seqs) @@ -428,21 +411,43 @@ def encode_dataset( completed.append(row_df) if output_file is not None and save_each_sequence: - new_rows = pd.concat(completed, ignore_index=True) - # Only prepend the initial snapshot when its columns exactly match the - # current run (mismatches arise from stale CSVs with mangled duplicate - # column names written by a prior buggy run). - if initial_df is not None and list(initial_df.columns) == list(new_rows.columns): - out = pd.concat([initial_df, new_rows], ignore_index=True) - else: - out = new_rows - out.to_csv(output_file, index=False) + if idx == 1: + _prepare_output_file(output_file, list(row_df.columns), replace_ids=ids) + row_df.to_csv(output_file, mode="a", header=False, index=False) if not completed: return pd.DataFrame() return pd.concat(completed, ignore_index=True) +def _prepare_output_file(output_file: str, columns: list, replace_ids: list | None = None) -> None: + """ + Get ``output_file`` ready for rows to be appended. A missing or empty file + gets a header row. An existing file must have exactly ``columns`` as its + header, otherwise a ValueError is raised and the file is not modified. + When ``replace_ids`` is given, existing rows whose first column matches + one of them are removed so a re-run does not duplicate them. + """ + if not os.path.isfile(output_file) or os.path.getsize(output_file) == 0: + with open(output_file, "w", newline="") as fh: + csv.writer(fh).writerow(columns) + return + + with open(output_file, newline="") as fh: + rows = list(csv.reader(fh)) + if rows[0] != [str(c) for c in columns]: + raise ValueError( + f"Existing output file '{output_file}' has different columns from this run; " + "refusing to overwrite it. Pass a different output_file or remove the file." + ) + if replace_ids: + replace = {str(i) for i in replace_ids} + kept = [rows[0]] + [r for r in rows[1:] if r and r[0] not in replace] + if len(kept) != len(rows): + with open(output_file, "w", newline="") as fh: + csv.writer(fh).writerows(kept) + + def _parse_fasta(filepath: str) -> list[str]: sequences: list[str] = [] current: list[str] = [] diff --git a/protpy/sequence_order.py b/protpy/sequence_order.py index e783c51..1629ec1 100644 --- a/protpy/sequence_order.py +++ b/protpy/sequence_order.py @@ -31,7 +31,7 @@ """ #list of amino acids -from ._constants import amino_acids, _validate_sequence +from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight @functools.lru_cache(maxsize=2) def _load_distance_matrix(name: str) -> dict: @@ -129,11 +129,8 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, #validate input protein sequence sequence = _validate_sequence(sequence) - #raise value error if int cant be parsed from input lag - try: - lag = int(lag) - except (ValueError, TypeError): - raise ValueError(f"Invalid lag value input: {lag}.") + #raise value error if a non-negative int cant be parsed from input lag + lag = _validate_int("lag", lag) #set dataframe column name prefixes based on input distance matrix, raise error if invalid name input col_prefix = "" @@ -145,7 +142,7 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, raise ValueError(f"Invalid distance matrix name input {distance_matrix}, values can be schneider-wrede or grantham.") #validate lag, set default lag of 30 if invalid value input - if (lag>=len(sequence) or (lag<0)): + if (lag>=len(sequence)): lag = 30 seq_order = {} @@ -191,14 +188,11 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataF #validate input protein sequence sequence = _validate_sequence(sequence) - #raise value error if int cant be parsed from input lag - try: - lag = int(lag) - except (ValueError, TypeError): - raise ValueError(f"Invalid lag value input: {lag}.") + #raise value error if a non-negative int cant be parsed from input lag + lag = _validate_int("lag", lag) #validate lag, set default lag of 30 if invalid value input - if (lag>=len(sequence) or (lag<0)): + if (lag>=len(sequence)): lag = 30 #calculate SOCN using schneider-wrede distance matrix @@ -248,19 +242,15 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, #validate input protein sequence sequence = _validate_sequence(sequence) - #raise value error if int can't be parsed from input lag - try: - lag = int(lag) - except (ValueError, TypeError): - raise ValueError(f"Invalid lag value input: {lag}.") + #raise value error if a non-negative int can't be parsed from input lag + lag = _validate_int("lag", lag) #validate lag, set default lag of 30 if invalid value input - if (lag>=len(sequence) or (lag<0)): + if (lag>=len(sequence)): lag = 30 - #validate weight, set default weight of 0.1 if invalid value input - if ((weight < 0) or not (isinstance(weight, (float, int)))): - weight = 0.1 + #validate weight + weight = _validate_weight(weight) #set dataframe column name prefixes based on input distance matrix, raise error if invalid name input col_prefix = "" @@ -272,29 +262,19 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, raise ValueError(f"Invalid distance matrix name input {distance_matrix}, values can be schneider-wrede or grantham.") quasi_sequence_order = {} - right_part = 0.0 - #calculate first 20 quasi sequence order using sequence order coupling number for - #proteins using lag and specificed physicochemical distance matrix - for aa in range(lag): - right_part = right_part + sequence_order_coupling_number_(sequence, aa+1, distance_matrix) + #calculate SOCN for each gap up to lag once, using the specified distance matrix + _right_part = [sequence_order_coupling_number_(sequence, i+1, distance_matrix) for i in range(lag)] #get amino acid composition aa_comp = composition.amino_acid_composition(sequence) - #iterate over amino acids, calculating descriptor value - temp = 1 + weight * right_part + #iterate over amino acids, calculating first 20 descriptor values + temp = 1 + weight * sum(_right_part) for index, aa in enumerate(amino_acids): quasi_sequence_order[col_prefix + str(index + 1)] = round(aa_comp[aa].values[0] / temp, 6) - - #calculate SOCN up until maxlag - _right_part = [] - for i in range(lag): - _right_part.append( - sequence_order_coupling_number_(sequence, i+1, distance_matrix)) - #calculate the last maxlag descriptor values - temp = 1 + weight * sum(_right_part) + #calculate the last lag descriptor values for index in range(20, 20 + lag): quasi_sequence_order[col_prefix + str(index + 1)] = round( weight * _right_part[index - 20] / temp, 6) @@ -335,19 +315,15 @@ def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1) #validate input protein sequence sequence = _validate_sequence(sequence) - #raise value error if int can't be parsed from input lag - try: - lag = int(lag) - except (ValueError, TypeError): - raise ValueError(f"Invalid lag value input: {lag}.") + #raise value error if a non-negative int can't be parsed from input lag + lag = _validate_int("lag", lag) #validate lag, set default lag of 30 if invalid value input - if (lag>=len(sequence) or (lag<0)): + if (lag>=len(sequence)): lag = 30 - #validate weight, set default weight if invalid value input - if ((weight < 0) or not (isinstance(weight, (float, int)))): - weight = 0.1 + #validate weight + weight = _validate_weight(weight) #calculate quasi seq order using schneider distance matrix quasi_schneider = quasi_sequence_order(sequence, lag, weight=weight, diff --git a/protpy/utils.py b/protpy/utils.py index fbd3a39..b7585de 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -274,7 +274,7 @@ }, "boman_index": { "description": ( - "Boman Index (potential protein interaction index) — sum of residue " + "Boman Index (potential protein interaction index) — negated sum of residue " "solubility values divided by sequence length. Values > 2.48 suggest " "high protein-binding potential." ), @@ -301,7 +301,7 @@ "sequence": "str — protein sequence", "window": "int (default=5) — sliding window length", "hydrophobicity_threshold": "float (default=2.0) — minimum mean Kyte-Doolittle hydrophobicity", - "charge_threshold": "int (default=1) — max charged residues allowed in an APR window", + "charge_threshold": "int (default=0) — max charged residues allowed in an APR window", }, "output_shape": "1 x 2 [AggregProneRegions, AggregProneFraction]", "speed": "Fast", @@ -321,7 +321,7 @@ "window": "int (default=11) — helical window size", "angle": "float (default=100) — residue rotation angle in degrees (100° for alpha-helix)", }, - "output_shape": "1 x 1", + "output_shape": "1 x 2 [HydrophobicMoment_Mean, HydrophobicMoment_Max]", "speed": "Fast", "reference": "[19] Eisenberg et al. (1982). Nature, 299, 371-374.", }, diff --git a/tests/test_autocorrelation.py b/tests/test_autocorrelation.py index f1e3e2a..3581251 100644 --- a/tests/test_autocorrelation.py +++ b/tests/test_autocorrelation.py @@ -259,7 +259,7 @@ def test_moran_autocorrelation(self): properties=["CIDH920105"], normalize=False) moran_nf_expected = pd.DataFrame(columns=["MAuto_CIDH920105_1", "MAuto_CIDH920105_2", "MAuto_CIDH920105_3", "MAuto_CIDH920105_4", "MAuto_CIDH920105_5"]) - moran_nf_expected.loc[0] = [-0.05779, -0.0779, -0.04184, -0.02121, -0.05371] + moran_nf_expected.loc[0] = [-0.05781, -0.07792, -0.04185, -0.02122, -0.05372] self.assertIsInstance(moran_nf_seq1, pd.DataFrame, 'Expected output to be of type DataFrame, got {}.'.format(type(moran_nf_seq1))) self.assertEqual(moran_nf_seq1.shape, (1, lag), diff --git a/tests/test_composition.py b/tests/test_composition.py index d4500e3..0963701 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -11,6 +11,7 @@ unittest.TestLoader.sortTestMethodsUsing = None import protpy as protpy +from protpy.composition import normalize_property class ProtpyCompositionTests(unittest.TestCase): """ @@ -231,7 +232,7 @@ def test_dipeptide_composition(self): dipeptide_comp_seq1 = protpy.dipeptide_composition(self.protein_seq1) #testing first 10 columns dipeptide_comp_seq1_expected_values = pd.DataFrame(columns=["AA", "AC", "AD", "AE", "AF", "AG", "AH", "AI", "AK", "AL"]) - dipeptide_comp_seq1_expected_values.loc[0] = [0.72, 0.16, 0.48, 0.4, 0.24, 0.48, 0.0, 0.56, 0.16, 0.48] + dipeptide_comp_seq1_expected_values.loc[0] = [0.8, 0.16, 0.48, 0.4, 0.24, 0.48, 0.0, 0.56, 0.16, 0.48] self.assertIsInstance(dipeptide_comp_seq1, pd.DataFrame, 'Expected output to be of type DataFrame, got {}.'.format(type(dipeptide_comp_seq1))) @@ -460,6 +461,18 @@ def test_tripeptide_composition(self): with self.assertRaises(TypeError): tripeptide_composition_seq8 = protpy.tripeptide_composition(invalid_seq8) + def test_short_sequence_pseudo_amino_acid_composition(self): + """Short sequences should not crash when lamda exceeds the usable distance.""" + result = protpy.pseudo_amino_acid_composition("A") + self.assertIsInstance(result, pd.DataFrame) + self.assertEqual(result.shape, (1, 50)) + self.assertFalse(result.isnull().any().any()) + + def test_zero_variance_property_normalization(self): + """Properties with zero variance should normalize safely to zeros.""" + result = normalize_property({"A": 1.0, "C": 1.0, "D": 1.0}) + self.assertEqual(result, {"A": 0.0, "C": 0.0, "D": 0.0}) + # @unittest.skip("Descriptor can take quite a bit of time to calculate therefore skipping.") #** def test_pseudo_amino_acid_composition(self): """ Testing pseudo amino acid composition protein descriptor attributes and functionality. """ @@ -753,16 +766,16 @@ def test_isoelectric_point(self): 'Expected 5.412 for seq1, got {}.'.format(pi_seq1["IsoelectricPoint"].values[0])) #2.) pi_seq2 = protpy.isoelectric_point(self.protein_seq2) - self.assertEqual(pi_seq2["IsoelectricPoint"].values[0], 10.514, - 'Expected 10.514 for seq2, got {}.'.format(pi_seq2["IsoelectricPoint"].values[0])) + self.assertEqual(pi_seq2["IsoelectricPoint"].values[0], 10.515, + 'Expected 10.515 for seq2, got {}.'.format(pi_seq2["IsoelectricPoint"].values[0])) #3.) pi_seq3 = protpy.isoelectric_point(self.protein_seq3) self.assertEqual(pi_seq3["IsoelectricPoint"].values[0], 6.134, 'Expected 6.134 for seq3, got {}.'.format(pi_seq3["IsoelectricPoint"].values[0])) #4.) pi_seq4 = protpy.isoelectric_point(self.protein_seq4) - self.assertEqual(pi_seq4["IsoelectricPoint"].values[0], 10.472, - 'Expected 10.472 for seq4, got {}.'.format(pi_seq4["IsoelectricPoint"].values[0])) + self.assertEqual(pi_seq4["IsoelectricPoint"].values[0], 10.473, + 'Expected 10.473 for seq4, got {}.'.format(pi_seq4["IsoelectricPoint"].values[0])) #5.) with self.assertRaises(ValueError): protpy.isoelectric_point("ABCDEF") @@ -787,20 +800,20 @@ def test_molecular_weight(self): 'Incorrect columns: {}.'.format(list(mw_seq1.columns))) self.assertTrue(mw_seq1.any().isnull().sum() == 0, 'Expected no null values.') - self.assertEqual(mw_seq1["MolecularWeight"].values[0], 139122.355, - 'Expected 139122.355 for seq1, got {}.'.format(mw_seq1["MolecularWeight"].values[0])) + self.assertEqual(mw_seq1["MolecularWeight"].values[0], 139125.12, + 'Expected 139125.12 for seq1, got {}.'.format(mw_seq1["MolecularWeight"].values[0])) #2.) mw_seq2 = protpy.molecular_weight(self.protein_seq2) - self.assertEqual(mw_seq2["MolecularWeight"].values[0], 46023.455, - 'Expected 46023.455 for seq2, got {}.'.format(mw_seq2["MolecularWeight"].values[0])) + self.assertEqual(mw_seq2["MolecularWeight"].values[0], 46025.03, + 'Expected 46025.03 for seq2, got {}.'.format(mw_seq2["MolecularWeight"].values[0])) #3.) mw_seq3 = protpy.molecular_weight(self.protein_seq3) - self.assertEqual(mw_seq3["MolecularWeight"].values[0], 8360.93, - 'Expected 8360.93 for seq3, got {}.'.format(mw_seq3["MolecularWeight"].values[0])) + self.assertEqual(mw_seq3["MolecularWeight"].values[0], 8361.0, + 'Expected 8361.0 for seq3, got {}.'.format(mw_seq3["MolecularWeight"].values[0])) #4.) mw_seq4 = protpy.molecular_weight(self.protein_seq4) - self.assertEqual(mw_seq4["MolecularWeight"].values[0], 45624.194, - 'Expected 45624.194 for seq4, got {}.'.format(mw_seq4["MolecularWeight"].values[0])) + self.assertEqual(mw_seq4["MolecularWeight"].values[0], 45625.699, + 'Expected 45625.699 for seq4, got {}.'.format(mw_seq4["MolecularWeight"].values[0])) #5.) with self.assertRaises(ValueError): protpy.molecular_weight("ABCDEF") @@ -1164,13 +1177,13 @@ def test_boman_index(self): self.assertEqual(list(bi_seq1.columns), ["BomanIndex"], 'Incorrect columns: {}.'.format(list(bi_seq1.columns))) self.assertTrue(bi_seq1.any().isnull().sum() == 0, 'Expected no null values.') - self.assertEqual(bi_seq1["BomanIndex"].values[0], 0.119) + self.assertEqual(bi_seq1["BomanIndex"].values[0], 1.198) #2.) - self.assertEqual(protpy.boman_index(self.protein_seq2)["BomanIndex"].values[0], -0.498) + self.assertEqual(protpy.boman_index(self.protein_seq2)["BomanIndex"].values[0], 2.598) #3.) - self.assertEqual(protpy.boman_index(self.protein_seq3)["BomanIndex"].values[0], 1.237) + self.assertEqual(protpy.boman_index(self.protein_seq3)["BomanIndex"].values[0], -0.354) #4.) - self.assertEqual(protpy.boman_index(self.protein_seq4)["BomanIndex"].values[0], -0.455) + self.assertEqual(protpy.boman_index(self.protein_seq4)["BomanIndex"].values[0], 2.519) #5.) with self.assertRaises(ValueError): protpy.boman_index("ABCDEF") with self.assertRaises(ValueError): protpy.boman_index("OOOOO") diff --git a/tests/test_conjoint_triad.py b/tests/test_conjoint_triad.py index e9f126e..fb7969c 100644 --- a/tests/test_conjoint_triad.py +++ b/tests/test_conjoint_triad.py @@ -43,7 +43,7 @@ def test_conjoint_triad(self): conjoint_triad_seq1 = protpy.conjoint_triad(self.protein_seq1) #testing first 10 columns conjoint_triad_seq1_expected_values = pd.DataFrame(columns=["conj_triad_111", "conj_triad_112", "conj_triad_113", "conj_triad_114", "conj_triad_115", "conj_triad_116", "conj_triad_117", "conj_triad_121", "conj_triad_122", "conj_triad_123"]) - conjoint_triad_seq1_expected_values.loc[0] = [7, 17, 11, 3, 6, 6, 2, 16, 11, 19] + conjoint_triad_seq1_expected_values.loc[0] = [9, 17, 11, 3, 6, 6, 2, 16, 11, 19] self.assertIsInstance(conjoint_triad_seq1, pd.DataFrame, 'Expected output to be a DataFrame, got {}.'.format(type(conjoint_triad_seq1))) diff --git a/tests/test_regressions.py b/tests/test_regressions.py new file mode 100644 index 0000000..44ce071 --- /dev/null +++ b/tests/test_regressions.py @@ -0,0 +1,84 @@ +"""Regression tests for bugs fixed in the post-1.4.2 audit.""" +import os +import tempfile +import unittest + +import pandas as pd +from aaindex import aaindex1 + +import protpy +from protpy.ctd import ctd_properties + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" + + +class ProtpyRegressionTests(unittest.TestCase): + + def test_overlapping_counts(self): + self.assertEqual(protpy.dipeptide_composition("AAA")["AA"].iloc[0], 100.0) + self.assertEqual(protpy.tripeptide_composition("AAAA")["AAA"].iloc[0], 100.0) + self.assertEqual(protpy.conjoint_triad("AAAAA")["conj_triad_111"].iloc[0], 3) + + def test_ctd_groups_partition_all_amino_acids(self): + for name, prop in ctd_properties.items(): + groups = prop["1"] + prop["2"] + prop["3"] + self.assertEqual(sorted(groups), sorted(protpy.amino_acids), name) + + def test_moran_matches_reference_formula(self): + vals = {aa: v for aa, v in aaindex1["CIDH920105"].values.items() if aa in protpy.amino_acids} + mean = sum(vals.values()) / 20 + std = (sum((v - mean) ** 2 for v in vals.values()) / 20) ** 0.5 + p = [(vals[aa] - mean) / std for aa in SEQ] + n, pbar = len(p), sum(p) / len(p) + var = sum((x - pbar) ** 2 for x in p) / n + ref = sum((p[i] - pbar) * (p[i + 1] - pbar) for i in range(n - 1)) / (n - 1) / var + got = protpy.moran_autocorrelation(SEQ, lag=1, properties=["CIDH920105"]).iloc[0, 0] + self.assertAlmostEqual(got, ref, places=4) + + def test_short_and_repetitive_sequences_do_not_crash(self): + funcs = [protpy.dipeptide_composition, protpy.ctd_transition, protpy.moreaubroto_autocorrelation, + protpy.moran_autocorrelation, protpy.geary_autocorrelation, + protpy.pseudo_amino_acid_composition, protpy.amphiphilic_pseudo_amino_acid_composition, + protpy.quasi_sequence_order] + for seq in ["A", "AAAAAAAAAA", "ACDEFGHIKL"]: + for func in funcs: + df = func(seq) + self.assertFalse(df.isnull().any().any(), f"{func.__name__}({seq})") + + def test_invalid_params_raise(self): + with self.assertRaises(ValueError): + protpy.moran_autocorrelation(SEQ, lag=-1) + with self.assertRaises(ValueError): + protpy.quasi_sequence_order(SEQ, weight="x") + with self.assertRaises(ValueError): + protpy.kmer_composition(SEQ, k=0) + with self.assertRaises(ValueError): + protpy.aggregation_propensity(SEQ, window=0) + + def test_misc_descriptor_fixes(self): + self.assertEqual(protpy.motif_composition("DAAALLK")["dileucine_sorting"].iloc[0], 1) + self.assertGreater(protpy.isoelectric_point("RRRRRRRRRR").iloc[0, 0], 12.0) + self.assertEqual(protpy.molecular_weight("G").iloc[0, 0], 75.067) + self.assertEqual(protpy.hydrophobic_moment(SEQ).shape, (1, 2)) + + def test_ctd_all_ctd_param(self): + df = protpy.all_descriptors(SEQ, params={"ctd_": {"all_ctd": False}}, _show_descriptor_list=False) + self.assertEqual(df.filter(like="CTD_").shape[1], 21) + + def test_encode_dataset_file_safety(self): + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "out.csv") + pd.DataFrame({"x": [1]}).to_csv(path, index=False) + with self.assertRaises(ValueError): + protpy.encode_dataset([SEQ], output_file=path, verbose=False) + self.assertEqual(list(pd.read_csv(path).columns), ["x"]) # untouched + + path = os.path.join(tmp, "enc.csv") + protpy.encode_dataset([SEQ, SEQ], output_file=path, verbose=False) + protpy.encode_dataset([SEQ], output_file=path, verbose=False) # re-run seq1 + ids = pd.read_csv(path)["sequence_id"].tolist() + self.assertEqual(sorted(ids), ["seq1", "seq2"]) + + +if __name__ == "__main__": + unittest.main() From 35db993a33c4403029ab872a137f7f7646417b56 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:00:48 +0100 Subject: [PATCH 02/17] v1.5.0 phase 0: golden backwards-compatibility snapshot --- tests/test_backwards_compat.py | 82 ++++++++++++++++++++++++++ tests/test_data/golden_v1_4_2.json.gz | Bin 0 -> 259286 bytes 2 files changed, 82 insertions(+) create mode 100644 tests/test_backwards_compat.py create mode 100644 tests/test_data/golden_v1_4_2.json.gz diff --git a/tests/test_backwards_compat.py b/tests/test_backwards_compat.py new file mode 100644 index 0000000..8406c11 --- /dev/null +++ b/tests/test_backwards_compat.py @@ -0,0 +1,82 @@ +########################################################################### +############### protPy - Backwards Compatibility Tests ############### +########################################################################### + +import gzip +import json +import os +import sys +import unittest + +from Bio import SeqIO + +import protpy + +TEST_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_data") +GOLDEN = os.path.join(TEST_DATA, "golden_v1_4_2.json.gz") + +#public descriptors that existed in v1.4.2; their default outputs must never change +V1_4_2_DESCRIPTORS = [ + "amino_acid_composition", "dipeptide_composition", "tripeptide_composition", + "gravy", "aromaticity", "instability_index", "isoelectric_point", "molecular_weight", + "charge_distribution", "hydrophobic_polar_charged_composition", "secondary_structure_propensity", + "kmer_composition", "reduced_alphabet_composition", "motif_composition", + "amino_acid_pair_composition", "aliphatic_index", "extinction_coefficient", "boman_index", + "aggregation_propensity", "hydrophobic_moment", + "moreaubroto_autocorrelation", "moran_autocorrelation", "geary_autocorrelation", + "pseudo_amino_acid_composition", "amphiphilic_pseudo_amino_acid_composition", + "conjoint_triad", "ctd_composition", "ctd_transition", "ctd_distribution", "ctd_", + "sequence_order_coupling_number_", "sequence_order_coupling_number", + "sequence_order_coupling_number_all", "quasi_sequence_order", "quasi_sequence_order_all", + "shannon_entropy", +] + + +def _sequences() -> dict: + seqs = {} + for name in sorted(os.listdir(TEST_DATA)): + if name.endswith(".fasta"): + seqs[name] = str(next(SeqIO.parse(os.path.join(TEST_DATA, name), "fasta")).seq) + return seqs + + +def _record(df) -> dict: + #json round-trips python floats exactly, so exact comparison is safe + if not hasattr(df, "columns"): #scalar-returning descriptor + return {"type": type(df).__name__, "value": df} + values = [df.iloc[0, i] for i in range(df.shape[1])] + return {"columns": [str(c) for c in df.columns], "dtypes": [str(d) for d in df.dtypes], + "values": [v.item() if hasattr(v, "item") else v for v in values], "shape": list(df.shape)} + + +def compute_snapshot() -> dict: + """Compute every default output that the golden snapshot pins.""" + out = {} + for fname, seq in _sequences().items(): + out[f"{fname}::all_descriptors"] = _record(protpy.all_descriptors(seq, _show_descriptor_list=False)) + for desc in V1_4_2_DESCRIPTORS: + out[f"{fname}::{desc}"] = _record(getattr(protpy, desc)(seq)) + return out + + +class ProtpyBackwardsCompatTests(unittest.TestCase): + """Default descriptor outputs must be identical to the v1.4.2 (post-audit) baseline.""" + + def test_golden_snapshot(self): + with gzip.open(GOLDEN, "rt") as f: + golden = json.load(f) + current = json.loads(json.dumps(compute_snapshot())) + self.assertEqual(sorted(golden), sorted(current)) + for key, expected in golden.items(): + with self.subTest(key=key): + self.assertEqual(current[key], expected) + + +if __name__ == "__main__": + #regenerate only when deliberately re-baselining: python tests/test_backwards_compat.py --regen + if "--regen" in sys.argv: + with gzip.open(GOLDEN, "wt") as f: + json.dump(compute_snapshot(), f) + print(f"wrote {GOLDEN}") + else: + unittest.main() diff --git a/tests/test_data/golden_v1_4_2.json.gz b/tests/test_data/golden_v1_4_2.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..b8d222be6184785c01e1c059dba17d1097dcffbb GIT binary patch literal 259286 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 literal 0 HcmV?d00001 From 09f97ed68e2dc752185a7df39a5f755581351966 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:07:02 +0100 Subject: [PATCH 03/17] v1.5.0 phase 0: helpers, data tables, registry DOIs, references.bib --- docs/references.bib | 2299 ++++++++++++++++++++++++++++ planning/BLOCKED.md | 8 + protpy/_helpers.py | 153 ++ protpy/data/aa_scales.json | 198 +++ protpy/data/reduced_alphabets.json | 151 ++ protpy/data/residue_tables.json | 178 +++ protpy/utils.py | 79 +- tests/test_helpers.py | 110 ++ 8 files changed, 3175 insertions(+), 1 deletion(-) create mode 100644 docs/references.bib create mode 100644 planning/BLOCKED.md create mode 100644 protpy/_helpers.py create mode 100644 protpy/data/aa_scales.json create mode 100644 protpy/data/reduced_alphabets.json create mode 100644 protpy/data/residue_tables.json create mode 100644 tests/test_helpers.py diff --git a/docs/references.bib b/docs/references.bib new file mode 100644 index 0000000..35eb87e --- /dev/null +++ b/docs/references.bib @@ -0,0 +1,2299 @@ +% ===== composition_encoding ===== +% References for composition / encoding / PSSM / embedding descriptors (protpy gap list) +% VERIFIED = DOI confirmed via exact-DOI web search resolving to the paper (Crossref/PubMed APIs blocked in-session). +% Volume/issue/pages partly from standard citation records; spot-check against Crossref when reachable. +% UNVERIFIED entries are commented out at the end. + +@article{chen2007cksaap, + author = {Chen, Ke and Kurgan, Lukasz A. and Ruan, Jishou}, + title = {Prediction of flexible/rigid regions from protein sequences using k-spaced amino acid pairs}, + journal = {BMC Structural Biology}, + year = {2007}, + volume = {7}, + pages = {25}, + doi = {10.1186/1472-6807-7-25} +} + +@article{chen2007crystallization, + author = {Chen, Ke and Kurgan, Lukasz and Rahbari, Mandana}, + title = {Prediction of protein crystallization using collocation of amino acid pairs}, + journal = {Biochemical and Biophysical Research Communications}, + year = {2007}, + volume = {355}, + number = {3}, + pages = {764--769}, + doi = {10.1016/j.bbrc.2007.02.040} +} + +@article{chen2008oglyc, + author = {Chen, Yong-Zi and Tang, Yu-Rong and Sheng, Zhi-Ya and Zhang, Ziding}, + title = {Prediction of mucin-type {O}-glycosylation sites in mammalian proteins using the composition of k-spaced amino acid pairs}, + journal = {BMC Bioinformatics}, + year = {2008}, + volume = {9}, + pages = {101}, + doi = {10.1186/1471-2105-9-101} +} + +@article{chen2018ifeature, + author = {Chen, Zhen and Zhao, Pei and Li, Fuyi and Leier, Andr{\'e} and Marquez-Lago, Tatiana T. and Wang, Yanan and Webb, Geoffrey I. and Smith, A. Ian and Daly, Roger J. and Chou, Kuo-Chen and Song, Jiangning}, + title = {{iFeature}: a {Python} package and web server for features extraction and selection from protein and peptide sequences}, + journal = {Bioinformatics}, + year = {2018}, + volume = {34}, + number = {14}, + pages = {2499--2502}, + doi = {10.1093/bioinformatics/bty140} +} + +@article{saravanan2015dde, + author = {Saravanan, Vijayakumar and Gautham, Namasivayam}, + title = {Harnessing computational biology for exact linear {B}-cell epitope prediction: a novel amino acid composition-based feature descriptor}, + journal = {OMICS: A Journal of Integrative Biology}, + year = {2015}, + volume = {19}, + number = {10}, + pages = {648--658}, + doi = {10.1089/omi.2015.0095} +} + +@article{wei2018acpredfl, + author = {Wei, Leyi and Zhou, Chen and Chen, Huangrong and Song, Jiangning and Su, Ran}, + title = {{ACPred-FL}: a sequence-based predictor using effective feature representation to improve the prediction of anti-cancer peptides}, + journal = {Bioinformatics}, + year = {2018}, + volume = {34}, + number = {23}, + pages = {4007--4016}, + doi = {10.1093/bioinformatics/bty451} +} + +@article{shen2007conjointtriad, + author = {Shen, Juwen and Zhang, Jian and Luo, Xiaomin and Zhu, Weiliang and Yu, Kunqian and Chen, Kaixian and Li, Yixue and Jiang, Hualiang}, + title = {Predicting protein--protein interactions based only on sequences information}, + journal = {Proceedings of the National Academy of Sciences USA}, + year = {2007}, + volume = {104}, + number = {11}, + pages = {4337--4341}, + doi = {10.1073/pnas.0607879104} +} + +@article{liu2014idnaprotdis, + author = {Liu, Bin and Xu, Jinghao and Lan, Xun and Xu, Ruifeng and Zhou, Jiyun and Wang, Xiaolong and Chou, Kuo-Chen}, + title = {{iDNA-Prot|dis}: identifying {DNA}-binding proteins by incorporating amino acid distance-pairs and reduced alphabet profile into the general pseudo amino acid composition}, + journal = {PLoS ONE}, + year = {2014}, + volume = {9}, + number = {9}, + pages = {e106691}, + doi = {10.1371/journal.pone.0106691} +} + +@article{cannata2002alphabets, + author = {Cannata, Nicola and Toppo, Stefano and Romualdi, Chiara and Valle, Giorgio}, + title = {Simplifying amino acid alphabets by means of a branch and bound algorithm and substitution matrices}, + journal = {Bioinformatics}, + year = {2002}, + volume = {18}, + number = {8}, + pages = {1102--1108}, + doi = {10.1093/bioinformatics/18.8.1102} +} + +@article{zuo2017psekraac, + author = {Zuo, Yongchun and Li, Yuan and Chen, Yingli and Li, Guangpeng and Yan, Zhenhe and Yang, Lei}, + title = {{PseKRAAC}: a flexible web server for generating pseudo {K}-tuple reduced amino acids composition}, + journal = {Bioinformatics}, + year = {2017}, + volume = {33}, + number = {1}, + pages = {122--124}, + doi = {10.1093/bioinformatics/btw564} +} + +@article{kumar2006mitpred, + author = {Kumar, Manish and Verma, Ruchi and Raghava, Gajendra P. S.}, + title = {Prediction of mitochondrial proteins using support vector machine and hidden {M}arkov model}, + journal = {Journal of Biological Chemistry}, + year = {2006}, + volume = {281}, + number = {9}, + pages = {5357--5363}, + doi = {10.1074/jbc.M511061200} +} + +@article{lata2007antibp, + author = {Lata, Sneh and Sharma, B. K. and Raghava, Gajendra P. S.}, + title = {Analysis and prediction of antibacterial peptides}, + journal = {BMC Bioinformatics}, + year = {2007}, + volume = {8}, + pages = {263}, + doi = {10.1186/1471-2105-8-263} +} + +@article{lata2010antibp2, + author = {Lata, Sneh and Mishra, Nitish K. and Raghava, Gajendra P. S.}, + title = {{AntiBP2}: improved version of antibacterial peptide prediction}, + journal = {BMC Bioinformatics}, + year = {2010}, + volume = {11}, + number = {Suppl 1}, + pages = {S19}, + doi = {10.1186/1471-2105-11-S1-S19} +} + +@article{dubchak1995ctd, + author = {Dubchak, Inna and Muchnik, Ilya and Holbrook, Stephen R. and Kim, Sung-Hou}, + title = {Prediction of protein folding class using global description of amino acid sequence}, + journal = {Proceedings of the National Academy of Sciences USA}, + year = {1995}, + volume = {92}, + number = {19}, + pages = {8700--8704}, + doi = {10.1073/pnas.92.19.8700} +} + +@article{dubchak1999scop, + author = {Dubchak, Inna and Muchnik, Ilya and Mayor, Christopher and Dralyuk, Igor and Kim, Sung-Hou}, + title = {Recognition of a protein fold in the context of the {SCOP} classification}, + journal = {Proteins: Structure, Function, and Genetics}, + year = {1999}, + volume = {35}, + number = {4}, + pages = {401--407}, + doi = {10.1002/(SICI)1097-0134(19990601)35:4<401::AID-PROT3>3.0.CO;2-K} +} + +@article{pande2023pfeature, + author = {Pande, Akshara and Patiyal, Sumeet and Lathwal, Anjali and Arora, Chakit and Kaur, Dilraj and Dhall, Anjali and Mishra, Gaurav and Kaur, Harpreet and Sharma, Neelam and Jain, Shipra and Usmani, Salman Sadullah and Agrawal, Piyush and Kumar, Rajesh and Kumar, Vinod and Raghava, Gajendra P. S.}, + title = {Pfeature: a tool for computing wide range of protein features and building prediction models}, + journal = {Journal of Computational Biology}, + year = {2023}, + volume = {30}, + number = {2}, + pages = {204--222}, + doi = {10.1089/cmb.2022.0241} +} + +@article{vanheel1991cmv, + author = {van Heel, Marin}, + title = {A new family of powerful multivariate statistical sequence analysis techniques}, + journal = {Journal of Molecular Biology}, + year = {1991}, + volume = {220}, + number = {4}, + pages = {877--887}, + doi = {10.1016/0022-2836(91)90360-I} +} + +@article{shamim2007svmfold, + author = {Shamim, Mohammad Tabrez Anwar and Anwaruddin, Mohammad and Nagarajaram, H. A.}, + title = {Support Vector Machine-based classification of protein folds using the structural properties of amino acid residues and amino acid residue pairs}, + journal = {Bioinformatics}, + year = {2007}, + volume = {23}, + number = {24}, + pages = {3320--3327}, + doi = {10.1093/bioinformatics/btm527} +} + +@article{muhammod2019pyfeat, + author = {Muhammod, Rafsanjani and Ahmed, Sajid and Md Farid, Dewan and Shatabda, Swakkhar and Sharma, Alok and Dehzangi, Abdollah}, + title = {{PyFeat}: a {Python}-based effective feature generation tool for {DNA}, {RNA} and protein sequences}, + journal = {Bioinformatics}, + year = {2019}, + volume = {35}, + number = {19}, + pages = {3831--3833}, + doi = {10.1093/bioinformatics/btz165} +} + +@article{chou2001pseaac, + author = {Chou, Kuo-Chen}, + title = {Prediction of protein cellular attributes using pseudo-amino acid composition}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {2001}, + volume = {43}, + number = {3}, + pages = {246--255}, + doi = {10.1002/prot.1035} +} + +@article{chou2005amphiphilic, + author = {Chou, Kuo-Chen}, + title = {Using amphiphilic pseudo amino acid composition to predict enzyme subfamily classes}, + journal = {Bioinformatics}, + year = {2005}, + volume = {21}, + number = {1}, + pages = {10--19}, + doi = {10.1093/bioinformatics/bth466} +} + +@article{chou2011generalform, + author = {Chou, Kuo-Chen}, + title = {Some remarks on protein attribute prediction and pseudo amino acid composition}, + journal = {Journal of Theoretical Biology}, + year = {2011}, + volume = {273}, + number = {1}, + pages = {236--247}, + doi = {10.1016/j.jtbi.2010.12.024} +} + +@article{shen2008pseaacserver, + author = {Shen, Hong-Bin and Chou, Kuo-Chen}, + title = {{PseAAC}: a flexible web server for generating various kinds of protein pseudo amino acid composition}, + journal = {Analytical Biochemistry}, + year = {2008}, + volume = {373}, + number = {2}, + pages = {386--388}, + doi = {10.1016/j.ab.2007.10.012} +} + +@article{yu2013naturalvector, + author = {Yu, Chenglong and Deng, Mo and Cheng, Shiu-Yuen and Yau, Shek-Chung and He, Rong L. and Yau, Stephen S.-T.}, + title = {Protein space: a natural method for realizing the nature of protein universe}, + journal = {Journal of Theoretical Biology}, + year = {2013}, + volume = {318}, + pages = {197--204}, + doi = {10.1016/j.jtbi.2012.11.005} +} + +@article{cosic1994rrm, + author = {Cosic, Irena}, + title = {Macromolecular bioactivity: is it resonant interaction between macromolecules? -- Theory and applications}, + journal = {IEEE Transactions on Biomedical Engineering}, + year = {1994}, + volume = {41}, + number = {12}, + pages = {1101--1114}, + doi = {10.1109/10.335859} +} + +@article{wang2017pssmdct, + author = {Wang, Yubo and Ding, Yijie and Guo, Fei and Wei, Leyi and Tang, Jijun}, + title = {Improved detection of {DNA}-binding proteins via compression technology on {PSSM} information}, + journal = {PLoS ONE}, + year = {2017}, + volume = {12}, + number = {9}, + pages = {e0185587}, + doi = {10.1371/journal.pone.0185587} +} + +@article{wen2005dwt, + author = {Wen, Zhi-Ning and Wang, Ke-Long and Li, Meng-Long and Nie, Fu-Sheng and Yang, Yi}, + title = {Analyzing functional similarity of protein sequences with discrete wavelet transform}, + journal = {Computational Biology and Chemistry}, + year = {2005}, + volume = {29}, + number = {3}, + pages = {220--228}, + doi = {10.1016/j.compbiolchem.2005.04.007} +} + +@article{shi2007multiscale, + author = {Shi, Jian-Yu and Zhang, Shao-Wu and Pan, Quan and Cheng, Yong-Mei and Xie, Jun}, + title = {Prediction of protein subcellular localization by support vector machines using multi-scale energy and pseudo amino acid composition}, + journal = {Amino Acids}, + year = {2007}, + volume = {33}, + number = {1}, + pages = {69--74}, + doi = {10.1007/s00726-006-0475-y} +} + +@article{jeffrey1990cgr, + author = {Jeffrey, H. Joel}, + title = {Chaos game representation of gene structure}, + journal = {Nucleic Acids Research}, + year = {1990}, + volume = {18}, + number = {8}, + pages = {2163--2170}, + doi = {10.1093/nar/18.8.2163} +} + +@article{fiser1994cgr, + author = {Fiser, Andr{\'a}s and Tusn{\'a}dy, G{\'a}bor E. and Simon, Istv{\'a}n}, + title = {Chaos game representation of protein structures}, + journal = {Journal of Molecular Graphics}, + year = {1994}, + volume = {12}, + number = {4}, + pages = {302--304}, + doi = {10.1016/0263-7855(94)80109-6} +} + +@article{basu1997cgr, + author = {Basu, Sumanta and Pan, Archana and Dutta, Chitra and Das, Jyotirmoy}, + title = {Chaos game representation of proteins}, + journal = {Journal of Molecular Graphics and Modelling}, + year = {1997}, + volume = {15}, + number = {5}, + pages = {279--289}, + doi = {10.1016/S1093-3263(97)00106-X} +} + +@article{xiao2008grey, + author = {Xiao, Xuan and Lin, Wei-Zhong and Chou, Kuo-Chen}, + title = {Using grey dynamic modeling and pseudo amino acid composition to predict protein structural classes}, + journal = {Journal of Computational Chemistry}, + year = {2008}, + volume = {29}, + number = {12}, + pages = {2018--2024}, + doi = {10.1002/jcc.20955} +} + +@article{lin2011idnaprot, + author = {Lin, Wei-Zhong and Fang, Jian-An and Xiao, Xuan and Chou, Kuo-Chen}, + title = {{iDNA-Prot}: identification of {DNA} binding proteins using random forest with grey model}, + journal = {PLoS ONE}, + year = {2011}, + volume = {6}, + number = {9}, + pages = {e24756}, + doi = {10.1371/journal.pone.0024756} +} + +@article{lempel1976complexity, + author = {Lempel, Abraham and Ziv, Jacob}, + title = {On the complexity of finite sequences}, + journal = {IEEE Transactions on Information Theory}, + year = {1976}, + volume = {22}, + number = {1}, + pages = {75--81}, + doi = {10.1109/TIT.1976.1055501} +} + +@article{diao2008lzprotein, + author = {Diao, Yuanbo and Ma, Deyuan and Wen, Zhining and Yin, Jiajun and Xiang, Jie and Li, Menglong}, + title = {Using pseudo amino acid composition to predict transmembrane regions in protein: cellular automata and {Lempel-Ziv} complexity}, + journal = {Amino Acids}, + year = {2008}, + volume = {34}, + number = {1}, + pages = {111--117}, + doi = {10.1007/s00726-007-0550-z} +} + +@article{henikoff1992blosum, + author = {Henikoff, Steven and Henikoff, Jorja G.}, + title = {Amino acid substitution matrices from protein blocks}, + journal = {Proceedings of the National Academy of Sciences USA}, + year = {1992}, + volume = {89}, + number = {22}, + pages = {10915--10919}, + doi = {10.1073/pnas.89.22.10915} +} + +@article{lin2002aesnn3, + author = {Lin, Kuang and May, Alex C. W. and Taylor, William R.}, + title = {Amino acid encoding schemes from protein structure alignments: multi-dimensional vectors to describe residue types}, + journal = {Journal of Theoretical Biology}, + year = {2002}, + volume = {216}, + number = {3}, + pages = {361--365}, + doi = {10.1006/jtbi.2001.2512} +} + +@article{chen2022ifeatureomega, + author = {Chen, Zhen and Liu, Xuhan and Zhao, Pei and Li, Chen and Wang, Yanan and Li, Fuyi and Akutsu, Tatsuya and Bain, Chris and Gasser, Robin B. and Li, Junzhou and Yang, Zuoren and Gao, Xin and Kurgan, Lukasz and Song, Jiangning}, + title = {{iFeatureOmega}: an integrative platform for engineering, visualization and analysis of features from molecular sequences, structural and ligand data sets}, + journal = {Nucleic Acids Research}, + year = {2022}, + volume = {50}, + number = {W1}, + pages = {W434--W447}, + doi = {10.1093/nar/gkac351} +} + +@article{altschul1997psiblast, + author = {Altschul, Stephen F. and Madden, Thomas L. and Sch{\"a}ffer, Alejandro A. and Zhang, Jinghui and Zhang, Zheng and Miller, Webb and Lipman, David J.}, + title = {Gapped {BLAST} and {PSI-BLAST}: a new generation of protein database search programs}, + journal = {Nucleic Acids Research}, + year = {1997}, + volume = {25}, + number = {17}, + pages = {3389--3402}, + doi = {10.1093/nar/25.17.3389} +} + +@article{wang2017possum, + author = {Wang, Jiawei and Yang, Bingjiao and Revote, Jerico and Leier, Andr{\'e} and Marquez-Lago, Tatiana T. and Webb, Geoffrey and Song, Jiangning and Chou, Kuo-Chen and Lithgow, Trevor}, + title = {{POSSUM}: a bioinformatics toolkit for generating numerical sequence feature descriptors based on {PSSM} profiles}, + journal = {Bioinformatics}, + year = {2017}, + volume = {33}, + number = {17}, + pages = {2756--2758}, + doi = {10.1093/bioinformatics/btx302} +} + +@article{sharma2013bigrampssm, + author = {Sharma, Alok and Lyons, James and Dehzangi, Abdollah and Paliwal, Kuldip K.}, + title = {A feature extraction technique using bi-gram probabilities of position specific scoring matrix for protein fold recognition}, + journal = {Journal of Theoretical Biology}, + year = {2013}, + volume = {320}, + pages = {41--46}, + doi = {10.1016/j.jtbi.2012.12.008} +} + +@article{liu2010aacpssm, + author = {Liu, Taigang and Zheng, Xiaoqi and Wang, Jun}, + title = {Prediction of protein structural class for low-similarity sequences using support vector machine and {PSI-BLAST} profile}, + journal = {Biochimie}, + year = {2010}, + volume = {92}, + number = {10}, + pages = {1330--1334}, + doi = {10.1016/j.biochi.2010.06.013} +} + +@article{zou2013t4se, + author = {Zou, Lingyun and Nan, Chonghan and Hu, Fuquan}, + title = {Accurate prediction of bacterial type {IV} secreted effectors using amino acid composition and {PSSM} profiles}, + journal = {Bioinformatics}, + year = {2013}, + volume = {29}, + number = {24}, + pages = {3135--3142}, + doi = {10.1093/bioinformatics/btt554} +} + +@article{dong2009acc, + author = {Dong, Qiwen and Zhou, Shuigeng and Guan, Jihong}, + title = {A new taxonomy-based protein fold recognition approach based on autocross-covariance transformation}, + journal = {Bioinformatics}, + year = {2009}, + volume = {25}, + number = {20}, + pages = {2655--2662}, + doi = {10.1093/bioinformatics/btp500} +} + +@article{lin2023esm2, + author = {Lin, Zeming and Akin, Halil and Rao, Roshan and Hie, Brian and Zhu, Zhongkai and Lu, Wenting and Smetanin, Nikita and Verkuil, Robert and Kabeli, Ori and Shmueli, Yaniv and dos Santos Costa, Allan and Fazel-Zarandi, Maryam and Sercu, Tom and Candido, Salvatore and Rives, Alexander}, + title = {Evolutionary-scale prediction of atomic-level protein structure with a language model}, + journal = {Science}, + year = {2023}, + volume = {379}, + number = {6637}, + pages = {1123--1130}, + doi = {10.1126/science.ade2574} +} + +@article{elnaggar2022prottrans, + author = {Elnaggar, Ahmed and Heinzinger, Michael and Dallago, Christian and Rehawi, Ghalia and Wang, Yu and Jones, Llion and Gibbs, Tom and Feher, Tamas and Angerer, Christoph and Steinegger, Martin and Bhowmik, Debsindhu and Rost, Burkhard}, + title = {{ProtTrans}: toward understanding the language of life through self-supervised learning}, + journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence}, + year = {2022}, + volume = {44}, + number = {10}, + pages = {7112--7127}, + doi = {10.1109/TPAMI.2021.3095381} +} + +@article{chen2020ilearn, + author = {Chen, Zhen and Zhao, Pei and Li, Fuyi and Marquez-Lago, Tatiana T. and Leier, Andr{\'e} and Revote, Jerico and Zhu, Yan and Powell, David R. and Akutsu, Tatsuya and Webb, Geoffrey I. and Chou, Kuo-Chen and Smith, A. Ian and Daly, Roger J. and Li, Jian and Song, Jiangning}, + title = {{iLearn}: an integrated platform and meta-learner for feature engineering, machine-learning analysis and modeling of {DNA}, {RNA} and protein sequence data}, + journal = {Briefings in Bioinformatics}, + year = {2020}, + volume = {21}, + number = {3}, + pages = {1047--1057}, + doi = {10.1093/bib/bbz041} +} + +@article{chen2021ilearnplus, + author = {Chen, Zhen and Zhao, Pei and Li, Chen and Li, Fuyi and Xiang, Dongxu and Chen, Yong-Zi and Akutsu, Tatsuya and Daly, Roger J. and Webb, Geoffrey I. and Zhao, Quanzhi and Kurgan, Lukasz and Song, Jiangning}, + title = {{iLearnPlus}: a comprehensive and automated machine-learning platform for nucleic acid and protein sequence analysis, prediction and visualization}, + journal = {Nucleic Acids Research}, + year = {2021}, + volume = {49}, + number = {10}, + pages = {e60}, + doi = {10.1093/nar/gkab122} +} + +@article{cao2013propy, + author = {Cao, Dong-Sheng and Xu, Qing-Song and Liang, Yi-Zeng}, + title = {propy: a tool to generate various modes of {Chou's} {PseAAC}}, + journal = {Bioinformatics}, + year = {2013}, + volume = {29}, + number = {7}, + pages = {960--962}, + doi = {10.1093/bioinformatics/btt072} +} + +@article{dong2018pybiomed, + author = {Dong, Jie and Yao, Zhi-Jiang and Zhang, Lin and Luo, Feijun and Lin, Qinlu and Lu, Ai-Ping and Chen, Alex F. and Cao, Dong-Sheng}, + title = {{PyBioMed}: a python library for various molecular representations of chemicals, proteins and {DNAs} and their interactions}, + journal = {Journal of Cheminformatics}, + year = {2018}, + volume = {10}, + pages = {16}, + doi = {10.1186/s13321-018-0270-2} +} + +@article{xiao2015protr, + author = {Xiao, Nan and Cao, Dong-Sheng and Zhu, Min-Feng and Xu, Qing-Song}, + title = {protr/{ProtrWeb}: {R} package and web server for generating various numerical representation schemes of protein sequences}, + journal = {Bioinformatics}, + year = {2015}, + volume = {31}, + number = {11}, + pages = {1857--1859}, + doi = {10.1093/bioinformatics/btv042} +} + +@article{sequeira2022propythia, + author = {Sequeira, Ana Marta and Lousa, Diana and Rocha, Miguel}, + title = {{ProPythia}: a {Python} package for protein classification based on machine and deep learning}, + journal = {Neurocomputing}, + year = {2022}, + volume = {484}, + pages = {172--182}, + doi = {10.1016/j.neucom.2021.07.102} +} + +@article{amerifar2022ftrcool, + author = {Amerifar, Sare and Norouzi, Mahmood and Ghandi, Mahmoud}, + title = {A tool for feature extraction from biological sequences}, + journal = {Briefings in Bioinformatics}, + year = {2022}, + volume = {23}, + number = {3}, + pages = {bbac108}, + doi = {10.1093/bib/bbac108} +} + +% ---------------- UNVERIFIED (DOI not confirmed) ---------------- + +% @article{chou2007memtype2l, +% author = {Chou, Kuo-Chen and Shen, Hong-Bin}, +% title = {{MemType-2L}: a web server for predicting membrane proteins and their types by incorporating evolution information through {Pse-PSSM}}, +% journal = {Biochemical and Biophysical Research Communications}, +% year = {2007}, +% volume = {360}, +% number = {2}, +% pages = {339--345}, +% note = {UNVERIFIED DOI; recalled as 10.1016/j.bbrc.2007.06.027. ScienceDirect PII S0006291X07012521} +% } + +% @inproceedings{juan2009dppssm, +% author = {Juan, Eric Y. T. and Li, W. J. and Jhang, J. H. and Chiu, C. H.}, +% title = {Predicting protein subcellular localizations for gram-negative bacteria using {DP-PSSM} and support vector machines}, +% booktitle = {2009 International Conference on Complex, Intelligent and Software Intensive Systems}, +% publisher = {IEEE}, +% year = {2009}, +% note = {UNVERIFIED: DOI/venue/pages not confirmed; IEEE Xplore document 5066887} +% } + +% @article{wei2017opf, +% author = {Wei, Leyi and Xing, Pengwei and Zeng, Jiancang and Chen, JinXiu and Su, Ran and Guo, Fei}, +% title = {Improved prediction of protein--protein interactions using novel negative samples, features, and an ensemble classifier}, +% journal = {Artificial Intelligence in Medicine}, +% year = {2017}, +% volume = {83}, +% pages = {67--74}, +% note = {UNVERIFIED DOI; ScienceDirect PII S0933365716305693} +% } + +% prodec: no peer-reviewed paper identified; cite software repository. +% ===== scales_physchem ===== +% References: amino-acid scales / physicochemical / charge patterning / stability / complexity descriptors (protpy gap list) +% VERIFIED = exact DOI fetched from Crossref, OpenAlex, a publisher landing page or PubMed, and metadata matched. +% UNVERIFIED entries (no DOI, or DOI not confirmed) are commented out at the end. + +@article{hellberg1987zscales, + author = {Hellberg, Sven and Sjöström, Michael and Skagerberg, Bert and Wold, Svante}, + title = {{Peptide quantitative structure-activity relationships, a multivariate approach}}, + journal = {Journal of Medicinal Chemistry}, + year = {1987}, + volume = {30}, + number = {7}, + pages = {1126--1135}, + doi = {10.1021/jm00390a003}, + note = {Crossref record; original 3 z-scales} +} + +@article{sandberg1998zscales, + author = {Sandberg, Maria and Eriksson, Lennart and Jonsson, Jörgen and Sjöström, Michael and Wold, Svante}, + title = {{New chemical descriptors relevant for the design of biologically active peptides. A multivariate characterization of 87 amino acids}}, + journal = {Journal of Medicinal Chemistry}, + year = {1998}, + volume = {41}, + number = {14}, + pages = {2481--2491}, + doi = {10.1021/jm9700575}, + note = {Crossref record; extended 5 z-scales, 87 amino acids} +} + +@article{mei2005vhse, + author = {Mei, H. and Liao, Z. H. and Zhou, Y. and Li, S. Z.}, + title = {{A new set of amino acid descriptors and its application in peptide QSARs}}, + journal = {Biopolymers}, + year = {2005}, + volume = {80}, + number = {6}, + pages = {775--786}, + doi = {10.1002/bip.20296}, + note = {Crossref record} +} + +@article{kidera1985factors, + author = {Kidera, Akinori and Konishi, Yasuo and Oka, Masahito and Ooi, Tatsuo and Scheraga, Harold A.}, + title = {{Statistical analysis of the physical properties of the 20 naturally occurring amino acids}}, + journal = {Journal of Protein Chemistry}, + year = {1985}, + volume = {4}, + number = {1}, + pages = {23--55}, + doi = {10.1007/BF01025492}, + note = {Springer publisher landing page} +} + +@article{atchley2005factors, + author = {Atchley, William R. and Zhao, Jieping and Fernandes, Andrew D. and Drüke, Tanja}, + title = {{Solving the protein sequence metric problem}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {2005}, + volume = {102}, + number = {18}, + pages = {6395--6400}, + doi = {10.1073/pnas.0408677102}, + note = {Crossref record; peptides.py docs print a truncated DOI (…040867710), and the correct one is …0408677102} +} + +@article{tian2007tscales, + author = {Tian, Feifei and Zhou, Peng and Li, Zhiliang}, + title = {{T-scale as a novel vector of topological descriptors for amino acids and its application in QSARs of peptides}}, + journal = {Journal of Molecular Structure}, + year = {2007}, + volume = {830}, + number = {1--3}, + pages = {106--115}, + doi = {10.1016/j.molstruc.2006.07.004}, + note = {Crossref record} +} + +@article{yang2010stscales, + author = {Yang, L. and Shu, M. and Ma, K. and Mei, H. and Jiang, Y. and Li, Z.}, + title = {{ST-scale as a novel amino acid descriptor and its application in QSAM of peptides and analogues}}, + journal = {Amino Acids}, + year = {2010}, + volume = {38}, + number = {3}, + pages = {805--816}, + doi = {10.1007/s00726-009-0287-y}, + note = {Crossref record; online 2009, issue 2010} +} + +@article{vanwesten2013protfp1, + author = {van Westen, Gerard J. P. and Swier, Remco F. and Wegner, Jörg K. and IJzerman, Adriaan P. and van Vlijmen, Herman W. T. and Bender, Andreas}, + title = {{Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets}}, + journal = {Journal of Cheminformatics}, + year = {2013}, + volume = {5}, + pages = {41}, + doi = {10.1186/1758-2946-5-41}, + note = {Crossref record; introduces ProtFP (PCA of 3D-free AAindex properties)} +} + +@article{vanwesten2013protfp2, + author = {van Westen, Gerard J. P. and Swier, Remco F. and Cortes-Ciriano, Isidro and Wegner, Jörg K. and Overington, John P. and IJzerman, Adriaan P. and van Vlijmen, Herman W. T. and Bender, Andreas}, + title = {{Benchmarking of protein descriptor sets in proteochemometric modeling (part 2): modeling performance of 13 amino acid descriptor sets}}, + journal = {Journal of Cheminformatics}, + year = {2013}, + volume = {5}, + pages = {42}, + doi = {10.1186/1758-2946-5-42}, + note = {OpenAlex record; companion benchmark of all A-section scales} +} + +@article{liang2007fasgai, + author = {Liang, Guizhao and Li, Zhiliang}, + title = {{Factor analysis scale of generalized amino acid information as the source of a new set of descriptors for elucidating the structure and activity relationships of cationic antimicrobial peptides}}, + journal = {QSAR \& Combinatorial Science}, + year = {2007}, + volume = {26}, + number = {6}, + pages = {754--763}, + doi = {10.1002/qsar.200630145}, + note = {OpenAlex record; original FASGAI paper} +} + +@article{liang2008fasgai, + author = {Liang, Guizhao and Chen, G. and Niu, W. and Li, Zhiliang}, + title = {{Factor analysis scales of generalized amino acid information as applied in predicting interactions between the human amphiphysin-1 SH3 domains and their peptide ligands}}, + journal = {Chemical Biology \& Drug Design}, + year = {2008}, + volume = {71}, + number = {4}, + pages = {345--351}, + doi = {10.1111/j.1747-0285.2008.00641.x}, + note = {PubMed 18318694 record; this is the reference peptides.py cites} +} + +@article{georgiev2009blosum, + author = {Georgiev, Alexander G.}, + title = {{Interpretable numerical descriptors of amino acid space}}, + journal = {Journal of Computational Biology}, + year = {2009}, + volume = {16}, + number = {5}, + pages = {703--723}, + doi = {10.1089/cmb.2008.0173}, + note = {Crossref record} +} + +@article{zaliani1999mswhim, + author = {Zaliani, Andrea and Gancia, Emanuela}, + title = {{MS-WHIM scores for amino acids: a new 3D-description for peptide QSAR and QSPR studies}}, + journal = {Journal of Chemical Information and Computer Sciences}, + year = {1999}, + volume = {39}, + number = {3}, + pages = {525--533}, + doi = {10.1021/ci980211b}, + note = {Crossref record} +} + +@article{cruciani2004properties, + author = {Cruciani, Gabriele and Baroni, Massimo and Carosati, Emanuele and Clementi, Monica and Valigi, Roberta and Clementi, Sergio}, + title = {{Peptide studies by means of principal properties of amino acids derived from MIF descriptors}}, + journal = {Journal of Chemometrics}, + year = {2004}, + volume = {18}, + number = {3--4}, + pages = {146--155}, + doi = {10.1002/cem.856}, + note = {OpenAlex record} +} + +@article{sneath1966vectors, + author = {Sneath, Peter H. A.}, + title = {{Relations between chemical structure and biological activity in peptides}}, + journal = {Journal of Theoretical Biology}, + year = {1966}, + volume = {12}, + number = {2}, + pages = {157--195}, + doi = {10.1016/0022-5193(66)90112-3}, + note = {OpenAlex record; peptides.py docs misprint the year as 1996} +} + +@article{venkatarajan2001pcp, + author = {Venkatarajan, Mathura S. and Braun, Werner}, + title = {{New quantitative descriptors of amino acids based on multidimensional scaling of a large number of physical-chemical properties}}, + journal = {Journal of Molecular Modeling}, + year = {2001}, + volume = {7}, + number = {12}, + pages = {445--453}, + doi = {10.1007/s00894-001-0058-5}, + note = {Crossref record; Crossref lists the authors in reverse order, while the article byline is Venkatarajan \& Braun} +} + +@article{tong2017svger, + author = {Tong, J. and Li, L. and Bai, M. and Li, K.}, + title = {{A new descriptor of amino acids-SVGER and its applications in peptide QSAR}}, + journal = {Molecular Informatics}, + year = {2017}, + volume = {36}, + number = {5--6}, + pages = {1501023}, + doi = {10.1002/minf.201501023}, + note = {Crossref record; online 2016, issue 2017. only initials confirmed} +} + +@article{shu2013vstpv, + author = {Shu, Mao and Yu, Rui and Zhang, Yunru and Wang, Juan and Yang, Li and Wang, Li and Lin, Zhihua}, + title = {{Predicting the activity of antimicrobial peptides with amino acid topological information}}, + journal = {Medicinal Chemistry}, + year = {2013}, + volume = {9}, + number = {1}, + pages = {32--44}, + doi = {10.2174/157340613804488350}, + note = {OpenAlex record; online 2012, issue 2013. peptides.py lists 6 authors, and OpenAlex has 7 (adds Yang L)} +} + +@article{vicatos2005prin, + author = {Vicatos, Spyridon and Reddy, Boojala V. B. and Kaznessis, Yiannis}, + title = {{Prediction of distant residue contacts with the use of evolutionary information}}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {2005}, + volume = {58}, + number = {4}, + pages = {935--949}, + doi = {10.1002/prot.20370}, + note = {OpenAlex record; this is the source peptides.py cites for PRIN (3 principal components of amino-acid properties)} +} + +@article{wold1993acc, + author = {Wold, Svante and Jonsson, Jörgen and Sjöström, Michael and Sandberg, Maria and Rännar, Stefan}, + title = {{DNA and peptide sequences and chemical processes multivariately modelled by principal component analysis and partial least-squares projections to latent structures}}, + journal = {Analytica Chimica Acta}, + year = {1993}, + volume = {277}, + number = {2}, + pages = {239--253}, + doi = {10.1016/0003-2670(93)80437-P}, + note = {OpenAlex record; introduces ACC transform of z-scale sequences} +} + +@article{lapinsh2002acc, + author = {Lapinsh, Maris and Prusis, Peteris and Lundstedt, Torbjörn and Wikberg, Jarl E. S.}, + title = {{Proteochemometrics modeling of the interaction of amine G-protein coupled receptors with a diverse set of ligands}}, + journal = {Molecular Pharmacology}, + year = {2002}, + volume = {61}, + number = {6}, + pages = {1465--1475}, + doi = {10.1124/mol.61.6.1465}, + note = {OpenAlex record; ACC of z-scales in proteochemometrics, which is the usual implementation reference. Earlier PCM paper: Lapinsh et al. 2001 BBA 1525:180–190, https://doi.org/10.1016/S0304-4165(00)00187-2, also VERIFIED via OpenAlex} +} + +@article{feng2000nmba, + author = {Feng, Zhi-Ping and Zhang, Chun-Ting}, + title = {{Prediction of membrane protein types based on the hydrophobic index of amino acids}}, + journal = {Journal of Protein Chemistry}, + year = {2000}, + volume = {19}, + number = {4}, + pages = {269--275}, + doi = {10.1023/A:1007091128394}, + note = {OpenAlex record; the normalised form used in PROFEAT/protr} +} + +@article{horne1988autocorr, + author = {Horne, David S.}, + title = {{Prediction of protein helix content from an autocorrelation analysis of sequence hydrophobicities}}, + journal = {Biopolymers}, + year = {1988}, + volume = {27}, + number = {3}, + pages = {451--477}, + doi = {10.1002/bip.360270308}, + note = {OpenAlex record; early protein-sequence autocorrelation} +} + +@article{liu2012pdt, + author = {Liu, Bin and Wang, Xiaolong and Chen, Qingcai and Dong, Qiwen and Lan, Xun}, + title = {{Using amino acid physicochemical distance transformation for fast protein remote homology detection}}, + journal = {PLoS ONE}, + year = {2012}, + volume = {7}, + number = {9}, + pages = {e46633}, + doi = {10.1371/journal.pone.0046633}, + note = {OpenAlex record} +} + +@article{das2013kappa, + author = {Das, Rahul K. and Pappu, Rohit V.}, + title = {{Conformations of intrinsically disordered proteins are influenced by linear sequence distributions of oppositely charged residues}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {2013}, + volume = {110}, + number = {33}, + pages = {13392--13397}, + doi = {10.1073/pnas.1304749110}, + note = {Crossref record; introduces κ and the FCR–NCPR diagram-of-states} +} + +@article{mao2010ncpr, + author = {Mao, Albert H. and Crick, Scott L. and Vitalis, Andreas and Chicoine, Caitlin L. and Pappu, Rohit V.}, + title = {{Net charge per residue modulates conformational ensembles of intrinsically disordered proteins}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {2010}, + volume = {107}, + number = {18}, + pages = {8183--8188}, + doi = {10.1073/pnas.0911107107}, + note = {OpenAlex record; NCPR/FCR as order parameters} +} + +@article{martin2016omega, + author = {Martin, Erik W. and Holehouse, Alex S. and Grace, Christy R. and Hughes, Alex and Pappu, Rohit V. and Mittag, Tanja}, + title = {{Sequence determinants of the conformational properties of an intrinsically disordered protein prior to and upon multisite phosphorylation}}, + journal = {Journal of the American Chemical Society}, + year = {2016}, + volume = {138}, + number = {47}, + pages = {15323--15335}, + doi = {10.1021/jacs.6b10272}, + note = {OpenAlex record} +} + +@article{sawle2015scd, + author = {Sawle, Lucas and Ghosh, Kingshuk}, + title = {{A theoretical method to compute sequence dependent configurational properties in charged polymers and proteins}}, + journal = {Journal of Chemical Physics}, + year = {2015}, + volume = {143}, + number = {8}, + pages = {085101}, + doi = {10.1063/1.4929391}, + note = {OpenAlex record} +} + +@article{zheng2020shd, + author = {Zheng, Wenwei and Dignon, Gregory L. and Brown, Matthew and Kim, Young C. and Mittal, Jeetain}, + title = {{Hydropathy patterning complements charge patterning to describe conformational preferences of disordered proteins}}, + journal = {Journal of Physical Chemistry Letters}, + year = {2020}, + volume = {11}, + number = {9}, + pages = {3408--3415}, + doi = {10.1021/acs.jpclett.0c00288}, + note = {OpenAlex record} +} + +@article{holehouse2017cider, + author = {Holehouse, Alex S. and Das, Rahul K. and Ahad, James N. and Richardson, Mary O. G. and Pappu, Rohit V.}, + title = {{CIDER: resources to analyze sequence-ensemble relationships of intrinsically disordered proteins}}, + journal = {Biophysical Journal}, + year = {2017}, + volume = {112}, + number = {1}, + pages = {16--21}, + doi = {10.1016/j.bpj.2016.11.3200}, + note = {OpenAlex record} +} + +@article{rice2000emboss, + author = {Rice, Peter and Longden, Ian and Bleasby, Alan}, + title = {{EMBOSS: the European Molecular Biology Open Software Suite}}, + journal = {Trends in Genetics}, + year = {2000}, + volume = {16}, + number = {6}, + pages = {276--277}, + doi = {10.1016/S0168-9525(00)02024-2}, + note = {OpenAlex record; source of the EMBOSS iep pKa table} +} + +@article{bjellqvist1993pi, + author = {Bjellqvist, Bengt and Hughes, Graham J. and Pasquali, Christian and Paquet, Nicole and Ravier, Florence and Sanchez, Jean-Charles and Frutiger, Séverine and Hochstrasser, Denis}, + title = {{The focusing positions of polypeptides in immobilized pH gradients can be predicted from their amino acid sequences}}, + journal = {Electrophoresis}, + year = {1993}, + volume = {14}, + number = {10}, + pages = {1023--1031}, + doi = {10.1002/elps.11501401163}, + note = {OpenAlex record; OpenAlex lists issue 1, while peptides.py and the print issue give 10} +} + +@article{bjellqvist1994pi, + author = {Bjellqvist, Bengt and Basse, Bodil and Olsen, Eydfinnur and Celis, Julio E.}, + title = {{Reference points for comparisons of two-dimensional maps of proteins from different human cell types defined in a pH scale where isoelectric points correlate with polypeptide compositions}}, + journal = {Electrophoresis}, + year = {1994}, + volume = {15}, + pages = {529--539}, + doi = {10.1002/elps.1150150171}, + note = {Crossref record; second Bjellqvist pK set used by ProtParam. Crossref's issue field (1) looks unreliable, so it is left out} +} + +@article{kozlowski2016ipc, + author = {Kozlowski, Lukasz P.}, + title = {{IPC – Isoelectric Point Calculator}}, + journal = {Biology Direct}, + year = {2016}, + volume = {11}, + pages = {55}, + doi = {10.1186/s13062-016-0159-9}, + note = {OpenAlex record} +} + +@article{muller2017modlamp, + author = {Müller, Alex T. and Gabernet, Gisela and Hiss, Jan A. and Schneider, Gisbert}, + title = {{modlAMP: Python for antimicrobial peptides}}, + journal = {Bioinformatics}, + year = {2017}, + volume = {33}, + number = {17}, + pages = {2753--2755}, + doi = {10.1093/bioinformatics/btx285}, + note = {OpenAlex record; charge density = net charge / MW} +} + +@article{kyte1982hydropathy, + author = {Kyte, Jack and Doolittle, Russell F.}, + title = {{A simple method for displaying the hydropathic character of a protein}}, + journal = {Journal of Molecular Biology}, + year = {1982}, + volume = {157}, + number = {1}, + pages = {105--132}, + doi = {10.1016/0022-2836(82)90515-0}, + note = {OpenAlex record} +} + +@article{hopp1981hydrophilicity, + author = {Hopp, Thomas P. and Woods, Kenneth R.}, + title = {{Prediction of protein antigenic determinants from amino acid sequences}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {1981}, + volume = {78}, + number = {6}, + pages = {3824--3828}, + doi = {10.1073/pnas.78.6.3824}, + note = {OpenAlex record} +} + +@article{eisenberg1982moment, + author = {Eisenberg, David and Weiss, Robert M. and Terwilliger, Thomas C.}, + title = {{The helical hydrophobic moment: a measure of the amphiphilicity of a helix}}, + journal = {Nature}, + year = {1982}, + volume = {299}, + number = {5881}, + pages = {371--374}, + doi = {10.1038/299371a0}, + note = {OpenAlex record; original hydrophobic moment} +} + +@article{eisenberg1984momentplot, + author = {Eisenberg, David and Schwarz, Erich M. and Komaromy, Michael C. and Wall, Randolph}, + title = {{Analysis of membrane and surface protein sequences with the hydrophobic moment plot}}, + journal = {Journal of Molecular Biology}, + year = {1984}, + volume = {179}, + number = {1}, + pages = {125--142}, + doi = {10.1016/0022-2836(84)90309-7}, + note = {OpenAlex record; normalised consensus scale + moment-plot membrane/surface/globular classification} +} + +@article{eisenberg1984periodicity, + author = {Eisenberg, David and Weiss, Robert M. and Terwilliger, Thomas C.}, + title = {{The hydrophobic moment detects periodicity in protein hydrophobicity}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {1984}, + volume = {81}, + number = {1}, + pages = {140--144}, + doi = {10.1073/pnas.81.1.140}, + note = {OpenAlex record; moment as implemented in peptides.py/modlAMP} +} + +@article{eisenberg1984annrev, + author = {Eisenberg, David}, + title = {{Three-dimensional structure of membrane and surface proteins}}, + journal = {Annual Review of Biochemistry}, + year = {1984}, + volume = {53}, + pages = {595--623}, + doi = {10.1146/annurev.bi.53.070184.003115}, + note = {OpenAlex record; consensus scale review} +} + +@article{wimley1996interface, + author = {Wimley, William C. and White, Stephen H.}, + title = {{Experimentally determined hydrophobicity scale for proteins at membrane interfaces}}, + journal = {Nature Structural Biology}, + year = {1996}, + volume = {3}, + number = {10}, + pages = {842--848}, + doi = {10.1038/nsb1096-842}, + note = {OpenAlex record} +} + +@article{hessa2005translocon, + author = {Hessa, Tara and Kim, Hyun and Bihlmaier, Karl and Lundin, Carolina and Boekel, Jorrit and Andersson, Helena and Nilsson, IngMarie and White, Stephen H. and von Heijne, Gunnar}, + title = {{Recognition of transmembrane helices by the endoplasmic reticulum translocon}}, + journal = {Nature}, + year = {2005}, + volume = {433}, + number = {7024}, + pages = {377--381}, + doi = {10.1038/nature03216}, + note = {OpenAlex record} +} + +@article{hessa2007code, + author = {Hessa, Tara and Meindl-Beinker, Nadja M. and Bernsel, Andreas and Kim, Hyun and Sato, Yoko and Lerch-Bader, Mirjam and Nilsson, IngMarie and White, Stephen H. and von Heijne, Gunnar}, + title = {{Molecular code for transmembrane-helix recognition by the Sec61 translocon}}, + journal = {Nature}, + year = {2007}, + volume = {450}, + number = {7172}, + pages = {1026--1030}, + doi = {10.1038/nature06387}, + note = {OpenAlex record; position-dependent ΔG_app} +} + +@article{moon2011scale, + author = {Moon, C. Preston and Fleming, Karen G.}, + title = {{Side-chain hydrophobicity scale derived from transmembrane protein folding into lipid bilayers}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {2011}, + volume = {108}, + number = {25}, + pages = {10174--10177}, + doi = {10.1073/pnas.1103979108}, + note = {Crossref record} +} + +@article{engelman1986ges, + author = {Engelman, Donald M. and Steitz, Thomas A. and Goldman, Adrian}, + title = {{Identifying nonpolar transbilayer helices in amino acid sequences of membrane proteins}}, + journal = {Annual Review of Biophysics and Biophysical Chemistry}, + year = {1986}, + volume = {15}, + pages = {321--353}, + doi = {10.1146/annurev.bb.15.060186.001541}, + note = {Annual Reviews publisher landing page} +} + +@article{juretic2009aasi, + author = {Juretić, Davor and Vukičević, Damir and Ilić, Nada and Antcheva, Nikolinka and Tossi, Alessandro}, + title = {{Computational design of highly selective antimicrobial peptides}}, + journal = {Journal of Chemical Information and Modeling}, + year = {2009}, + volume = {49}, + number = {12}, + pages = {2873--2882}, + doi = {10.1021/ci900327a}, + note = {OpenAlex record. The DOI is confirmed, but the attribution of the AASI scale to this paper follows modlAMP's documentation and was not checked against the article text} +} + +@article{suyama2003domcut, + author = {Suyama, Mikita and Ohara, Osamu}, + title = {{DomCut: prediction of inter-domain linker regions in amino acid sequences}}, + journal = {Bioinformatics}, + year = {2003}, + volume = {19}, + number = {5}, + pages = {673--674}, + doi = {10.1093/bioinformatics/btg031}, + note = {OpenAlex record} +} + +@article{wang2016apd3, + author = {Wang, Guangshun and Li, Xia and Wang, Zhe}, + title = {{APD3: the antimicrobial peptide database as a tool for research and education}}, + journal = {Nucleic Acids Research}, + year = {2016}, + volume = {44}, + number = {D1}, + pages = {D1087--D1093}, + doi = {10.1093/nar/gkv1278}, + note = {OpenAlex record; APD definition of hydrophobic ratio, implemented in modlAMP (see D5). Online 2015} +} + +@article{bachmair1986nend, + author = {Bachmair, Andreas and Finley, Daniel and Varshavsky, Alexander}, + title = {{In vivo half-life of a protein is a function of its amino-terminal residue}}, + journal = {Science}, + year = {1986}, + volume = {234}, + number = {4773}, + pages = {179--186}, + doi = {10.1126/science.3018930}, + note = {Crossref record} +} + +@article{gonda1989nend, + author = {Gonda, David K. and Bachmair, Andreas and Wünning, Ingrid and Tobias, John W. and Lane, William S. and Varshavsky, Alexander}, + title = {{Universality and structure of the N-end rule}}, + journal = {Journal of Biological Chemistry}, + year = {1989}, + volume = {264}, + number = {28}, + pages = {16700--16712}, + doi = {10.1016/S0021-9258(19)84762-2}, + note = {Crossref record; mammalian (reticulocyte) values} +} + +@article{tobias1991nend, + author = {Tobias, John W. and Shrader, Thomas E. and Rocap, Gabrielle and Varshavsky, Alexander}, + title = {{The N-end rule in bacteria}}, + journal = {Science}, + year = {1991}, + volume = {254}, + number = {5036}, + pages = {1374--1377}, + doi = {10.1126/science.1962196}, + note = {Crossref record; E. coli values} +} + +@article{varshavsky1996nend, + author = {Varshavsky, Alexander}, + title = {{The N-end rule: functions, mysteries, uses}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {1996}, + volume = {93}, + number = {22}, + pages = {12142--12149}, + doi = {10.1073/pnas.93.22.12142}, + note = {OpenAlex record; review} +} + +@article{pace1995extinction, + author = {Pace, C. Nick and Vajdos, Felix and Fee, Lanette and Grimsley, Gerald and Gray, Theronica}, + title = {{How to measure and predict the molar absorption coefficient of a protein}}, + journal = {Protein Science}, + year = {1995}, + volume = {4}, + number = {11}, + pages = {2411--2423}, + doi = {10.1002/pro.5560041120}, + note = {Crossref record; ProtParam coefficients} +} + +@article{gill1989extinction, + author = {Gill, Stanley C. and von Hippel, Peter H.}, + title = {{Calculation of protein extinction coefficients from amino acid sequence data}}, + journal = {Analytical Biochemistry}, + year = {1989}, + volume = {182}, + number = {2}, + pages = {319--326}, + doi = {10.1016/0003-2697(89)90602-7}, + note = {OpenAlex record} +} + +@incollection{gasteiger2005expasy, + author = {Gasteiger, Elisabeth and Hoogland, Christine and Gattiker, Alexandre and Duvaud, Séverine and Wilkins, Marc R. and Appel, Ron D. and Bairoch, Amos}, + title = {Protein identification and analysis tools on the ExPASy server}, + editor = {Walker, John M.}, + booktitle = {The Proteomics Protocols Handbook}, + publisher = {Humana Press}, + year = {2005}, + pages = {571--607}, + doi = {10.1385/1-59259-890-0:571}, + note = {Springer publisher landing page; covers both ProtParam and ProtScale} +} + +@article{vihinen1994flex, + author = {Vihinen, Mauno and Torkkila, Esa and Riikonen, Pentti}, + title = {{Accuracy of protein flexibility predictions}}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {1994}, + volume = {19}, + number = {2}, + pages = {141--149}, + doi = {10.1002/prot.340190207}, + note = {Crossref record; Biopython flexibility() scale} +} + +@article{karplus1985flex, + author = {Karplus, P. Andrew and Schulz, Georg E.}, + title = {{Prediction of chain flexibility in proteins}}, + journal = {Naturwissenschaften}, + year = {1985}, + volume = {72}, + number = {4}, + pages = {212--213}, + doi = {10.1007/BF01195768}, + note = {Springer publisher landing page} +} + +@article{zeldovich2007ivywrel, + author = {Zeldovich, Konstantin B. and Berezovsky, Igor N. and Shakhnovich, Eugene I.}, + title = {{Protein and DNA sequence determinants of thermophilic adaptation}}, + journal = {PLoS Computational Biology}, + year = {2007}, + volume = {3}, + number = {1}, + pages = {e5}, + doi = {10.1371/journal.pcbi.0030005}, + note = {PLOS publisher landing page} +} + +@article{suhre2003cvp, + author = {Suhre, Karsten and Claverie, Jean-Michel}, + title = {{Genomic correlates of hyperthermostability, an update}}, + journal = {Journal of Biological Chemistry}, + year = {2003}, + volume = {278}, + number = {19}, + pages = {17198--17202}, + doi = {10.1074/jbc.M301327200}, + note = {OpenAlex record; CvP bias = (E+K) − (Q+H)} +} + +@article{akashi2002cost, + author = {Akashi, Hiroshi and Gojobori, Takashi}, + title = {{Metabolic efficiency and amino acid composition in the proteomes of Escherichia coli and Bacillus subtilis}}, + journal = {Proceedings of the National Academy of Sciences of the USA}, + year = {2002}, + volume = {99}, + number = {6}, + pages = {3695--3700}, + doi = {10.1073/pnas.062526999}, + note = {Crossref record} +} + +@article{craig1998cost, + author = {Craig, Catherine L. and Weber, Robert S.}, + title = {{Selection costs of amino acid substitutions in ColE1 and ColIa gene clusters harbored by Escherichia coli}}, + journal = {Molecular Biology and Evolution}, + year = {1998}, + volume = {15}, + number = {6}, + pages = {774--776}, + doi = {10.1093/oxfordjournals.molbev.a025981}, + note = {OpenAlex record} +} + +@article{wagner2005cost, + author = {Wagner, Andreas}, + title = {{Energy constraints on the evolution of gene expression}}, + journal = {Molecular Biology and Evolution}, + year = {2005}, + volume = {22}, + number = {6}, + pages = {1365--1374}, + doi = {10.1093/molbev/msi126}, + note = {Crossref record} +} + +@article{heizer2006cost, + author = {Heizer, Esley M. and Raiford, Douglas W. and Raymer, Michael L. and Doom, Travis E. and Miller, Robert V. and Krane, Dan E.}, + title = {{Amino acid cost and codon-usage biases in 6 prokaryotic genomes: a whole-genome analysis}}, + journal = {Molecular Biology and Evolution}, + year = {2006}, + volume = {23}, + number = {9}, + pages = {1670--1680}, + doi = {10.1093/molbev/msl029}, + note = {OpenAlex record} +} + +@article{wilkinson1991solubility, + author = {Wilkinson, David L. and Harrison, Roger G.}, + title = {{Predicting the solubility of recombinant proteins in Escherichia coli}}, + journal = {Bio/Technology}, + year = {1991}, + volume = {9}, + number = {5}, + pages = {443--448}, + doi = {10.1038/nbt0591-443}, + note = {Nature publisher landing page} +} + +@article{davis1999solubility, + author = {Davis, Gregory D. and Elisee, Claude and Newham, Denton M. and Harrison, Roger G.}, + title = {{New fusion protein systems designed to give soluble expression in Escherichia coli}}, + journal = {Biotechnology and Bioengineering}, + year = {1999}, + volume = {65}, + number = {4}, + pages = {382--388}, + doi = {10.1002/(SICI)1097-0290(19991120)65:4<382::AID-BIT2>3.0.CO;2-I}, + note = {OpenAlex record; revised two-parameter model} +} + +@article{nakashima1986class, + author = {Nakashima, Hiroshi and Nishikawa, Ken and Ooi, Tatsuo}, + title = {{The folding type of a protein is relevant to the amino acid composition}}, + journal = {Journal of Biochemistry}, + year = {1986}, + volume = {99}, + number = {1}, + pages = {153--162}, + doi = {10.1093/oxfordjournals.jbchem.a135454}, + note = {OpenAlex record} +} + +@article{chou1995class, + author = {Chou, Kuo-Chen}, + title = {{A novel approach to predicting protein structural classes in a (20–1)-D amino acid composition space}}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {1995}, + volume = {21}, + number = {4}, + pages = {319--344}, + doi = {10.1002/prot.340210406}, + note = {OpenAlex record} +} + +@article{wootton1993seg, + author = {Wootton, John C. and Federhen, Scott}, + title = {{Statistics of local complexity in amino acid sequences and sequence databases}}, + journal = {Computers \& Chemistry}, + year = {1993}, + volume = {17}, + number = {2}, + pages = {149--163}, + doi = {10.1016/0097-8485(93)85006-X}, + note = {Crossref record} +} + +@article{wootton1996seg, + author = {Wootton, John C. and Federhen, Scott}, + title = {{Analysis of compositionally biased regions in sequence databases}}, + journal = {Methods in Enzymology}, + year = {1996}, + volume = {266}, + pages = {554--571}, + doi = {10.1016/S0076-6879(96)66035-2}, + note = {OpenAlex record} +} + +@article{troyanskaya2002lc, + author = {Troyanskaya, Olga G. and Arbell, Ora and Koren, Yair and Landau, Gad M. and Bolshoy, Alexander}, + title = {{Sequence complexity profiles of prokaryotic genomic sequences: a fast algorithm for calculating linguistic complexity}}, + journal = {Bioinformatics}, + year = {2002}, + volume = {18}, + number = {5}, + pages = {679--688}, + doi = {10.1093/bioinformatics/18.5.679}, + note = {OUP publisher landing page} +} + +@article{tsallis1988entropy, + author = {Tsallis, Constantino}, + title = {{Possible generalization of Boltzmann-Gibbs statistics}}, + journal = {Journal of Statistical Physics}, + year = {1988}, + volume = {52}, + number = {1--2}, + pages = {479--487}, + doi = {10.1007/BF01016429}, + note = {Springer publisher landing page} +} + +@article{osorio2015peptides, + author = {Osorio, Daniel and Rondón-Villarreal, Paola and Torres, Rodrigo}, + title = {{Peptides: a package for data mining of antimicrobial peptides}}, + journal = {The R Journal}, + year = {2015}, + volume = {7}, + number = {1}, + pages = {4--14}, + doi = {10.32614/RJ-2015-001}, + note = {Crossref record; the R ancestor of peptides.py} +} + +@article{cock2009biopython, + author = {Cock, Peter J. A. and Antao, Tiago and Chang, Jeffrey T. and Chapman, Brad A. and Cox, Cymon J. and Dalke, Andrew and Friedberg, Iddo and Hamelryck, Thomas and Kauff, Frank and Wilczynski, Bartek and de Hoon, Michiel J. L.}, + title = {{Biopython: freely available Python tools for computational molecular biology and bioinformatics}}, + journal = {Bioinformatics}, + year = {2009}, + volume = {25}, + number = {11}, + pages = {1422--1423}, + doi = {10.1093/bioinformatics/btp163}, + note = {Crossref record} +} + +% ---------------- UNVERIFIED (no DOI or DOI not confirmed) ---------------- +% @article{sjostrom1995acc, +% author = {Sjöström, Michael and Rännar, Stefan and Wieslander, Åke}, +% title = {{Polypeptide sequence property relationships in Escherichia coli based on auto cross covariances}}, +% journal = {Chemometrics and Intelligent Laboratory Systems}, +% year = {1995}, +% volume = {29}, +% number = {2}, +% pages = {295--305}, +% note = {no DOI could be confirmed. The guessed DOI returned 404 and Crossref lookup was rate-limited. Volume and pages are from memory} +% } + +% @article{broto1984autocorr, +% author = {Broto, P. and Moreau, G. and Vandycke, C.}, +% title = {{Molecular structures: perception, autocorrelation descriptor and SAR studies}}, +% journal = {European Journal of Medicinal Chemistry}, +% year = {1984}, +% volume = {19}, +% pages = {66--70}, +% note = {original Moreau-Broto autocorrelation for small molecules. No DOI was found and the details are from memory} +% } + +% @book{nelson2005lehninger, +% author = {Nelson, David L. and Cox, Michael M.}, +% title = {Lehninger Principles of Biochemistry (4th ed.)}, +% publisher = {W. H. Freeman}, +% year = {2005}, +% note = {textbook with no DOI. peptides.py cites the 4th ed. (2005), so pin the edition whose table you use} +% } + +% @incollection{trifonov1990lc, +% author = {Trifonov, Edward N.}, +% title = {Making sense of the human genome}, +% editor = {Sarma, R. H. and Sarma, M. H.}, +% booktitle = {Structure \& Methods, Vol. 1: Human Genome Initiative \& DNA Recombination}, +% publisher = {Adenine Press}, +% year = {1990}, +% pages = {69--77}, +% note = {book chapter with no DOI, and the details are from memory} +% } + +% @inproceedings{renyi1961entropy, +% author = {Rényi, Alfréd}, +% title = {On measures of entropy and information}, +% booktitle = {Proceedings of the Fourth Berkeley Symposium on Mathematical Statistics and Probability}, +% year = {1961}, +% volume = {1}, +% pages = {547--561}, +% note = {proceedings with no DOI (hosted on Project Euclid). Details are from memory and were not fetched} +% } + +% @article{shannon1948entropy, +% author = {Shannon, Claude E.}, +% title = {{A mathematical theory of communication}}, +% journal = {Bell System Technical Journal}, +% year = {1948}, +% volume = {27}, +% number = {3}, +% pages = {379--423}, +% doi = {10.1002/j.1538-7305.1948.tb01338.x}, +% note = {DOI is taken from the peptides.py docs and was not checked independently because of rate limits} +% } + +% @incollection{dayhoff1978pam, +% author = {Dayhoff, Margaret O. and Schwartz, Robert M. and Orcutt, Bruce C.}, +% title = {A model of evolutionary change in proteins}, +% editor = {Dayhoff, Margaret O.}, +% booktitle = {Atlas of Protein Sequence and Structure, Vol. 5, Suppl. 3}, +% publisher = {National Biomedical Research Foundation}, +% year = {1978}, +% pages = {345--352}, +% note = {Atlas chapter with no DOI, and the details are from memory} +% } + +% @misc{larralde_peptidespy, +% author = {Larralde, Martin}, +% title = {peptides.py: physicochemical properties, indices and descriptors for amino-acid sequences (software)}, +% howpublished = {GitHub: althonos/peptides.py (GPL-3.0)}, +% year = {2021}, +% note = {no DOI, JOSS paper or CITATION file found in the repository. Cite the repo and version. The year is the first-release year and was not confirmed} +% } +% ===== structural ===== +% References for sequence-based structural/functional protein descriptors (protpy gap analysis) +% Every entry below is VERIFIED: its DOI was resolved against Crossref, OpenAlex or Europe PMC on 2026-10-11. +% No UNVERIFIED entries remain, so nothing is commented out. +% UTF-8 encoded; use biber/biblatex or add \usepackage[utf8]{inputenc}. + +% ---- 1. AGGRESCAN ---- +@article{ConchilloSole2007aggrescan, + author = {Conchillo-Sol{\'e}, Oscar and de Groot, Natalia S. and Avil{\'e}s, Francesc X. and Vendrell, Josep and Daura, Xavier and Ventura, Salvador}, + title = {{AGGRESCAN}: a server for the prediction and evaluation of "hot spots" of aggregation in polypeptides}, + journal = {BMC Bioinformatics}, + year = {2007}, + volume = {8}, + pages = {65}, + doi = {10.1186/1471-2105-8-65} +} + +% ---- 2. Uversky charge-hydropathy ---- +@article{Uversky2000natively, + author = {Uversky, Vladimir N. and Gillespie, Joel R. and Fink, Anthony L.}, + title = {Why are "natively unfolded" proteins unstructured under physiologic conditions?}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {2000}, + volume = {41}, + number = {3}, + pages = {415--427}, + doi = {10.1002/1097-0134(20001115)41:3<415::AID-PROT130>3.0.CO;2-7} +} + +% ---- 3. FoldIndex ---- +@article{Prilusky2005foldindex, + author = {Prilusky, Jaime and Felder, Clifford E. and Zeev-Ben-Mordehai, Tzviya and Rydberg, Edwin H. and Man, Orna and Beckmann, Jacques S. and Silman, Israel and Sussman, Joel L.}, + title = {{FoldIndex}{\copyright}: a simple tool to predict whether a given protein sequence is intrinsically unfolded}, + journal = {Bioinformatics}, + year = {2005}, + volume = {21}, + number = {16}, + pages = {3435--3438}, + doi = {10.1093/bioinformatics/bti537} +} + +% ---- 4. FoldUnfold ---- +@article{Galzitskaya2006foldunfold, + author = {Galzitskaya, Oxana V. and Garbuzynskiy, Sergiy O. and Lobanov, Michail Yu.}, + title = {{FoldUnfold}: web server for the prediction of disordered regions in protein chain}, + journal = {Bioinformatics}, + year = {2006}, + volume = {22}, + number = {23}, + pages = {2948--2949}, + doi = {10.1093/bioinformatics/btl504} +} + +% ---- 5. IUPred family ---- +@article{Dosztanyi2005pairwise, + author = {Doszt{\'a}nyi, Zsuzsanna and Csizm{\'o}k, Veronika and Tompa, P{\'e}ter and Simon, Istv{\'a}n}, + title = {The pairwise energy content estimated from amino acid composition discriminates between folded and intrinsically unstructured proteins}, + journal = {Journal of Molecular Biology}, + year = {2005}, + volume = {347}, + number = {4}, + pages = {827--839}, + doi = {10.1016/j.jmb.2005.01.071} +} + +@article{Dosztanyi2005iupred, + author = {Doszt{\'a}nyi, Zsuzsanna and Csizm{\'o}k, Veronika and Tompa, P{\'e}ter and Simon, Istv{\'a}n}, + title = {{IUPred}: web server for the prediction of intrinsically unstructured regions of proteins based on estimated energy content}, + journal = {Bioinformatics}, + year = {2005}, + volume = {21}, + number = {16}, + pages = {3433--3434}, + doi = {10.1093/bioinformatics/bti541} +} + +@article{Meszaros2018iupred2a, + author = {M{\'e}sz{\'a}ros, B{\'a}lint and Erd{\H{o}}s, G{\'a}bor and Doszt{\'a}nyi, Zsuzsanna}, + title = {{IUPred2A}: context-dependent prediction of protein disorder as a function of redox state and protein binding}, + journal = {Nucleic Acids Research}, + year = {2018}, + volume = {46}, + number = {W1}, + pages = {W329--W337}, + doi = {10.1093/nar/gky384} +} + +@article{Erdos2021iupred3, + author = {Erd{\H{o}}s, G{\'a}bor and Pajkos, M{\'a}ty{\'a}s and Doszt{\'a}nyi, Zsuzsanna}, + title = {{IUPred3}: prediction of protein disorder enhanced with unambiguous experimental annotation and visualization of evolutionary conservation}, + journal = {Nucleic Acids Research}, + year = {2021}, + volume = {49}, + number = {W1}, + pages = {W297--W303}, + doi = {10.1093/nar/gkab408} +} + +% ---- 6. Disorder-promoting residues / TOP-IDP ---- +@article{Dunker2001idp, + author = {Dunker, A. Keith and Lawson, J. David and Brown, Celeste J. and Williams, Ryan M. and Romero, Pedro and Oh, Jeong S. and Oldfield, Christopher J. and Campen, Andrew M. and Ratliff, Catherine M. and Hipps, Kerry W. and Ausio, Juan and Nissen, Mark S. and Reeves, Raymond and Kang, ChulHee and Kissinger, Charles R. and Bailey, Robert W. and Griswold, Michael D. and Chiu, Wah and Garner, Ethan C. and Obradovic, Zoran}, + title = {Intrinsically disordered protein}, + journal = {Journal of Molecular Graphics and Modelling}, + year = {2001}, + volume = {19}, + number = {1}, + pages = {26--59}, + doi = {10.1016/S1093-3263(00)00138-8} +} + +@article{Romero2001complexity, + author = {Romero, Pedro and Obradovic, Zoran and Li, Xiaohong and Garner, Ethan C. and Brown, Celeste J. and Dunker, A. Keith}, + title = {Sequence complexity of disordered protein}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {2001}, + volume = {42}, + number = {1}, + pages = {38--48}, + doi = {10.1002/1097-0134(20010101)42:1<38::AID-PROT50>3.0.CO;2-3} +} + +@article{Campen2008topidp, + author = {Campen, Andrew and Williams, Ryan M. and Brown, Celeste J. and Meng, Jingwei and Uversky, Vladimir N. and Dunker, A. Keith}, + title = {{TOP-IDP}-scale: a new amino acid scale measuring propensity for intrinsic disorder}, + journal = {Protein \& Peptide Letters}, + year = {2008}, + volume = {15}, + number = {9}, + pages = {956--963}, + doi = {10.2174/092986608785849164} +} + +% ---- 7. Review of simple disorder predictors ---- +@article{Szilagyi2008twilight, + author = {Szil{\'a}gyi, Andr{\'a}s and Gy{\"o}rffy, D{\'a}niel and Z{\'a}vodszky, P{\'e}ter}, + title = {The twilight zone between protein order and disorder}, + journal = {Biophysical Journal}, + year = {2008}, + volume = {95}, + number = {4}, + pages = {1612--1626}, + doi = {10.1529/biophysj.108.131151} +} + +% ---- 8. metapredict ---- +@article{Emenecker2021metapredict, + author = {Emenecker, Ryan J. and Griffith, Daniel and Holehouse, Alex S.}, + title = {Metapredict: a fast, accurate, and easy-to-use predictor of consensus disorder and structure}, + journal = {Biophysical Journal}, + year = {2021}, + volume = {120}, + number = {20}, + pages = {4312--4319}, + doi = {10.1016/j.bpj.2021.08.039} +} + +@misc{Emenecker2022metapredictV2, + author = {Emenecker, Ryan J. and Griffith, Daniel and Holehouse, Alex S.}, + title = {Metapredict {V2}: an update to metapredict, a fast, accurate, and easy-to-use predictor of consensus disorder and structure}, + howpublished = {bioRxiv preprint 2022.06.06.494887}, + year = {2022}, + doi = {10.1101/2022.06.06.494887} +} + +@misc{Lotthammer2024metapredictV3, + author = {Lotthammer, Jeffrey M. and Hern{\'a}ndez-Garc{\'i}a, Jorge and Griffith, Daniel P. and Weijers, Dolf and Holehouse, Alex S. and Emenecker, Ryan J.}, + title = {Metapredict enables accurate disorder prediction across the Tree of Life}, + howpublished = {bioRxiv preprint 2024.11.05.622168}, + year = {2024}, + doi = {10.1101/2024.11.05.622168}, + note = {metapredict V3 preprint; check for a peer-reviewed version before citing} +} + +% ---- 9. Transmembrane hydropathy / topology ---- +@article{vonHeijne1986positive, + author = {von Heijne, Gunnar}, + title = {The distribution of positively charged residues in bacterial inner membrane proteins correlates with the trans-membrane topology}, + journal = {The EMBO Journal}, + year = {1986}, + volume = {5}, + number = {11}, + pages = {3021--3027}, + doi = {10.1002/j.1460-2075.1986.tb04601.x} +} + +@article{vonHeijne1992membrane, + author = {von Heijne, Gunnar}, + title = {Membrane protein structure prediction: hydrophobicity analysis and the positive-inside rule}, + journal = {Journal of Molecular Biology}, + year = {1992}, + volume = {225}, + number = {2}, + pages = {487--494}, + doi = {10.1016/0022-2836(92)90934-C} +} + +@article{Krogh2001tmhmm, + author = {Krogh, Anders and Larsson, Bj{\"o}rn and von Heijne, Gunnar and Sonnhammer, Erik L. L.}, + title = {Predicting transmembrane protein topology with a hidden {Markov} model: application to complete genomes}, + journal = {Journal of Molecular Biology}, + year = {2001}, + volume = {305}, + number = {3}, + pages = {567--580}, + doi = {10.1006/jmbi.2000.4315} +} + +% ---- 10. Signal peptides ---- +@article{vonHeijne1983patterns, + author = {von Heijne, Gunnar}, + title = {Patterns of amino acids near signal-sequence cleavage sites}, + journal = {European Journal of Biochemistry}, + year = {1983}, + volume = {133}, + number = {1}, + pages = {17--21}, + doi = {10.1111/j.1432-1033.1983.tb07424.x} +} + +@article{vonHeijne1986cleavage, + author = {von Heijne, Gunnar}, + title = {A new method for predicting signal sequence cleavage sites}, + journal = {Nucleic Acids Research}, + year = {1986}, + volume = {14}, + number = {11}, + pages = {4683--4690}, + doi = {10.1093/nar/14.11.4683} +} + +@article{Teufel2022signalp6, + author = {Teufel, Felix and Almagro Armenteros, Jos{\'e} Juan and Johansen, Alexander Rosenberg and G{\'i}slason, Magn{\'u}s Halld{\'o}r and Pihl, Silas Irby and Tsirigos, Konstantinos D. and Winther, Ole and Brunak, S{\o}ren and von Heijne, Gunnar and Nielsen, Henrik}, + title = {{SignalP} 6.0 predicts all five types of signal peptides using protein language models}, + journal = {Nature Biotechnology}, + year = {2022}, + volume = {40}, + number = {7}, + pages = {1023--1025}, + doi = {10.1038/s41587-021-01156-3} +} + +% ---- 11. Motifs ---- +@article{Sigrist2013prosite, + author = {Sigrist, Christian J. A. and de Castro, Edouard and Cerutti, Lorenzo and Cuche, B{\'e}atrice A. and Hulo, Nicolas and Bridge, Alan and Bougueleret, Lydie and Xenarios, Ioannis}, + title = {New and continuing developments at {PROSITE}}, + journal = {Nucleic Acids Research}, + year = {2013}, + volume = {41}, + number = {D1}, + pages = {D344--D347}, + doi = {10.1093/nar/gks1067} +} + +@article{Marshall1972glycoproteins, + author = {Marshall, R. D.}, + title = {Glycoproteins}, + journal = {Annual Review of Biochemistry}, + year = {1972}, + volume = {41}, + pages = {673--702}, + doi = {10.1146/annurev.bi.41.070172.003325} +} + +@article{Gavel1990sequon, + author = {Gavel, Yvonne and von Heijne, Gunnar}, + title = {Sequence differences between glycosylated and non-glycosylated {Asn-X-Thr/Ser} acceptor sites: implications for protein engineering}, + journal = {Protein Engineering}, + year = {1990}, + volume = {3}, + number = {5}, + pages = {433--442}, + doi = {10.1093/protein/3.5.433} +} + +@article{Kalderon1984cell, + author = {Kalderon, Daniel and Roberts, Bruce L. and Richardson, William D. and Smith, Alan E.}, + title = {A short amino acid sequence able to specify nuclear location}, + journal = {Cell}, + year = {1984}, + volume = {39}, + number = {3}, + pages = {499--509}, + doi = {10.1016/0092-8674(84)90457-4} +} + +@article{Kalderon1984nature, + author = {Kalderon, Daniel and Richardson, William D. and Markham, Alexander F. and Smith, Alan E.}, + title = {Sequence requirements for nuclear location of simian virus 40 large-{T} antigen}, + journal = {Nature}, + year = {1984}, + volume = {311}, + number = {5981}, + pages = {33--38}, + doi = {10.1038/311033a0} +} + +@article{Kosugi2009cnls, + author = {Kosugi, Shunichi and Hasebe, Masako and Tomita, Masaru and Yanagawa, Hiroshi}, + title = {Systematic identification of cell cycle-dependent yeast nucleocytoplasmic shuttling proteins by prediction of composite motifs}, + journal = {Proceedings of the National Academy of Sciences}, + year = {2009}, + volume = {106}, + number = {25}, + pages = {10171--10176}, + doi = {10.1073/pnas.0900604106} +} + +@article{laCour2004nes, + author = {la Cour, Tanja and Kiemer, Lars and M{\o}lgaard, Anne and Gupta, Ramneek and Skriver, Karen and Brunak, S{\o}ren}, + title = {Analysis and prediction of leucine-rich nuclear export signals}, + journal = {Protein Engineering, Design and Selection}, + year = {2004}, + volume = {17}, + number = {6}, + pages = {527--536}, + doi = {10.1093/protein/gzh062} +} + +@article{Munro1987kdel, + author = {Munro, Sean and Pelham, Hugh R. B.}, + title = {A {C}-terminal signal prevents secretion of luminal {ER} proteins}, + journal = {Cell}, + year = {1987}, + volume = {48}, + number = {5}, + pages = {899--907}, + doi = {10.1016/0092-8674(87)90086-9} +} + +@article{Gould1989pts1, + author = {Gould, Stephen J. and Keller, Gilbert-Andr{\'e} and Hosken, Nancy and Wilkinson, Jack and Subramani, Suresh}, + title = {A conserved tripeptide sorts proteins to peroxisomes}, + journal = {Journal of Cell Biology}, + year = {1989}, + volume = {108}, + number = {5}, + pages = {1657--1664}, + doi = {10.1083/jcb.108.5.1657}, + pmid = {2654139} +} + +@article{Olsen2004trypsin, + author = {Olsen, Jesper V. and Ong, Shao-En and Mann, Matthias}, + title = {Trypsin cleaves exclusively {C}-terminal to arginine and lysine residues}, + journal = {Molecular \& Cellular Proteomics}, + year = {2004}, + volume = {3}, + number = {6}, + pages = {608--614}, + doi = {10.1074/mcp.T400003-MCP200} +} + +% ---- 12. Secondary-structure propensity ---- +@article{Chou1974parameters, + author = {Chou, Peter Y. and Fasman, Gerald D.}, + title = {Conformational parameters for amino acids in helical, $\beta$-sheet, and random coil regions calculated from proteins}, + journal = {Biochemistry}, + year = {1974}, + volume = {13}, + number = {2}, + pages = {211--222}, + doi = {10.1021/bi00699a001} +} + +@article{Chou1974prediction, + author = {Chou, Peter Y. and Fasman, Gerald D.}, + title = {Prediction of protein conformation}, + journal = {Biochemistry}, + year = {1974}, + volume = {13}, + number = {2}, + pages = {222--245}, + doi = {10.1021/bi00699a002} +} + +@article{Chou1978advenzymol, + author = {Chou, Peter Y. and Fasman, Gerald D.}, + title = {Prediction of the secondary structure of proteins from their amino acid sequence}, + journal = {Advances in Enzymology and Related Areas of Molecular Biology}, + year = {1978}, + volume = {47}, + pages = {45--148}, + doi = {10.1002/9780470122921.ch2} +} + +@article{Garnier1978gor, + author = {Garnier, Jean and Osguthorpe, David J. and Robson, Barry}, + title = {Analysis of the accuracy and implications of simple methods for predicting the secondary structure of globular proteins}, + journal = {Journal of Molecular Biology}, + year = {1978}, + volume = {120}, + number = {1}, + pages = {97--120}, + doi = {10.1016/0022-2836(78)90297-8} +} + +@article{Garnier1996gor4, + author = {Garnier, Jean and Gibrat, Jean-Fran{\c{c}}ois and Robson, Barry}, + title = {{GOR} method for predicting protein secondary structure from amino acid sequence}, + journal = {Methods in Enzymology}, + year = {1996}, + volume = {266}, + pages = {540--553}, + doi = {10.1016/S0076-6879(96)66034-0} +} + +@article{Levitt1978conformational, + author = {Levitt, Michael}, + title = {Conformational preferences of amino acids in globular proteins}, + journal = {Biochemistry}, + year = {1978}, + volume = {17}, + number = {20}, + pages = {4277--4285}, + doi = {10.1021/bi00613a026} +} + +@article{Deleage1987class, + author = {Del{\'e}age, Gilbert and Roux, Bernard}, + title = {An algorithm for protein secondary structure prediction based on class prediction}, + journal = {Protein Engineering}, + year = {1987}, + volume = {1}, + number = {4}, + pages = {289--294}, + doi = {10.1093/protein/1.4.289} +} + +% ---- 13. Coiled coils ---- +@article{Lupas1991coils, + author = {Lupas, Andrei and Van Dyke, Marc and Stock, Jeff}, + title = {Predicting coiled coils from protein sequences}, + journal = {Science}, + year = {1991}, + volume = {252}, + number = {5009}, + pages = {1162--1164}, + doi = {10.1126/science.252.5009.1162} +} + +% ---- 14. FoldAmyloid ---- +@article{Garbuzynskiy2010foldamyloid, + author = {Garbuzynskiy, Sergiy O. and Lobanov, Michail Yu. and Galzitskaya, Oxana V.}, + title = {{FoldAmyloid}: a method of prediction of amyloidogenic regions from protein sequence}, + journal = {Bioinformatics}, + year = {2010}, + volume = {26}, + number = {3}, + pages = {326--332}, + doi = {10.1093/bioinformatics/btp691} +} + +% ---- 15. PLAAC ---- +@article{Lancaster2014plaac, + author = {Lancaster, Alex K. and Nutter-Upham, Andrew and Lindquist, Susan and King, Oliver D.}, + title = {{PLAAC}: a web and command-line application to identify proteins with prion-like amino acid composition}, + journal = {Bioinformatics}, + year = {2014}, + volume = {30}, + number = {17}, + pages = {2501--2502}, + doi = {10.1093/bioinformatics/btu310} +} + +@article{Alberti2009prions, + author = {Alberti, Simon and Halfmann, Randal and King, Oliver and Kapila, Atul and Lindquist, Susan}, + title = {A systematic survey identifies prions and illuminates sequence features of prionogenic proteins}, + journal = {Cell}, + year = {2009}, + volume = {137}, + number = {1}, + pages = {146--158}, + doi = {10.1016/j.cell.2009.02.044} +} + +% ---- 16. Antigenicity / surface / flexibility scales ---- +@article{Emini1985surface, + author = {Emini, Emilio A. and Hughes, James V. and Perlow, Deborah S. and Boger, Joshua}, + title = {Induction of hepatitis {A} virus-neutralizing antibody by a virus-specific synthetic peptide}, + journal = {Journal of Virology}, + year = {1985}, + volume = {55}, + number = {3}, + pages = {836--839}, + doi = {10.1128/jvi.55.3.836-839.1985} +} + +@article{Parker1986hydrophilicity, + author = {Parker, J. M. Robert and Guo, Dean and Hodges, Robert S.}, + title = {New hydrophilicity scale derived from high-performance liquid chromatography peptide retention data: correlation of predicted surface residues with antigenicity and {X}-ray-derived accessible sites}, + journal = {Biochemistry}, + year = {1986}, + volume = {25}, + number = {19}, + pages = {5425--5432}, + doi = {10.1021/bi00367a013} +} + +@article{Kolaskar1990antigenic, + author = {Kolaskar, A. S. and Tongaonkar, Prasad C.}, + title = {A semi-empirical method for prediction of antigenic determinants on protein antigens}, + journal = {FEBS Letters}, + year = {1990}, + volume = {276}, + number = {1--2}, + pages = {172--174}, + doi = {10.1016/0014-5793(90)80535-Q} +} + +@article{Janin1979surface, + author = {Janin, Jo{\"e}l}, + title = {Surface and inside volumes in globular proteins}, + journal = {Nature}, + year = {1979}, + volume = {277}, + number = {5696}, + pages = {491--492}, + doi = {10.1038/277491a0} +} + + +% ---- 17. Phase separation ---- +@article{Martin2020valence, + author = {Martin, Erik W. and Holehouse, Alex S. and Peran, Ivan and Farag, Mina and Incicco, J. Jeremias and Bremer, Anne and Grace, Christy R. and Soranno, Andrea and Pappu, Rohit V. and Mittag, Tanja}, + title = {Valence and patterning of aromatic residues determine the phase behavior of prion-like domains}, + journal = {Science}, + year = {2020}, + volume = {367}, + number = {6478}, + pages = {694--699}, + doi = {10.1126/science.aaw8653} +} + +@article{Vernon2018pscore, + author = {Vernon, Robert McCoy and Chong, Paul Andrew and Tsang, Brian and Kim, Tae Hun and Bah, Alaji and Farber, Patrick and Lin, Hong and Forman-Kay, Julie Deborah}, + title = {Pi-Pi contacts are an overlooked protein feature relevant to phase separation}, + journal = {eLife}, + year = {2018}, + volume = {7}, + pages = {e31486}, + doi = {10.7554/eLife.31486} +} + +@article{Bolognesi2016catgranule, + author = {Bolognesi, Benedetta and Lorenzo Gotor, Nieves and Dhar, Riddhiman and Cirillo, Davide and Baldrighi, Marta and Tartaglia, Gian Gaetano and Lehner, Ben}, + title = {A concentration-dependent liquid phase separation can cause toxicity upon increased protein expression}, + journal = {Cell Reports}, + year = {2016}, + volume = {16}, + number = {1}, + pages = {222--231}, + doi = {10.1016/j.celrep.2016.05.076} +} + +@article{Hardenberg2020fuzdrop, + author = {Hardenberg, Maarten and Horvath, Attila and Ambrus, Viktor and Fuxreiter, Monika and Vendruscolo, Michele}, + title = {Widespread occurrence of the droplet state of proteins in the human proteome}, + journal = {Proceedings of the National Academy of Sciences}, + year = {2020}, + volume = {117}, + number = {52}, + pages = {33254--33262}, + doi = {10.1073/pnas.2007670117} +} + +% ---- 18. PPII propensity ---- +@article{Rucker2003ppii, + author = {Rucker, Adam L. and Pager, Cara T. and Campbell, Mary N. and Qualls, Joseph E. and Creamer, Trevor P.}, + title = {Host-guest scale of left-handed polyproline {II} helix formation}, + journal = {Proteins: Structure, Function, and Bioinformatics}, + year = {2003}, + volume = {53}, + number = {1}, + pages = {68--75}, + doi = {10.1002/prot.10477} +} + +@article{Shi2005ppii, + author = {Shi, Zhengshuang and Chen, Kang and Liu, Zhigang and Ng, Amanda and Bracken, W. Clay and Kallenbach, Neville R.}, + title = {Polyproline {II} propensities from {GGXGG} peptides reveal an anticorrelation with $\beta$-sheet scales}, + journal = {Proceedings of the National Academy of Sciences}, + year = {2005}, + volume = {102}, + number = {50}, + pages = {17964--17968}, + doi = {10.1073/pnas.0507124102} +} + +@article{Elam2013ppii, + author = {Elam, W. Austin and Schrank, Travis P. and Campagnolo, Andrew J. and Hilser, Vincent J.}, + title = {Evolutionary conservation of the polyproline {II} conformation surrounding intrinsically disordered phosphorylation sites}, + journal = {Protein Science}, + year = {2013}, + volume = {22}, + number = {4}, + pages = {405--417}, + doi = {10.1002/pro.2217} +} + +% ---- 19. CxxC motifs ---- +@article{Fomenko2003cxxc, + author = {Fomenko, Dmitri E. and Gladyshev, Vadim N.}, + title = {Identity and functions of {CxxC}-derived motifs}, + journal = {Biochemistry}, + year = {2003}, + volume = {42}, + number = {38}, + pages = {11214--11225}, + doi = {10.1021/bi034459s} +} + +% ---- 20. Excluded tools (background only) ---- +@article{FernandezEscamilla2004tango, + author = {Fernandez-Escamilla, Ana-Maria and Rousseau, Frederic and Schymkowitz, Joost and Serrano, Luis}, + title = {Prediction of sequence-dependent and mutational effects on the aggregation of peptides and proteins}, + journal = {Nature Biotechnology}, + year = {2004}, + volume = {22}, + number = {10}, + pages = {1302--1306}, + doi = {10.1038/nbt1012} +} + +@article{Sormanni2015camsol, + author = {Sormanni, Pietro and Aprile, Francesco A. and Vendruscolo, Michele}, + title = {The {CamSol} method of rational design of protein mutants with enhanced solubility}, + journal = {Journal of Molecular Biology}, + year = {2015}, + volume = {427}, + number = {2}, + pages = {478--490}, + doi = {10.1016/j.jmb.2014.09.026} +} + +@article{Munoz1994agadir, + author = {Mu{\~n}oz, Victor and Serrano, Luis}, + title = {Elucidating the folding problem of helical peptides using empirical parameters}, + journal = {Nature Structural Biology}, + year = {1994}, + volume = {1}, + number = {6}, + pages = {399--409}, + doi = {10.1038/nsb0694-399} +} + +@article{Kall2004phobius, + author = {K{\"a}ll, Lukas and Krogh, Anders and Sonnhammer, Erik L. L.}, + title = {A combined transmembrane topology and signal peptide prediction method}, + journal = {Journal of Molecular Biology}, + year = {2004}, + volume = {338}, + number = {5}, + pages = {1027--1036}, + doi = {10.1016/j.jmb.2004.03.016} +} + +@article{Tartaglia2008zyggregator, + author = {Tartaglia, Gian Gaetano and Pawar, Amol P. and Campioni, Silvia and Dobson, Christopher M. and Chiti, Fabrizio and Vendruscolo, Michele}, + title = {Prediction of aggregation-prone regions in structured proteins}, + journal = {Journal of Molecular Biology}, + year = {2008}, + volume = {380}, + number = {2}, + pages = {425--436}, + doi = {10.1016/j.jmb.2008.05.013} +} + +@article{MaurerStroh2010waltz, + author = {Maurer-Stroh, Sebastian and Debulpaep, Maja and Kuemmerer, Nico and Lopez de la Paz, Manuela and Martins, Ivo Cristiano and Reumers, Joke and Morris, Kyle L. and Copland, Alastair and Serpell, Louise and Serrano, Luis and Schymkowitz, Joost W. H. and Rousseau, Frederic}, + title = {Exploring the sequence determinants of amyloid structure using position-specific scoring matrices}, + journal = {Nature Methods}, + year = {2010}, + volume = {7}, + number = {3}, + pages = {237--242}, + doi = {10.1038/nmeth.1432} +} + +@article{Walsh2014pasta2, + author = {Walsh, Ian and Seno, Flavio and Tosatto, Silvio C. E. and Trovato, Antonio}, + title = {{PASTA} 2.0: an improved server for protein aggregation prediction}, + journal = {Nucleic Acids Research}, + year = {2014}, + volume = {42}, + number = {W1}, + pages = {W301--W307}, + doi = {10.1093/nar/gku399} +} + +@article{Hebditch2017proteinsol, + author = {Hebditch, Max and Carballo-Amador, M. Alejandro and Charonis, Spyros and Curtis, Robin and Warwicker, Jim}, + title = {Protein--Sol: a web tool for predicting protein solubility from sequence}, + journal = {Bioinformatics}, + year = {2017}, + volume = {33}, + number = {19}, + pages = {3098--3100}, + doi = {10.1093/bioinformatics/btx345} +} + + +% ===== added from Crossref (protpy registry) ===== +@article{Chou_2000, title={Prediction of Protein Subcellular Locations by Incorporating Quasi-Sequence-Order Effect}, volume={278}, ISSN={0006-291X}, url={http://dx.doi.org/10.1006/bbrc.2000.3815}, DOI={10.1006/bbrc.2000.3815}, number={2}, journal={Biochemical and Biophysical Research Communications}, publisher={Elsevier BV}, author={Chou, Kuo-Chen}, year={2000}, month=Nov, pages={477–483} } + +@article{Hollas_2003, title={An Analysis of the Autocorrelation Descriptor for Molecules}, volume={33}, ISSN={1572-8897}, url={http://dx.doi.org/10.1023/A:1023247831238}, DOI={10.1023/a:1023247831238}, number={2}, journal={Journal of Mathematical Chemistry}, publisher={Springer Science and Business Media LLC}, author={Hollas, Boris}, year={2003}, month=Feb, pages={91–101} } + +@article{Boman_2003, title={Antibacterial peptides: basic facts and emerging concepts}, volume={254}, ISSN={1365-2796}, url={http://dx.doi.org/10.1046/j.1365-2796.2003.01228.x}, DOI={10.1046/j.1365-2796.2003.01228.x}, number={3}, journal={Journal of Internal Medicine}, publisher={Wiley}, author={Boman, H. G.}, year={2003}, month=Aug, pages={197–215} } + +@article{Lobry_1994, title={Hydrophobicity, expressivity and aromaticity are the major trends of amino-acid usage in 999 Escherichia coli chromosome-encoded genes}, volume={22}, ISSN={1362-4962}, url={http://dx.doi.org/10.1093/nar/22.15.3174}, DOI={10.1093/nar/22.15.3174}, number={15}, journal={Nucleic Acids Research}, publisher={Oxford University Press (OUP)}, author={Lobry, J.R. and Gautier, C.}, year={1994}, pages={3174–3180} } + +@article{1980, ISSN={1756-2651}, url={http://dx.doi.org/10.1093/oxfordjournals.jbchem.a133168}, DOI={10.1093/oxfordjournals.jbchem.a133168}, journal={The Journal of Biochemistry}, publisher={Oxford University Press (OUP)}, year={1980}, month=Oct } + +@article{Guruprasad_1990, title={Correlation between stability of a protein and its dipeptide composition: a novel approach for predicting + in vivo + stability of a protein from its primary sequence}, volume={4}, ISSN={1741-0134}, url={http://dx.doi.org/10.1093/protein/4.2.155}, DOI={10.1093/protein/4.2.155}, number={2}, journal={“Protein Engineering, Design and Selection”}, publisher={Oxford University Press (OUP)}, author={Guruprasad, Kunchur and Reddy, B.V.Bhasker and Pandit, Madhusudan W.}, year={1990}, pages={155–161} } + +@article{Ong_2007, title={Efficacy of different protein descriptors in predicting protein functional families}, volume={8}, ISSN={1471-2105}, url={http://dx.doi.org/10.1186/1471-2105-8-300}, DOI={10.1186/1471-2105-8-300}, number={1}, journal={BMC Bioinformatics}, publisher={Springer Science and Business Media LLC}, author={Ong, Serene AK and Lin, Hong Huang and Chen, Yu Zong and Li, Ze Rong and Cao, Zhiwei}, year={2007}, month=Aug } diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md new file mode 100644 index 0000000..4c7566a --- /dev/null +++ b/planning/BLOCKED.md @@ -0,0 +1,8 @@ +# v1.5.0 blocked items + +Items from IMPLEMENTATION_PLAN.md that could not be implemented without inventing data. +Each entry names the table that is missing and where it would have to come from. + +| # | Item | Missing data | Notes | +|---|---|---|---| +| 6 | `psekraac` | The 16 PseKRAAC reduced-alphabet type tables (types 1–16, every cluster size) | The Zuo et al. 2017 supplement (`bioinformatics_33_1_122_s1.doc`, fetched 2026-10-12) only lists the 16 types and their source papers; it does not contain the cluster tables. They would have to be transcribed from the 16 source papers (Liang 2015, Ogul 2007, Kosiol 2004, Zuo 2010, Melo 2006, Rakshit 2008, Susko 2007, Liu 2002, Li 2007, Peterson 2009, Cannata 2002, Li 2003) or obtained from the PseKRAAC server authors. The iFeature `PseKRAAC/` copies have no licence. | diff --git a/protpy/_helpers.py b/protpy/_helpers.py new file mode 100644 index 0000000..ef9cf46 --- /dev/null +++ b/protpy/_helpers.py @@ -0,0 +1,153 @@ +################################################################################ +############# Internal Helpers ############## +################################################################################ + +from __future__ import annotations + +""" +Internal engines shared by the descriptor modules added in v1.5.0. Nothing in +this module is exported from the protpy namespace. +""" + +import copy +import json +import os +from collections import Counter +from functools import lru_cache + +import numpy as np +import pandas as pd + +from ._constants import amino_acids + +_DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") + + +@lru_cache(maxsize=None) +def _read_json(filename: str) -> dict: + with open(os.path.join(_DATA_DIR, filename)) as f: + return json.load(f) + + +def _load_json_table(filename: str, key: str) -> dict: + """ + Return a copy of table ``key`` from ``protpy/data/``. Each table + has the keys name, values, reference, doi and source_note. + + Raises + ====== + KeyError + if ``key`` is not a table in the file; the message lists the valid keys. + """ + tables = _read_json(filename) + if key not in tables: + raise KeyError(f"Table '{key}' not found in {filename}. Valid tables are: {sorted(tables)}.") + #copy so callers cannot mutate the cached table + return copy.deepcopy(tables[key]) + + +def _scale_values(scale: str | dict) -> dict: + """ + Resolve a scale to {aa: float | list[float]}. ``scale`` may be a table name + in aa_scales.json, an AAindex1 accession code, or a user dict. + """ + if isinstance(scale, dict): + return scale + tables = _read_json("aa_scales.json") + if scale in tables: + return tables[scale]["values"] + from .autocorrelation import _get_properties + try: + return _get_properties(scale, normalize=False)[scale] + except ValueError: + raise ValueError(f"Unknown scale '{scale}'. Use a protpy scale name {sorted(tables)}, " + "an AAindex1 accession code, or a {amino_acid: value} dict.") from None + + +def _property_matrix(sequence: str, scales: str | list[str] | dict, standardize: bool = False) -> np.ndarray: + """ + Return the L x d matrix of property values for ``sequence``. + + ``scales`` may be a factor-scale family / single-value scale name from + aa_scales.json, an AAindex1 accession code, a list of AAindex1 codes, or a + user dict {aa: float} / {aa: list[float]}. With ``standardize=True`` each + dimension is z-scored over the 20 canonical amino acids (population std). + """ + if isinstance(scales, (list, tuple)): + tables = [_scale_values(s) for s in scales] + table = {aa: [t[aa] for t in tables] for aa in amino_acids} + else: + table = _scale_values(scales) + missing = [aa for aa in amino_acids if aa not in table] + if missing: + raise ValueError(f"Scale is missing values for amino acids: {missing}.") + #rows ordered by amino_acids so standardisation is over exactly the 20 residues + aa_matrix = np.array([np.atleast_1d(np.asarray(table[aa], dtype=float)) for aa in amino_acids]) + if standardize: + std = aa_matrix.std(axis=0) + aa_matrix = np.divide(aa_matrix - aa_matrix.mean(axis=0), std, + out=np.zeros_like(aa_matrix), where=std != 0) + index = {aa: i for i, aa in enumerate(amino_acids)} + return aa_matrix[[index[aa] for aa in sequence]] + + +def _sliding_windows(values: np.ndarray, window: int, edge: float = 1.0) -> np.ndarray: + """ + Weighted sliding-window means of a 1-D profile. + + Weights fall linearly from ``edge`` at the window ends to 1 at the centre + (ExPASy ProtScale / Biopython ``protein_scale`` semantics); ``edge=1`` is a + plain mean. If ``window`` exceeds the profile length a single window of the + full length is used. + """ + values = np.asarray(values, dtype=float) + window = min(window, len(values)) + half = (window - 1) / 2 + dist = np.minimum(np.arange(window), np.arange(window)[::-1]) + weights = edge + (1 - edge) * dist / half if half else np.ones(window) + weights = np.minimum(weights, 1.0) + return np.convolve(values, weights[::-1], mode="valid") / weights.sum() + + +def _profile_stats(profile: np.ndarray, prefix: str, stats=("mean", "max", "min", "std"), + threshold: float | None = None) -> dict: + """Reduce a profile to {prefix_stat: value}; adds ``frac_above`` when ``threshold`` is set.""" + profile = np.asarray(profile, dtype=float) + funcs = {"mean": np.mean, "max": np.max, "min": np.min, "std": np.std} + out = {f"{prefix}_{s}": float(funcs[s](profile)) for s in stats} + if threshold is not None: + out[f"{prefix}_frac_above"] = float(np.mean(profile > threshold)) + return out + + +def _gapped_pair_counts(sequence: str, gap: int, alphabet: dict[str, str] | None = None) -> Counter: + """Count pairs (x_i, x_{i+gap+1}), optionally mapped through a residue -> group alphabet.""" + seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence) + step = gap + 1 + return Counter((seq[i], seq[i + step]) for i in range(len(seq) - step)) + + +def _kmer_counts(sequence: str, k: int, alphabet: dict[str, str] | None = None) -> Counter: + """Overlapping k-mer counts (as tuples) over the raw or reduced alphabet.""" + seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence) + return Counter(tuple(seq[i:i + k]) for i in range(len(seq) - k + 1)) + + +def _charge_vector(sequence: str, his: bool = False) -> np.ndarray: + """+1 for K/R (and H when ``his``), -1 for D/E, else 0.""" + positive = "KRH" if his else "KR" + return np.array([1 if aa in positive else -1 if aa in "DE" else 0 for aa in sequence], dtype=float) + + +def _runs(mask: np.ndarray, min_len: int = 1) -> list[tuple[int, int]]: + """Return (start, end) half-open index pairs of consecutive True runs of length >= min_len.""" + mask = np.concatenate(([False], np.asarray(mask, dtype=bool), [False])) + edges = np.flatnonzero(np.diff(mask.astype(int))) + return [(int(s), int(e)) for s, e in zip(edges[::2], edges[1::2]) if e - s >= min_len] + + +def _to_df(values: list, columns: list[str]) -> pd.DataFrame: + """Round floats to 3 dp and return a 1 x N DataFrame.""" + values = [round(float(v), 3) if isinstance(v, (float, np.floating)) else + int(v) if isinstance(v, (bool, np.bool_, np.integer)) else v for v in values] + return pd.DataFrame([values], columns=columns) diff --git a/protpy/data/aa_scales.json b/protpy/data/aa_scales.json new file mode 100644 index 0000000..d97e1d7 --- /dev/null +++ b/protpy/data/aa_scales.json @@ -0,0 +1,198 @@ +{ + "kyte_doolittle": { + "name": "Hydropathy index (Kyte-Doolittle, 1982)", + "values": { + "A": 1.8, + "C": 2.5, + "D": -3.5, + "E": -3.5, + "F": 2.8, + "G": -0.4, + "H": -3.2, + "I": 4.5, + "K": -3.9, + "L": 3.8, + "M": 1.9, + "N": -3.5, + "P": -1.6, + "Q": -3.5, + "R": -4.5, + "S": -0.8, + "T": -0.7, + "V": 4.2, + "W": -0.9, + "Y": -1.3 + }, + "reference": "Kyte, J. and Doolittle, R.F. 'A simple method for displaying the hydropathic character of a protein' J. Mol. Biol. 157, 105-132 (1982)", + "doi": "10.1016/0022-2836(82)90515-0", + "source_note": "AAindex1 record KYTJ820101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "hopp_woods": { + "name": "Hydrophilicity value (Hopp-Woods, 1981)", + "values": { + "A": -0.5, + "C": -1.0, + "D": 3.0, + "E": 3.0, + "F": -2.5, + "G": 0.0, + "H": -0.5, + "I": -1.8, + "K": 3.0, + "L": -1.8, + "M": -1.3, + "N": 0.2, + "P": 0.0, + "Q": 0.2, + "R": 3.0, + "S": 0.3, + "T": -0.4, + "V": -1.5, + "W": -3.4, + "Y": -2.3 + }, + "reference": "Hopp, T.P. and Woods, K.R. 'Prediction of protein antigenic determinants from amino acid sequecces' Proc. Natl. Acad. Sci. USA 78, 3824-3828 (1981)", + "doi": "10.1073/pnas.78.6.3824", + "source_note": "AAindex1 record HOPT810101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "eisenberg": { + "name": "Consensus normalized hydrophobicity scale (Eisenberg, 1984)", + "values": { + "A": 0.25, + "C": 0.04, + "D": -0.72, + "E": -0.62, + "F": 0.61, + "G": 0.16, + "H": -0.4, + "I": 0.73, + "K": -1.1, + "L": 0.53, + "M": 0.26, + "N": -0.64, + "P": -0.07, + "Q": -0.69, + "R": -1.76, + "S": -0.26, + "T": -0.18, + "V": 0.54, + "W": 0.37, + "Y": 0.02 + }, + "reference": "Eisenberg, D. 'Three-dimensional structure of membrane and surface proteins' Ann. Rev. Biochem. 53, 595-623 (1984) Original references: Eisenberg, D., Weiss, R.M., Terwilliger, T.C. and Wilcox, W. Faraday Symp. Chem. Soc. 17, 109-120 (1982) Eisenberg, D., Weiss, R.M. and Terwilliger, T.C. The hydrophobic moment detects periodicity in protein hydrophobicity Proc. Natl. Acad. Sci. USA 81, 140-144 (1984)", + "doi": "10.1146/annurev.bi.53.070184.003115", + "source_note": "AAindex1 record EISD840101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "wimley_white": { + "name": "Free energies of transfer of AcWl-X-LL peptides from bilayer interface to water (Wimley-White, 1996)", + "values": { + "A": 4.08, + "C": 4.49, + "D": 3.02, + "E": 2.23, + "F": 5.38, + "G": 4.24, + "H": 4.08, + "I": 4.52, + "K": 3.77, + "L": 4.81, + "M": 4.48, + "N": 3.83, + "P": 3.8, + "Q": 3.67, + "R": 3.91, + "S": 4.12, + "T": 4.11, + "V": 4.18, + "W": 6.1, + "Y": 5.19 + }, + "reference": "Wimley, W.C. and White, S. 'Experimentally determined hydrophobicity scale for proteins at membrane interfaces' Nature Structual biol. 3, 842-848 (1996)", + "doi": "10.1038/nsb1096-842", + "source_note": "AAindex1 record WIMW960101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "engelman": { + "name": "Hydrophobicity index (Engelman et al., 1986)", + "values": { + "A": -1.6, + "C": -2.0, + "D": 9.2, + "E": 8.2, + "F": -3.7, + "G": -1.0, + "H": 3.0, + "I": -3.1, + "K": 8.8, + "L": -2.8, + "M": -3.4, + "N": 4.8, + "P": 0.2, + "Q": 4.1, + "R": 12.3, + "S": -0.6, + "T": -1.2, + "V": -2.6, + "W": -1.9, + "Y": 0.7 + }, + "reference": "Engelman, D.M., Steitz, T.A. and Goldman, A. 'Identifying Nonpolar Transbilayer Helices in Amino Acid Sequences of Membrane Proteins' Ann.Rev.Biophys.Biophys.Chem. 15, 321-353 (1986)", + "doi": "10.1146/annurev.bb.15.060186.001541", + "source_note": "AAindex1 record ENGD860101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "domcut": { + "name": "Linker propensity index (Suyama-Ohara, 2003)", + "values": { + "A": -0.058, + "C": 0.447, + "D": 0.016, + "E": -0.128, + "F": 0.24, + "G": 0.331, + "H": 0.195, + "I": 0.06, + "K": -0.112, + "L": 0.138, + "M": 0.275, + "N": 0.027, + "P": -0.478, + "Q": -0.073, + "R": 0.0, + "S": -0.177, + "T": -0.163, + "V": -0.052, + "W": 0.564, + "Y": 0.322 + }, + "reference": "Suyama, M. and Ohara, O. 'DomCut: Prediction of inter-domain linker regions in amino acid sequences' Bioinformatics 19, 673-674 (2003)", + "doi": "10.1093/bioinformatics/btg031", + "source_note": "AAindex1 record SUYM030101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "eiip": { + "name": "Electron-ion interaction potential values (Cosic, 1994)", + "values": { + "A": 0.0373, + "C": 0.0829, + "D": 0.1263, + "E": 0.0058, + "F": 0.0946, + "G": 0.005, + "H": 0.0242, + "I": 0.0, + "K": 0.0371, + "L": 0.0, + "M": 0.0823, + "N": 0.0036, + "P": 0.0198, + "Q": 0.0761, + "R": 0.0959, + "S": 0.0829, + "T": 0.0941, + "V": 0.0057, + "W": 0.0548, + "Y": 0.0516 + }, + "reference": "Cosic, I. 'Macromolecular bioactivity: is it resonant interaction between macromolecules?--Theory and applications' IEEE Trans Biomed Eng. 41, 1101-1114 (1994) (values are cited from Protein Eng. 15:193-203)", + "doi": "10.1109/10.335859", + "source_note": "AAindex1 record COSI940101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + } +} \ No newline at end of file diff --git a/protpy/data/reduced_alphabets.json b/protpy/data/reduced_alphabets.json new file mode 100644 index 0000000..89bbf57 --- /dev/null +++ b/protpy/data/reduced_alphabets.json @@ -0,0 +1,151 @@ +{ + "ifeature5": { + "name": "iFeature 5-group alphabet (GAAC/CKSAAGP)", + "values": { + "aliphatic": "GAVLMI", + "aromatic": "FYW", + "positive": "KRH", + "negative": "DE", + "uncharged": "STCPNQ" + }, + "reference": "iFeature: Chen Z. et al. (2018). iFeature: a Python package and web server for features extraction and selection from protein and peptide sequences. Bioinformatics, 34(14), 2499-2502.", + "doi": "10.1093/bioinformatics/bty140", + "source_note": "Groups as defined for GAAC in the iFeature paper and its reference implementation; transcribed 2026-10-12." + }, + "cp13": { + "name": "cp(13) cluster profile", + "values": [ + "MF", + "IL", + "V", + "A", + "C", + "WYQHP", + "G", + "T", + "S", + "N", + "RK", + "D", + "E" + ], + "reference": "Liu B. et al. (2014). iDNA-Prot|dis: identifying DNA-binding proteins by incorporating amino acid distance-pairs and reduced alphabet profile into the general pseudo amino acid composition. PLoS ONE, 9(9), e106691.", + "doi": "10.1371/journal.pone.0106691", + "source_note": "Eq. 10 of the paper (image pone.0106691.e027); transcribed 2026-10-12." + }, + "cp14": { + "name": "cp(14) cluster profile", + "values": [ + "IMV", + "L", + "F", + "WY", + "G", + "P", + "C", + "A", + "S", + "T", + "N", + "HRKQ", + "E", + "D" + ], + "reference": "Liu B. et al. (2014). iDNA-Prot|dis: identifying DNA-binding proteins by incorporating amino acid distance-pairs and reduced alphabet profile into the general pseudo amino acid composition. PLoS ONE, 9(9), e106691.", + "doi": "10.1371/journal.pone.0106691", + "source_note": "Eq. 10 of the paper prints cp(14) = {EIMV; L; F; WY; G; P; C; A; S; T; N; HRKQ; E; D}, listing E twice. The paper fixes n(c)=14 (dimension 14+14x14x3=602), so the stray E in the first cluster is a typo: dropping it is the only reading with 14 clusters that partitions the 20 residues. Transcribed 2026-10-12." + }, + "cp19": { + "name": "cp(19) cluster profile", + "values": [ + "P", + "G", + "E", + "K", + "R", + "Q", + "D", + "S", + "N", + "T", + "H", + "C", + "I", + "V", + "W", + "YF", + "A", + "L", + "M" + ], + "reference": "Liu B. et al. (2014). iDNA-Prot|dis: identifying DNA-binding proteins by incorporating amino acid distance-pairs and reduced alphabet profile into the general pseudo amino acid composition. PLoS ONE, 9(9), e106691.", + "doi": "10.1371/journal.pone.0106691", + "source_note": "Eq. 10 of the paper (image pone.0106691.e027); transcribed 2026-10-12." + }, + "cp20": { + "name": "cp(20) cluster profile (no reduction)", + "values": [ + "A", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "K", + "L", + "M", + "N", + "P", + "Q", + "R", + "S", + "T", + "V", + "W", + "Y" + ], + "reference": "Liu B. et al. (2014). iDNA-Prot|dis: identifying DNA-binding proteins by incorporating amino acid distance-pairs and reduced alphabet profile into the general pseudo amino acid composition. PLoS ONE, 9(9), e106691.", + "doi": "10.1371/journal.pone.0106691", + "source_note": "cp(20) is the conventional 20 amino acid profile (Liu 2014, Section 2); 2026-10-12." + }, + "ctd_ifeature_extra": { + "name": "Six additional CTD property partitions (iFeature 13-property CTD)", + "values": { + "hydrophobicity_ARGP820101": { + "1": "QSTNGDE", + "2": "RAHCKMV", + "3": "LYPFIW" + }, + "hydrophobicity_ZIMJ680101": { + "1": "QNGSWTDERA", + "2": "HMCKV", + "3": "LPFYI" + }, + "hydrophobicity_PONP930101": { + "1": "KPDESNQT", + "2": "GRHA", + "3": "YMFWLCVI" + }, + "hydrophobicity_CASG920101": { + "1": "KDEQPSRNTG", + "2": "AHYMLV", + "3": "FIWC" + }, + "hydrophobicity_ENGD860101": { + "1": "RDKENQHYP", + "2": "SGTAW", + "3": "CVLIMF" + }, + "hydrophobicity_FASG890101": { + "1": "KERSQD", + "2": "NTPG", + "3": "AYHWVMFLIC" + } + }, + "reference": "iFeature: Chen Z. et al. (2018). iFeature: a Python package and web server for features extraction and selection from protein and peptide sequences. Bioinformatics, 34(14), 2499-2502. Following Dubchak I. et al. (1995). PNAS, 92(19), 8700-8704.", + "doi": "10.1093/bioinformatics/bty140", + "source_note": "Three-group partitions for the six AAindex hydrophobicity properties used by iFeature's 13-property CTD (Table/Supplementary of iFeature, cross-checked against the iFeature reference implementation; data only, no code copied). Transcribed 2026-10-12." + } +} \ No newline at end of file diff --git a/protpy/data/residue_tables.json b/protpy/data/residue_tables.json new file mode 100644 index 0000000..50ea04f --- /dev/null +++ b/protpy/data/residue_tables.json @@ -0,0 +1,178 @@ +{ + "codon_counts": { + "name": "Number of sense codons per amino acid (standard genetic code)", + "values": { + "A": 4, + "C": 2, + "D": 2, + "E": 2, + "F": 2, + "G": 4, + "H": 2, + "I": 3, + "K": 2, + "L": 6, + "M": 1, + "N": 2, + "P": 4, + "Q": 2, + "R": 6, + "S": 6, + "T": 4, + "V": 4, + "W": 1, + "Y": 2 + }, + "reference": "Saravanan V. & Gautham N. (2015). Harnessing computational biology for exact linear B-cell epitope prediction: a novel amino acid composition-based feature descriptor. OMICS, 19(10), 648-658.", + "doi": "10.1089/omi.2015.0095", + "source_note": "Standard genetic code (NCBI translation table 1), 61 sense codons, as used for the DDE theoretical mean; 2026-10-12." + }, + "atomic_formulas": { + "name": "Molecular formulas of the free amino acids", + "values": { + "A": { + "C": 3, + "H": 7, + "N": 1, + "O": 2, + "S": 0 + }, + "R": { + "C": 6, + "H": 14, + "N": 4, + "O": 2, + "S": 0 + }, + "N": { + "C": 4, + "H": 8, + "N": 2, + "O": 3, + "S": 0 + }, + "D": { + "C": 4, + "H": 7, + "N": 1, + "O": 4, + "S": 0 + }, + "C": { + "C": 3, + "H": 7, + "N": 1, + "O": 2, + "S": 1 + }, + "Q": { + "C": 5, + "H": 10, + "N": 2, + "O": 3, + "S": 0 + }, + "E": { + "C": 5, + "H": 9, + "N": 1, + "O": 4, + "S": 0 + }, + "G": { + "C": 2, + "H": 5, + "N": 1, + "O": 2, + "S": 0 + }, + "H": { + "C": 6, + "H": 9, + "N": 3, + "O": 2, + "S": 0 + }, + "I": { + "C": 6, + "H": 13, + "N": 1, + "O": 2, + "S": 0 + }, + "L": { + "C": 6, + "H": 13, + "N": 1, + "O": 2, + "S": 0 + }, + "K": { + "C": 6, + "H": 14, + "N": 2, + "O": 2, + "S": 0 + }, + "M": { + "C": 5, + "H": 11, + "N": 1, + "O": 2, + "S": 1 + }, + "F": { + "C": 9, + "H": 11, + "N": 1, + "O": 2, + "S": 0 + }, + "P": { + "C": 5, + "H": 9, + "N": 1, + "O": 2, + "S": 0 + }, + "S": { + "C": 3, + "H": 7, + "N": 1, + "O": 3, + "S": 0 + }, + "T": { + "C": 4, + "H": 9, + "N": 1, + "O": 3, + "S": 0 + }, + "W": { + "C": 11, + "H": 12, + "N": 2, + "O": 2, + "S": 0 + }, + "Y": { + "C": 9, + "H": 11, + "N": 1, + "O": 3, + "S": 0 + }, + "V": { + "C": 5, + "H": 11, + "N": 1, + "O": 2, + "S": 0 + } + }, + "reference": "Pande A. et al. (2023). Computing wide range of protein/peptide features from their sequence and structure. J Comput Biol, 30(2), 204-222.", + "doi": "10.1089/cmb.2022.0241", + "source_note": "Standard free amino-acid formulas; cross-checked against Pfeature's Standalone/Data/atom.csv. Pfeature sums free amino-acid formulas without removing water (protpy's default convention). Its table over-counts H by one for W (C11H13N2O2) and Y (C9H12NO3); the correct formulas C11H12N2O2 and C9H11NO3 are used here, so W/Y-containing sequences differ slightly from Pfeature output. Transcribed 2026-10-12." + } +} \ No newline at end of file diff --git a/protpy/utils.py b/protpy/utils.py index b7585de..05ae1bd 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -26,6 +26,8 @@ "output_shape": "1 x 20", "speed": "Fast", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "dipeptide_composition": { "description": ( @@ -41,6 +43,8 @@ "output_shape": "1 x 400", "speed": "Fast", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "tripeptide_composition": { "description": ( @@ -55,6 +59,8 @@ "output_shape": "1 x 8000", "speed": "Moderate", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "gravy": { "description": ( @@ -70,6 +76,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[8] Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132.", + "doi": "10.1016/0022-2836(82)90515-0", + "in_default_set": True, }, "aromaticity": { "description": ( @@ -84,6 +92,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[9] Lobry & Gautier (1994). Nucleic Acids Research, 22(15), 3174-3180.", + "doi": "10.1093/nar/22.15.3174", + "in_default_set": True, }, "instability_index": { "description": ( @@ -99,6 +109,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[10] Guruprasad et al. (1990). Protein Engineering, 4(2), 155-161.", + "doi": "10.1093/protein/4.2.155", + "in_default_set": True, }, "isoelectric_point": { "description": ( @@ -114,6 +126,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[11] Bjellqvist et al. (1993). Electrophoresis, 14(1), 1023-1031.", + "doi": "10.1002/elps.11501401163", + "in_default_set": True, }, "molecular_weight": { "description": ( @@ -129,6 +143,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[12] Gasteiger et al. (2005). The Proteomics Protocols Handbook.", + "doi": "10.1385/1-59259-890-0:571", + "in_default_set": True, }, "charge_distribution": { "description": ( @@ -145,6 +161,8 @@ "output_shape": "1 x 3", "speed": "Fast", "reference": "[13] Cameselle-Teijeiro (1979). Biochemical Education, 7(3), 69-70.", + "doi": None, + "in_default_set": True, }, "hydrophobic_polar_charged_composition": { "description": ( @@ -161,6 +179,8 @@ "output_shape": "1 x 3", "speed": "Fast", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "secondary_structure_propensity": { "description": ( @@ -176,6 +196,8 @@ "output_shape": "1 x 3", "speed": "Fast", "reference": "[14] Chou & Fasman (1974). Biochemistry, 13(2), 211-222.", + "doi": "10.1021/bi00699a001", + "in_default_set": True, }, "kmer_composition": { "description": ( @@ -192,6 +214,8 @@ "output_shape": "1 x 20^k (e.g. 1 x 400 for k=2)", "speed": "Fast (k≤3) / Slow (k=4)", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "reduced_alphabet_composition": { "description": ( @@ -208,6 +232,8 @@ "output_shape": "1 x alphabet_size", "speed": "Fast", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "motif_composition": { "description": ( @@ -225,6 +251,8 @@ "output_shape": "1 x M (M = number of motifs, default M=8)", "speed": "Fast", "reference": "[15] PROSITE: https://prosite.expasy.org/", + "doi": "10.1093/nar/gks1067", + "in_default_set": True, }, "amino_acid_pair_composition": { "description": ( @@ -240,6 +268,8 @@ "output_shape": "1 x 400", "speed": "Moderate", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "aliphatic_index": { "description": ( @@ -256,6 +286,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[16] Ikai (1980). J. Biochemistry, 88(6), 1895-1898.", + "doi": "10.1093/oxfordjournals.jbchem.a133168", + "in_default_set": True, }, "extinction_coefficient": { "description": ( @@ -271,6 +303,8 @@ "output_shape": "1 x 2 [ExtCoeff_Reduced, ExtCoeff_Oxidized]", "speed": "Fast", "reference": "[17] Gasteiger et al. (2005). The Proteomics Protocols Handbook.", + "doi": "10.1385/1-59259-890-0:571", + "in_default_set": True, }, "boman_index": { "description": ( @@ -287,6 +321,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[18] Boman (2003). J. Internal Medicine, 254(3), 197-215.", + "doi": "10.1046/j.1365-2796.2003.01228.x", + "in_default_set": True, }, "aggregation_propensity": { "description": ( @@ -306,6 +342,8 @@ "output_shape": "1 x 2 [AggregProneRegions, AggregProneFraction]", "speed": "Fast", "reference": "[8] Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132.", + "doi": "10.1016/0022-2836(82)90515-0", + "in_default_set": True, }, "hydrophobic_moment": { "description": ( @@ -324,6 +362,8 @@ "output_shape": "1 x 2 [HydrophobicMoment_Mean, HydrophobicMoment_Max]", "speed": "Fast", "reference": "[19] Eisenberg et al. (1982). Nature, 299, 371-374.", + "doi": "10.1038/299371a0", + "in_default_set": True, }, "shannon_entropy": { "description": ( @@ -339,6 +379,8 @@ "output_shape": "1 x 1", "speed": "Fast", "reference": "[1] Gromiha, M. M. (2010). Protein Bioinformatics. Elsevier.", + "doi": None, + "in_default_set": True, }, "pseudo_amino_acid_composition": { "description": ( @@ -358,6 +400,8 @@ "output_shape": "1 x (20 + lamda) (default 1 x 50)", "speed": "Moderate", "reference": "[4] Chou, K-C. (2001). PROTEINS, 43, 246-255.", + "doi": "10.1002/prot.1035", + "in_default_set": True, }, "amphiphilic_pseudo_amino_acid_composition": { "description": ( @@ -377,6 +421,8 @@ "output_shape": "1 x (20 + 2·lamda) (default 1 x 80)", "speed": "Moderate", "reference": "[5] Chou, K-C. (2005). Bioinformatics, 21(1), 10-19.", + "doi": "10.1093/bioinformatics/bth466", + "in_default_set": True, }, # ── Autocorrelation ──────────────────────────────────────────────────────── "moreaubroto_autocorrelation": { @@ -397,6 +443,8 @@ "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", "reference": "[1] Hollas (2003). J. Math. Chem., 33(2), 91-101.", + "doi": "10.1023/A:1023247831238", + "in_default_set": True, }, "moran_autocorrelation": { "description": ( @@ -416,6 +464,8 @@ "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", "reference": "[2] Ong et al. (2007). BMC Bioinformatics, 8, 300.", + "doi": "10.1186/1471-2105-8-300", + "in_default_set": True, }, "geary_autocorrelation": { "description": ( @@ -435,6 +485,8 @@ "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", "reference": "[2] Ong et al. (2007). BMC Bioinformatics, 8, 300.", + "doi": "10.1186/1471-2105-8-300", + "in_default_set": True, }, # ── Conjoint Triad ───────────────────────────────────────────────────────── "conjoint_triad": { @@ -452,6 +504,8 @@ "output_shape": "1 x 343", "speed": "Fast", "reference": "[1] Shen et al. (2007). PNAS, 104(11), 4337-4341.", + "doi": "10.1073/pnas.0607879104", + "in_default_set": True, }, # ── CTD ──────────────────────────────────────────────────────────────────── "ctd_composition": { @@ -469,6 +523,8 @@ "output_shape": "1 x 3 (per property)", "speed": "Fast", "reference": "[1] Dubchak et al. (1995). PNAS, 92, 8700-8704.", + "doi": "10.1073/pnas.92.19.8700", + "in_default_set": True, }, "ctd_transition": { "description": ( @@ -485,6 +541,8 @@ "output_shape": "1 x 3 (per property)", "speed": "Fast", "reference": "[1] Dubchak et al. (1995). PNAS, 92, 8700-8704.", + "doi": "10.1073/pnas.92.19.8700", + "in_default_set": True, }, "ctd_distribution": { "description": ( @@ -502,6 +560,8 @@ "output_shape": "1 x 15 (per property)", "speed": "Fast", "reference": "[1] Dubchak et al. (1995). PNAS, 92, 8700-8704.", + "doi": "10.1073/pnas.92.19.8700", + "in_default_set": True, }, "ctd_": { "description": ( @@ -520,6 +580,8 @@ "output_shape": "1 x 21 (single property) / 1 x 147 (all 7 properties)", "speed": "Fast (single) / Moderate (all)", "reference": "[1] Dubchak et al. (1995). PNAS, 92, 8700-8704.", + "doi": "10.1073/pnas.92.19.8700", + "in_default_set": True, }, # ── Sequence Order ───────────────────────────────────────────────────────── "sequence_order_coupling_number_": { @@ -539,6 +601,8 @@ "output_shape": "scalar float", "speed": "Fast", "reference": "[1] Chou, K-C. (2000). Biochem. Biophys. Res. Commun., 278, 477-483.", + "doi": "10.1006/bbrc.2000.3815", + "in_default_set": True, }, "sequence_order_coupling_number": { "description": ( @@ -556,6 +620,8 @@ "output_shape": "1 x lag (default 1 x 30)", "speed": "Moderate", "reference": "[1] Chou, K-C. (2000). Biochem. Biophys. Res. Commun., 278, 477-483.", + "doi": "10.1006/bbrc.2000.3815", + "in_default_set": True, }, "sequence_order_coupling_number_all": { "description": ( @@ -572,6 +638,8 @@ "output_shape": "1 x (lag × 2) (default 1 x 60)", "speed": "Moderate", "reference": "[1] Chou, K-C. (2000). Biochem. Biophys. Res. Commun., 278, 477-483.", + "doi": "10.1006/bbrc.2000.3815", + "in_default_set": True, }, "quasi_sequence_order": { "description": ( @@ -591,6 +659,8 @@ "output_shape": "1 x (lag + 20) (default 1 x 50)", "speed": "Moderate", "reference": "[1] Chou, K-C. (2000). Biochem. Biophys. Res. Commun., 278, 477-483.", + "doi": "10.1006/bbrc.2000.3815", + "in_default_set": True, }, "quasi_sequence_order_all": { "description": ( @@ -607,6 +677,8 @@ "output_shape": "1 x ((lag + 20) × 2) (default 1 x 100)", "speed": "Moderate", "reference": "[1] Chou, K-C. (2000). Biochem. Biophys. Res. Commun., 278, 477-483.", + "doi": "10.1006/bbrc.2000.3815", + "in_default_set": True, }, } @@ -645,6 +717,8 @@ def get_descriptor_info(name: str) -> dict: - ``output_shape`` : string describing output DataFrame shape - ``speed`` : speed rating (Fast / Moderate / Slow) - ``reference`` : primary citation + - ``doi`` : DOI of the primary citation (None for textbooks / sources without one) + - ``in_default_set`` : whether the descriptor is part of the classic ``all_descriptors`` set Raises ====== @@ -683,7 +757,8 @@ def list_descriptors(category: str | None = None) -> list[str]: :category: str | None (default=None) If provided, only descriptors belonging to this category are returned. Available categories: "Composition", "Autocorrelation", "Conjoint Triad", - "CTD", "Sequence Order". Case-insensitive. + "CTD", "Sequence Order", "Scales", "Physicochemical", "Complexity", + "Signal", "Structural". Case-insensitive. Returns ======= @@ -737,4 +812,6 @@ def print_descriptor_info(name: str) -> None: for param, desc in info["parameters"].items(): print(f" - {param}: {desc}") print(f"\n Reference: {info['reference']}") + if info.get("doi"): + print(f" DOI : https://doi.org/{info['doi']}") print(f"{'='*60}\n") diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..4bca79b --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,110 @@ +########################################################################### +############### protPy - Internal Helpers Tests ############### +########################################################################### + +import os +import re +import unittest + +import numpy as np + +import protpy +from protpy import _helpers as h +from protpy.utils import _DESCRIPTOR_REGISTRY + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +class ProtpyHelpersTests(unittest.TestCase): + + def test_load_json_table(self): + t = h._load_json_table("reduced_alphabets.json", "ifeature5") + self.assertEqual(set(t), {"name", "values", "reference", "doi", "source_note"}) + t["values"]["aromatic"] = "X" #a copy: the cache must be unaffected + self.assertEqual(h._load_json_table("reduced_alphabets.json", "ifeature5")["values"]["aromatic"], "FYW") + with self.assertRaisesRegex(KeyError, "Valid tables"): + h._load_json_table("reduced_alphabets.json", "nope") + + def test_every_data_table_has_provenance(self): + data = os.path.join(ROOT, "protpy", "data") + for fname in ("aa_scales.json", "reduced_alphabets.json", "residue_tables.json", "structural_params.json"): + if not os.path.exists(os.path.join(data, fname)): + continue + for key, table in h._read_json(fname).items(): + with self.subTest(table=f"{fname}:{key}"): + self.assertTrue(table["reference"]) + self.assertTrue(table["source_note"]) + self.assertIn("doi", table) + + def test_property_matrix(self): + m = h._property_matrix("ACA", "KYTJ820101") + np.testing.assert_allclose(m[:, 0], [1.8, 2.5, 1.8]) + m = h._property_matrix("AC", ["KYTJ820101", "HOPT810101"]) + self.assertEqual(m.shape, (2, 2)) + m = h._property_matrix("AAA", {aa: [1.0, float(i)] for i, aa in enumerate(protpy.amino_acids)}, standardize=True) + np.testing.assert_allclose(m[:, 0], 0) #constant column -> 0, no division by zero + self.assertAlmostEqual(m[0, 1], (0 - 9.5) / np.std(np.arange(20))) + with self.assertRaises(ValueError): + h._property_matrix("A", "NOT_A_SCALE") + with self.assertRaises(ValueError): + h._property_matrix("A", {"A": 1.0}) + + def test_sliding_windows(self): + np.testing.assert_allclose(h._sliding_windows([1, 2, 3, 4, 5], 3), [2, 3, 4]) + #window longer than profile -> one window + np.testing.assert_allclose(h._sliding_windows([1, 2, 3], 9), [2]) + #edge weighting: weights 0.5,1,0.5 for window 3 + np.testing.assert_allclose(h._sliding_windows([0, 4, 0], 3, edge=0.5), [2.0]) + #window 5, edge 0.2 -> weights 0.2,0.6,1,0.6,0.2 + w = np.array([0.2, 0.6, 1, 0.6, 0.2]) + x = np.array([1., 2, 3, 4, 7]) + np.testing.assert_allclose(h._sliding_windows(x, 5, edge=0.2), [(w * x).sum() / w.sum()]) + + def test_profile_stats(self): + out = h._profile_stats(np.array([1., 2, 3]), "P", threshold=1.5) + self.assertEqual(out, {"P_mean": 2.0, "P_max": 3.0, "P_min": 1.0, "P_std": np.std([1, 2, 3]), + "P_frac_above": 2 / 3}) + + def test_counts(self): + self.assertEqual(h._gapped_pair_counts("AAAA", 1)[("A", "A")], 2) + self.assertEqual(h._gapped_pair_counts("ACDE", 0, {"A": "x", "C": "x", "D": "y", "E": "y"}), + {("x", "x"): 1, ("x", "y"): 1, ("y", "y"): 1}) + self.assertEqual(h._kmer_counts("AAAA", 3)[("A", "A", "A")], 2) + self.assertEqual(sum(h._kmer_counts("A", 2).values()), 0) + + def test_charge_and_runs(self): + np.testing.assert_array_equal(h._charge_vector("KRDEHA"), [1, 1, -1, -1, 0, 0]) + np.testing.assert_array_equal(h._charge_vector("H", his=True), [1]) + self.assertEqual(h._runs([1, 1, 0, 1, 0, 1, 1, 1]), [(0, 2), (3, 4), (5, 8)]) + self.assertEqual(h._runs([1, 1, 0, 1, 0, 1, 1, 1], min_len=2), [(0, 2), (5, 8)]) + self.assertEqual(h._runs([0, 0]), []) + + def test_to_df(self): + df = h._to_df([1.23456, np.float64(2.0004), np.int64(3), True], ["a", "b", "c", "d"]) + self.assertEqual(df.shape, (1, 4)) + self.assertEqual(df.iloc[0].tolist(), [1.235, 2.0, 3, 1]) + + +class ProtpyRegistryTests(unittest.TestCase): + + def test_every_descriptor_registered_with_reference(self): + for name in protpy.descriptor_names: + with self.subTest(name=name): + self.assertIn(name, _DESCRIPTOR_REGISTRY) + meta = _DESCRIPTOR_REGISTRY[name] + self.assertTrue(meta["reference"]) + self.assertIn("doi", meta) + self.assertIn("in_default_set", meta) + + def test_registry_dois_in_bib(self): + with open(os.path.join(ROOT, "docs", "references.bib")) as f: + bib = {d.lower() for d in re.findall(r"doi\s*=\s*\{([^}]*)\}", f.read(), flags=re.I)} + for name, meta in _DESCRIPTOR_REGISTRY.items(): + if meta["doi"] is None: + continue #textbooks / sources without a DOI + with self.subTest(name=name): + self.assertTrue(meta["doi"].lower() in bib, meta["doi"]) + + +if __name__ == "__main__": + unittest.main() From 5ccc115cbe99a5fc7e397f9ad93b68e224ca8689 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:11:36 +0100 Subject: [PATCH 04/17] v1.5.0 phase 1: CKSAAP, CKSAAGP, DDE, ASDC, distance pair, natural vector, atomic composition; GAAC/GDPC/GTPC, KSCTriad, 13-property CTD, split AAC/DPC --- protpy/__init__.py | 4 +- protpy/composition.py | 458 +++++++++++++++++++++++++++-- protpy/conjoint_triad.py | 36 ++- protpy/ctd.py | 53 +++- protpy/utils.py | 118 ++++++++ tests/test_composition_extended.py | 183 ++++++++++++ tests/test_protpy.py | 9 +- 7 files changed, 804 insertions(+), 57 deletions(-) create mode 100644 tests/test_composition_extended.py diff --git a/protpy/__init__.py b/protpy/__init__.py index a91feca..5cd8931 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -39,5 +39,7 @@ "conjoint_triad", "ctd_composition", "ctd_transition", "ctd_distribution", "ctd_", "sequence_order_coupling_number_", "sequence_order_coupling_number", "sequence_order_coupling_number_all", "quasi_sequence_order", "quasi_sequence_order_all", - "shannon_entropy" + "shannon_entropy", + #v1.5.0 + "cksaap", "cksaagp", "dde", "asdc", "distance_pair", "natural_vector", "atomic_composition", ] \ No newline at end of file diff --git a/protpy/composition.py b/protpy/composition.py index 678c888..ac1ac12 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -7,6 +7,7 @@ import re import itertools from collections import Counter +import numpy as np import pandas as pd import math from aaindex import aaindex1 @@ -62,9 +63,42 @@ class prediction: new methods of statistical classification. Proc Int Conf [19] Eisenberg, D., Weiss, R. M., & Terwilliger, T. C. (1982). The helical hydrophobic moment: a measure of the amphiphilicity of a helix. Nature, 299, 371-374. https://doi.org/10.1038/299371a0 +[20] Chen, K., Kurgan, L. A., & Ruan, J. (2007). Prediction of flexible/rigid + regions from protein sequences using k-spaced amino acid pairs. BMC + Structural Biology, 7, 25. https://doi.org/10.1186/1472-6807-7-25 +[21] Chen, Z. et al. (2018). iFeature: a Python package and web server for + features extraction and selection from protein and peptide sequences. + Bioinformatics, 34(14), 2499-2502. https://doi.org/10.1093/bioinformatics/bty140 +[22] Saravanan, V., & Gautham, N. (2015). Harnessing computational biology for + exact linear B-cell epitope prediction: a novel amino acid composition-based + feature descriptor. OMICS, 19(10), 648-658. https://doi.org/10.1089/omi.2015.0095 +[23] Wei, L. et al. (2018). ACPred-FL: a sequence-based predictor using effective + feature representation to improve the prediction of anti-cancer peptides. + Bioinformatics, 34(23), 4007-4016. https://doi.org/10.1093/bioinformatics/bty451 +[24] Liu, B. et al. (2014). iDNA-Prot|dis: identifying DNA-binding proteins by + incorporating amino acid distance-pairs and reduced alphabet profile into + the general pseudo amino acid composition. PLoS ONE, 9(9), e106691. + https://doi.org/10.1371/journal.pone.0106691 +[25] Cannata, N. et al. (2002). Simplifying amino acid alphabets by means of a + branch and bound algorithm and substitution matrices. Bioinformatics, 18(8), + 1102-1108. https://doi.org/10.1093/bioinformatics/18.8.1102 +[26] Yu, C. et al. (2013). Protein space: a natural method for realizing the + nature of protein universe. Journal of Theoretical Biology, 318, 197-204. + https://doi.org/10.1016/j.jtbi.2012.11.005 +[27] Pande, A. et al. (2023). Computing wide range of protein/peptide features + from their sequence and structure. Journal of Computational Biology, 30(2), + 204-222. https://doi.org/10.1089/cmb.2022.0241 +[28] Kumar, M., Verma, R., & Raghava, G. P. S. (2006). Prediction of + mitochondrial proteins using support vector machine and hidden Markov + model. Journal of Biological Chemistry, 281(9), 5357-5363. + https://doi.org/10.1074/jbc.M511061200 +[29] Lata, S., Sharma, B. K., & Raghava, G. P. S. (2007). Analysis and + prediction of antibacterial peptides. BMC Bioinformatics, 8, 263. + https://doi.org/10.1186/1471-2105-8-263 """ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight +from ._helpers import _load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df #physicochemical values for all amino acids for hydrophobicity, hydrophilicty and residue mass #values taken from http://www.csbio.sjtu.edu.cn/bioinf/PseAAC/ParaValue.htm @@ -92,7 +126,7 @@ class prediction: new methods of statistical classification. Proc Int Conf ################################# Amino Acid Composition ################################## -def amino_acid_composition(sequence: str) -> pd.DataFrame: +def amino_acid_composition(sequence: str, region: str | None = None, n: int = 25) -> pd.DataFrame: """ Calculate Amino Acid Composition (AAComp) of protein sequence. AAComp describes the fraction of each amino acid type within a protein sequence, @@ -108,6 +142,14 @@ def amino_acid_composition(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :region: str | None (default=None) + restrict the calculation to part of the sequence: None (whole sequence), + "nterm" (first ``n`` residues, columns prefixed ``NT_``), "cterm" (last + ``n`` residues, ``CT_``) or "split" (N-terminal, middle and C-terminal + blocks concatenated, prefixed ``NT_``, ``MID_``, ``CT_``; split + composition [28, 29]). + :n: int (default=25) + terminal block length used by ``region``. Returns ======= @@ -119,22 +161,24 @@ def amino_acid_composition(sequence: str) -> pd.DataFrame: """ #validate input protein sequence sequence = _validate_sequence(sequence) + if region is not None: + return _regional(amino_acid_composition, sequence, region, n) - amino_acid_composition = {} + aa_composition = {} #iterate through each amino acid, calculating each composition for aa in amino_acids: - amino_acid_composition[aa] = round(float(sequence.count(aa)) / len(sequence) * 100, 3) + aa_composition[aa] = round(float(sequence.count(aa)) / len(sequence) * 100, 3) #transform values and columns to DataFrame - amino_acid_composition_df = pd.DataFrame([list(amino_acid_composition.values())], - columns=list(amino_acid_composition.keys())) + amino_acid_composition_df = pd.DataFrame([list(aa_composition.values())], + columns=list(aa_composition.keys())) return amino_acid_composition_df ################################## Dipeptide Composition ################################## -def dipeptide_composition(sequence: str) -> pd.DataFrame: +def dipeptide_composition(sequence: str, region: str | None = None, n: int = 25) -> pd.DataFrame: """ Calculate Dipeptide Composition (DPComp) for protein sequence. Dipeptide composition is the fraction of each dipeptide type within a @@ -153,6 +197,14 @@ def dipeptide_composition(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :region: str | None (default=None) + restrict the calculation to part of the sequence: None (whole sequence), + "nterm" (first ``n`` residues, columns prefixed ``NT_``), "cterm" (last + ``n`` residues, ``CT_``) or "split" (N-terminal, middle and C-terminal + blocks concatenated, prefixed ``NT_``, ``MID_``, ``CT_``; split + composition [28, 29]). + :n: int (default=25) + terminal block length used by ``region``. Returns ======= @@ -164,8 +216,10 @@ def dipeptide_composition(sequence: str) -> pd.DataFrame: """ #validate input protein sequence sequence = _validate_sequence(sequence) + if region is not None: + return _regional(dipeptide_composition, sequence, region, n) - dipeptide_composition = {} + dipep_composition = {} #count overlapping dipeptides with a sliding window (str.count misses overlaps, e.g. "AA" in "AAA") total_dipeptides = len(sequence) - 1 @@ -175,15 +229,31 @@ def dipeptide_composition(sequence: str) -> pd.DataFrame: for i in amino_acids: for j in amino_acids: dipep = i + j - dipeptide_composition[dipep] = round( + dipep_composition[dipep] = round( counts[dipep] / total_dipeptides * 100, 2) if total_dipeptides > 0 else 0.0 #transform values and columns to DataFrame - dipeptide_composition_df = pd.DataFrame([list(dipeptide_composition.values())], - columns=list(dipeptide_composition.keys())) + dipeptide_composition_df = pd.DataFrame([list(dipep_composition.values())], + columns=list(dipep_composition.keys())) return dipeptide_composition_df +def _regional(func, sequence: str, region: str, n: int) -> pd.DataFrame: + """Apply ``func`` to the N-terminal, C-terminal or split (N/middle/C) regions of ``sequence``.""" + n = _validate_int("n", n, 1) + parts = {"nterm": [("NT_", sequence[:n])], "cterm": [("CT_", sequence[-n:])], + "split": [("NT_", sequence[:n]), ("MID_", sequence[n:-n]), ("CT_", sequence[-n:])]} + if region not in parts: + raise ValueError(f"Invalid region '{region}'. Valid regions are: None, {list(parts)}.") + #the middle block of a split must be non-empty + if region == "split" and len(sequence) <= 2 * n: + raise ValueError(f"region='split' needs a sequence longer than 2n ({2 * n}), got length {len(sequence)}.") + frames = [] + for prefix, part in parts[region]: + df = func(part) + frames.append(df.add_prefix(prefix)) + return pd.concat(frames, axis=1) + ################################## Tripeptide Composition ################################# def tripeptide_composition(sequence: str) -> pd.DataFrame: @@ -706,47 +776,87 @@ def kmer_composition(sequence: str, k: int = 2) -> pd.DataFrame: ] } -def reduced_alphabet_composition(sequence: str, alphabet_size: int = 6) -> pd.DataFrame: +def reduced_alphabet_composition(sequence: str, alphabet_size: int = 6, alphabet: str | None = None, + k: int = 1) -> pd.DataFrame: """ Calculate Reduced Alphabet Composition of a protein sequence. The 20 standard amino acids are clustered into a smaller alphabet of physicochemically similar - groups. The fraction of residues belonging to each group is returned [1]. + groups. The percentage of residues belonging to each group is returned [1]. Supported alphabet sizes: 2, 3, 4, 6 (default=6). Invalid sizes are reset to 6. + A named alphabet can be used instead with ``alphabet``. ``alphabet="ifeature5"`` + (aliphatic GAVLMI, aromatic FYW, positive KRH, negative DE, uncharged STCPNQ) + gives grouped amino acid composition (GAAC, k=1, 1 x 5), grouped dipeptide + composition (GDPC, k=2, 1 x 25) and grouped tripeptide composition (GTPC, + k=3, 1 x 125) [21]. The cluster profiles "cp13", "cp14", "cp19" and "cp20" [24] + are also accepted. With k > 1 overlapping k-mers of groups are counted and + divided by (L - k + 1): + + GkPC(g1..gk) = N(g1..gk) / (L - k + 1) x 100 + Parameters ========== :sequence: str protein sequence. :alphabet_size: int (default=6) - number of amino acid groups (2, 3, 4 or 6). + number of amino acid groups (2, 3, 4 or 6); ignored when ``alphabet`` is set. + :alphabet: str | None (default=None) + named alphabet: "ifeature5", "cp13", "cp14", "cp19" or "cp20". + :k: int (default=1) + group k-mer length, 1-3. Returns ======= :reduced_alphabet_df: pd.DataFrame - pandas DataFrame of shape 1 x alphabet_size. + pandas DataFrame of shape 1 x G^k, where G is the number of groups. Values + are percentages. """ #validate input protein sequence sequence = _validate_sequence(sequence) + k = _validate_int("k", k, 1) + if k > 3: + raise ValueError(f"k must be 1, 2 or 3, got {k}.") + + if alphabet is None: + #default to 6 if an unsupported size is requested + if alphabet_size not in reduced_alphabets_: + alphabet_size = 6 + groups = [(str(idx + 1), group) for idx, group in enumerate(reduced_alphabets_[alphabet_size])] + prefix = "ReducedAlphabet" + else: + groups = _named_alphabet(alphabet) + prefix = {1: "GAAC", 2: "GDPC", 3: "GTPC"}[k] if alphabet == "ifeature5" else f"RA_{alphabet}" - #default to 6 if an unsupported size is requested - if alphabet_size not in reduced_alphabets_: - alphabet_size = 6 - - groups = reduced_alphabets_[alphabet_size] n = len(sequence) - values = [] - col_names = [] - #compute fractional composition for each group - for idx, group in enumerate(groups): - freq = round(sum(1 for aa in sequence if aa in group) / n * 100, 3) - values.append(freq) - col_names.append(f"ReducedAlphabet_{idx + 1}") - - reduced_alphabet_df = pd.DataFrame([values], columns=col_names) - - return reduced_alphabet_df + if k == 1 and alphabet is None: + values = [] + col_names = [] + #compute percentage composition for each group + for label, group in groups: + values.append(round(sum(1 for aa in sequence if aa in group) / n * 100, 3)) + col_names.append(f"{prefix}_{label}") + return pd.DataFrame([values], columns=col_names) + + mapping = {aa: label for label, group in groups for aa in group} + counts = _kmer_counts(sequence, k, mapping) + total = n - k + 1 + labels = [label for label, _ in groups] + combos = list(itertools.product(labels, repeat=k)) + values = [counts[c] / total * 100 if total > 0 else 0.0 for c in combos] + return _to_df(values, [f"{prefix}_" + ".".join(c) for c in combos]) + + +def _named_alphabet(alphabet: str) -> list[tuple[str, str]]: + """Return [(label, residues), ...] for a named partition alphabet in reduced_alphabets.json.""" + valid = ["ifeature5", "cp13", "cp14", "cp19", "cp20"] + if alphabet not in valid: + raise ValueError(f"Invalid alphabet '{alphabet}'. Valid alphabets are: {valid}.") + values = _load_json_table("reduced_alphabets.json", alphabet)["values"] + if isinstance(values, dict): + return list(values.items()) + return [(group, group) for group in values] ############################## Motif Composition ########################################## @@ -1501,4 +1611,290 @@ def amphiphilic_correllation_function(Ri: str = "S", Rj: str = "D", if normalized_props is None: normalized_props = [normalize_property(hydrophobicity_), normalize_property(hydrophilicity_)] - return [round(prop[Ri] * prop[Rj], 3) for prop in normalized_props] \ No newline at end of file + return [round(prop[Ri] * prop[Rj], 3) for prop in normalized_props] +####################### Composition of k-Spaced Amino Acid Pairs ###################### + +def cksaap(sequence: str, gap: int = 5) -> pd.DataFrame: + """ + Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein + sequence [20]. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) + is counted and divided by the number of such pairs: + + CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 + + g = 0 is the dipeptide composition. Values are percentages; a spacing with no + pairs (L - g - 1 <= 0) gives zeros. + + Parameters + ========== + :sequence: str + protein sequence. + :gap: int (default=5) + maximum number of residues between the two residues of a pair. + + Returns + ======= + :cksaap_df: pd.DataFrame + pandas DataFrame of shape 1 x 400·(gap+1), columns ``CKSAAP_{XY}_gap{g}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + gap = _validate_int("gap", gap) + + pairs = list(itertools.product(amino_acids, repeat=2)) + values, columns = [], [] + for g in range(gap + 1): + counts = _gapped_pair_counts(sequence, g) + total = len(sequence) - g - 1 + values += [counts[p] / total * 100 if total > 0 else 0.0 for p in pairs] + columns += [f"CKSAAP_{x}{y}_gap{g}" for x, y in pairs] + return _to_df(values, columns) + +################# Composition of k-Spaced Amino Acid Group Pairs ####################### + +def cksaagp(sequence: str, gap: int = 5) -> pd.DataFrame: + """ + Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP) [21]. + As CKSAAP, but residues are first mapped to the five iFeature groups: + aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and + uncharged (STCPNQ). + + CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 + + Values are percentages and the 25 values for each spacing sum to 100 + (0 when the spacing has no pairs). + + Parameters + ========== + :sequence: str + protein sequence. + :gap: int (default=5) + maximum number of residues between the two residues of a pair. + + Returns + ======= + :cksaagp_df: pd.DataFrame + pandas DataFrame of shape 1 x 25·(gap+1), columns ``CKSAAGP_{G1}.{G2}_gap{g}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + gap = _validate_int("gap", gap) + + groups = _named_alphabet("ifeature5") + mapping = {aa: label for label, residues in groups for aa in residues} + pairs = list(itertools.product([label for label, _ in groups], repeat=2)) + values, columns = [], [] + for g in range(gap + 1): + counts = _gapped_pair_counts(sequence, g, mapping) + total = len(sequence) - g - 1 + values += [counts[p] / total * 100 if total > 0 else 0.0 for p in pairs] + columns += [f"CKSAAGP_{a}.{b}_gap{g}" for a, b in pairs] + return _to_df(values, columns) + +############################ Dipeptide Deviation from Expected Mean ###################### + +def dde(sequence: str) -> pd.DataFrame: + """ + Calculate the Dipeptide Deviation from Expected Mean (DDE) [22]. The observed + dipeptide composition is standardised against the composition expected from + the number of codons coding each residue: + + Dc(XY) = N(XY) / (L - 1) + Tm(XY) = (C_X / 61) x (C_Y / 61) + Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) + DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) + + where C_X is the number of sense codons for residue X in the standard genetic + code (61 in total). DDE is a standardised statistic, not a percentage. + + Parameters + ========== + :sequence: str + protein sequence, at least 2 residues. + + Returns + ======= + :dde_df: pd.DataFrame + pandas DataFrame of shape 1 x 400, columns ``DDE_{XY}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + if len(sequence) < 2: + raise ValueError("DDE needs a sequence of at least 2 residues.") + + codons = _load_json_table("residue_tables.json", "codon_counts")["values"] + counts = _gapped_pair_counts(sequence, 0) + total = len(sequence) - 1 + values, columns = [], [] + for x, y in itertools.product(amino_acids, repeat=2): + tm = (codons[x] / 61) * (codons[y] / 61) + tv = tm * (1 - tm) / total + values.append((counts[(x, y)] / total - tm) / math.sqrt(tv)) + columns.append(f"DDE_{x}{y}") + return _to_df(values, columns) + +################################ Adaptive Skip Dipeptide Composition ##################### + +def asdc(sequence: str) -> pd.DataFrame: + """ + Calculate the Adaptive Skip Dipeptide Composition (ASDC) [23]: the composition + of every ordered residue pair (s_i, s_j) with i < j, over all gaps: + + ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 + + The denominator is the total number of ordered pairs, L(L-1)/2. Values are + percentages and sum to 100 (zeros for a single residue). + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :asdc_df: pd.DataFrame + pandas DataFrame of shape 1 x 400, columns ``ASDC_{XY}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + index = {aa: i for i, aa in enumerate(amino_acids)} + #integer matmul: exact counts, and avoids spurious BLAS float warnings + onehot = np.zeros((len(sequence), 20), dtype=np.int64) + onehot[np.arange(len(sequence)), [index[aa] for aa in sequence]] = 1 + #rows of `before` count each residue type strictly before position j + before = np.cumsum(onehot, axis=0) - onehot + pair_counts = before.T @ onehot + total = len(sequence) * (len(sequence) - 1) / 2 + values = (pair_counts / total * 100).ravel() if total else np.zeros(400) + return _to_df(list(values), [f"ASDC_{x}{y}" for x, y in itertools.product(amino_acids, repeat=2)]) + +#################################### Distance Pair ####################################### + +def distance_pair(sequence: str, rng: int = 3, alphabet: str = "cp14") -> pd.DataFrame: + """ + Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet + (iDNA-Prot|dis) [24, 25]. Residues are mapped to the clusters of a cluster + profile cp(n) and the vector is + + [f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] + + where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the + composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the + nearest-neighbour pair. This follows Eqs. 5-12 of [24], giving G + G^2·rng + values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. + + Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of [24]. The + printed cp(14) lists E twice; the stray E in the first cluster is dropped, + the only reading with 14 clusters. + + Parameters + ========== + :sequence: str + protein sequence. + :rng: int (default=3) + maximum pair distance d. + :alphabet: str (default="cp14") + cluster profile: "cp13", "cp14", "cp19" or "cp20". + + Returns + ======= + :distance_pair_df: pd.DataFrame + pandas DataFrame of shape 1 x (G + G^2·rng), columns ``DP_{c}`` and + ``DP_{c1}.{c2}_d{d}`` where c are cluster labels (their residues). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + rng = _validate_int("rng", rng) + if alphabet not in ("cp13", "cp14", "cp19", "cp20"): + raise ValueError(f"Invalid alphabet '{alphabet}'. Valid alphabets are: ['cp13', 'cp14', 'cp19', 'cp20'].") + + labels = [label for label, _ in _named_alphabet(alphabet)] + mapping = {aa: label for label in labels for aa in label} + comp = Counter(mapping[aa] for aa in sequence) + values = [comp[c] / len(sequence) * 100 for c in labels] + columns = [f"DP_{c}" for c in labels] + pairs = list(itertools.product(labels, repeat=2)) + for d in range(1, rng + 1): + counts = _gapped_pair_counts(sequence, d - 1, mapping) + total = len(sequence) - d + values += [counts[p] / total * 100 if total > 0 else 0.0 for p in pairs] + columns += [f"DP_{a}.{b}_d{d}" for a, b in pairs] + return _to_df(values, columns) + +#################################### Natural Vector ###################################### + +def natural_vector(sequence: str) -> pd.DataFrame: + """ + Calculate the Natural Vector of a protein sequence [26]. For each amino acid k: + + n_k = number of occurrences of k + mu_k = mean (1-based) position of k + D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) + + mu_k and D2_k are 0 when k does not occur. Values are not percentages. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :natural_vector_df: pd.DataFrame + pandas DataFrame of shape 1 x 60, columns ``NV_n_{X}``, ``NV_mu_{X}`` and + ``NV_D2_{X}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + L = len(sequence) + positions = {aa: [] for aa in amino_acids} + for i, aa in enumerate(sequence, start=1): + positions[aa].append(i) + n = [len(positions[aa]) for aa in amino_acids] + mu = [sum(positions[aa]) / len(positions[aa]) if positions[aa] else 0.0 for aa in amino_acids] + d2 = [sum((p - m) ** 2 for p in positions[aa]) / (len(positions[aa]) * L) if positions[aa] else 0.0 + for aa, m in zip(amino_acids, mu)] + columns = ([f"NV_n_{aa}" for aa in amino_acids] + [f"NV_mu_{aa}" for aa in amino_acids] + + [f"NV_D2_{aa}" for aa in amino_acids]) + return _to_df(n + mu + d2, columns) + +#################################### Atomic Composition ################################## + +def atomic_composition(sequence: str, subtract_water: bool = False) -> pd.DataFrame: + """ + Calculate the Atomic Composition (ATC) of a protein sequence [27]: the + percentage of C, H, N, O and S atoms, summed over the molecular formulas of + the residues' free amino acids. + + ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 + + By default free amino-acid formulas are summed without removing water, + matching Pfeature [27]. With ``subtract_water=True``, (L - 1) H2O are removed + for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" + -> C4H8N2O3). + + Parameters + ========== + :sequence: str + protein sequence. + :subtract_water: bool (default=False) + remove one H2O per peptide bond. + + Returns + ======= + :atomic_composition_df: pd.DataFrame + pandas DataFrame of shape 1 x 5, columns ``ATC_C``, ``ATC_H``, ``ATC_N``, + ``ATC_O`` and ``ATC_S``. Values are percentages. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + formulas = _load_json_table("residue_tables.json", "atomic_formulas")["values"] + atoms = {e: sum(formulas[aa][e] for aa in sequence) for e in "CHNOS"} + if subtract_water: + atoms["H"] -= 2 * (len(sequence) - 1) + atoms["O"] -= len(sequence) - 1 + total = sum(atoms.values()) + return _to_df([atoms[e] / total * 100 for e in "CHNOS"], [f"ATC_{e}" for e in "CHNOS"]) diff --git a/protpy/conjoint_triad.py b/protpy/conjoint_triad.py index c8ca2b1..e4153f2 100644 --- a/protpy/conjoint_triad.py +++ b/protpy/conjoint_triad.py @@ -12,9 +12,13 @@ ========== [1] J. Shen et al., “Predicting protein-protein interactions based only on sequences information,” Proc. Natl. Acad. Sci. U. S. A., vol. 104, no. 11, pp. 4337–4341, 2007. + https://doi.org/10.1073/pnas.0607879104 +[2] Chen, Z. et al. (2018). iFeature: a Python package and web server for features + extraction and selection from protein and peptide sequences. Bioinformatics, + 34(14), 2499-2502. https://doi.org/10.1093/bioinformatics/bty140 """ #list of amino acids -from ._constants import amino_acids, _validate_sequence +from ._constants import amino_acids, _validate_sequence, _validate_int #amino acid triads aa_triads = { @@ -27,7 +31,7 @@ 7: ["C"], } -def conjoint_triad(sequence: str) -> pd.DataFrame: +def conjoint_triad(sequence: str, gap: int = 0) -> pd.DataFrame: """ Calculate Conjoint Triad features (CTriad) for a protein sequence. The descriptor mainly considers neighbor relationships in protein sequences by encoding @@ -36,10 +40,17 @@ def conjoint_triad(sequence: str) -> pd.DataFrame: descriptor calculates 343 different features (7x7x7), with the output being of shape 1 x 343 for a sequence. + With ``gap > 0`` the k-spaced conjoint triad (KSCTriad) is returned: for each + g = 0..gap, triads (s_i, s_{i+g+1}, s_{i+2g+2}) are counted, giving an output + of shape 1 x 343·(gap+1) with columns suffixed ``_gap{g}`` [2]. Values are + raw counts, as for the default descriptor. + Parameters ========== :sequence: str protein sequence. + :gap: int (default=0) + maximum residue spacing for KSCTriad; 0 gives the original conjoint triad. Returns ======= @@ -50,6 +61,7 @@ def conjoint_triad(sequence: str) -> pd.DataFrame: """ #validate input protein sequence sequence = _validate_sequence(sequence) + gap = _validate_int("gap", gap) conjoint_triad = {} _aa_triads = {} @@ -64,15 +76,19 @@ def conjoint_triad(sequence: str) -> pd.DataFrame: for i in _aa_triads: protein_num = protein_num.replace(i, str(_aa_triads[i])) - #count overlapping triads with a sliding window (str.count misses overlaps, e.g. "111" in "11111") - triad_counts = Counter(protein_num[i:i+3] for i in range(len(protein_num) - 2)) + for g in range(gap + 1): + #count overlapping (g-spaced) triads with a sliding window (str.count misses overlaps, e.g. "111" in "11111") + step = g + 1 + triad_counts = Counter(protein_num[i] + protein_num[i+step] + protein_num[i+2*step] + for i in range(len(protein_num) - 2*step)) + suffix = f"_gap{g}" if gap > 0 else "" - #calculate 7 triads for amino acids - for i in range(1, 8): - for j in range(1, 8): - for k in range(1, 8): - temp = str(i) + str(j) + str(k) - conjoint_triad["conj_triad_" + temp] = triad_counts[temp] + #calculate 7 triads for amino acids + for i in range(1, 8): + for j in range(1, 8): + for k in range(1, 8): + temp = str(i) + str(j) + str(k) + conjoint_triad["conj_triad_" + temp + suffix] = triad_counts[temp] #transform descriptor values to dataframe conjoint_triad_df = pd.DataFrame([list(conjoint_triad.values())], columns=list(conjoint_triad.keys())) diff --git a/protpy/ctd.py b/protpy/ctd.py index 91587ec..ae3d43a 100644 --- a/protpy/ctd.py +++ b/protpy/ctd.py @@ -11,6 +11,7 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence +from ._helpers import _load_json_table """ References @@ -23,6 +24,9 @@ classification. Proteins: Structure, Function and Genetics, 1999, 35, 401-407. [3] Gromiha, M. M. (2010). Protein Sequence Analysis. In M. M. Gromiha (Ed.), Protein Bioinformatics (pp. 29–62). Elsevier. +[4] Chen, Z. et al. (2018). iFeature: a Python package and web server for features + extraction and selection from protein and peptide sequences. Bioinformatics, + 34(14), 2499-2502. https://doi.org/10.1093/bioinformatics/bty140 """ hydrophobicity = {"name": "hydrophobicity", "1": "RKEDQN", "2": "GASTPHY", "3": "CLVIMFW"} @@ -57,6 +61,22 @@ "polarizability": polarizability } +#six extra hydrophobicity partitions used by iFeature's 13-property CTD [4]; kept out of +#ctd_properties so the default 7-property outputs are unchanged +ctd_properties_ifeature_extra = { + name: {"name": name, **groups} + for name, groups in _load_json_table("reduced_alphabets.json", "ctd_ifeature_extra")["values"].items() + } + +#property sets accepted by ctd_(property_set=...) +ctd_property_sets = { + "default": ctd_properties, + "ifeature13": {**ctd_properties, **ctd_properties_ifeature_extra}, + } + +#every property accepted by the single-property CTD functions +_all_ctd_properties = ctd_property_sets["ifeature13"] + def str_to_num(sequence: str, property: dict[str, str]) -> str: """ Convert sequences str to number from input physicochemical property. @@ -115,13 +135,13 @@ def ctd_composition(sequence: str, property: str = "hydrophobicity") -> pd.DataF sequence = _validate_sequence(sequence) #get closest matched CTD property - using cutoff of 0.8 - property_matches = get_close_matches(property, ctd_properties.keys(), cutoff=0.8) + property_matches = get_close_matches(property, _all_ctd_properties.keys(), cutoff=0.8) #raise error if no matching property found if (property_matches != []): - prop = ctd_properties[property_matches[0]] + prop = _all_ctd_properties[property_matches[0]] else: - raise ValueError(f"Invalid property '{property}'. Available properties are: {list(ctd_properties.keys())}.") + raise ValueError(f"Invalid property '{property}'. Available properties are: {list(_all_ctd_properties.keys())}.") #convert sequence to number seq = str_to_num(sequence, prop) @@ -166,13 +186,13 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFr sequence = _validate_sequence(sequence) #get closest matched CTD property - using cutoff of 0.8 - property_matches = get_close_matches(property, ctd_properties.keys(), cutoff=0.8) + property_matches = get_close_matches(property, _all_ctd_properties.keys(), cutoff=0.8) #raise error if no matching property found if (property_matches != []): - prop = ctd_properties[property_matches[0]] + prop = _all_ctd_properties[property_matches[0]] else: - raise ValueError(f"Invalid property '{property}'. Available properties are: {list(ctd_properties.keys())}.") + raise ValueError(f"Invalid property '{property}'. Available properties are: {list(_all_ctd_properties.keys())}.") #convert sequence to number seq = str_to_num(sequence, prop) @@ -224,13 +244,13 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.Data sequence = _validate_sequence(sequence) #get closest matched CTD property - using cutoff of 0.8 - property_matches = get_close_matches(property, ctd_properties.keys(), cutoff=0.8) + property_matches = get_close_matches(property, _all_ctd_properties.keys(), cutoff=0.8) #raise error if no matching property found if (property_matches != []): - prop = ctd_properties[property_matches[0]] + prop = _all_ctd_properties[property_matches[0]] else: - raise ValueError(f"Invalid property '{property}'. Available properties are: {list(ctd_properties.keys())}.") + raise ValueError(f"Invalid property '{property}'. Available properties are: {list(_all_ctd_properties.keys())}.") #convert sequence to number seq = str_to_num(sequence, prop) @@ -276,7 +296,8 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.Data return ctd_distribution_df -def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True) -> pd.DataFrame: +def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, + property_set: str = "default") -> pd.DataFrame: """ Calculate all Composition, Transition and Distribution (CTD) features of protein sequences. Composition is the number of amino acids of a particular property (e.g., hydrophobicity) @@ -299,8 +320,12 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True) protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. - :ctd: bool + :all_ctd: bool (default=True) calculate all CTD descriptors and concatenate together. + :property_set: str (default="default") + property set used when ``all_ctd=True``: "default" (the 7 properties + above, 1 x 147) or "ifeature13" (adds the six AAindex hydrophobicity + partitions used by iFeature, 13 properties, 1 x 273) [4]. Returns ======= @@ -311,7 +336,9 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True) """ #validate input protein sequence sequence = _validate_sequence(sequence) - + if property_set not in ctd_property_sets: + raise ValueError(f"Invalid property_set '{property_set}'. Available sets are: {list(ctd_property_sets)}.") + #initialise ctd dataframes comp_df = pd.DataFrame() trans_df = pd.DataFrame() @@ -324,7 +351,7 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True) distr_df = ctd_distribution(sequence, property=property) else: #if using all calculable properties, calculate CTD descriptors for each property - for prop in ctd_properties: + for prop in ctd_property_sets[property_set]: comp = ctd_composition(sequence, property=prop) comp_df = pd.concat([comp_df, comp], axis=1) trans = ctd_transition(sequence, property=prop) diff --git a/protpy/utils.py b/protpy/utils.py index 05ae1bd..42b4b52 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -22,6 +22,8 @@ "abbreviation": "AAComp", "parameters": { "sequence": "str — protein sequence", + "region": "str | None (default=None) — None, 'nterm', 'cterm' or 'split' (NT_/MID_/CT_ blocks; split AAC, Kumar et al. 2006)", + "n": "int (default=25) — terminal block length", }, "output_shape": "1 x 20", "speed": "Fast", @@ -39,6 +41,8 @@ "abbreviation": "DPComp", "parameters": { "sequence": "str — protein sequence", + "region": "str | None (default=None) — None, 'nterm', 'cterm' or 'split' (NT_/MID_/CT_ blocks)", + "n": "int (default=25) — terminal block length", }, "output_shape": "1 x 400", "speed": "Fast", @@ -228,6 +232,8 @@ "parameters": { "sequence": "str — protein sequence", "alphabet_size": "int (default=6) — number of groups (2, 3, 4 or 6)", + "alphabet": "str | None (default=None) — 'ifeature5' (GAAC/GDPC/GTPC), 'cp13', 'cp14', 'cp19' or 'cp20'", + "k": "int (default=1) — group k-mer length, 1-3", }, "output_shape": "1 x alphabet_size", "speed": "Fast", @@ -500,6 +506,7 @@ "abbreviation": "CTriad", "parameters": { "sequence": "str — protein sequence", + "gap": "int (default=0) — >0 gives KSCTriad (columns suffixed _gap{g})", }, "output_shape": "1 x 343", "speed": "Fast", @@ -576,6 +583,7 @@ "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — CTD property (used when all_ctd=False)", "all_ctd": "bool (default=True) — if True, compute over all 7 properties", + "property_set": "str (default='default') — 'default' (7 properties) or 'ifeature13' (13 properties, 1 x 273)", }, "output_shape": "1 x 21 (single property) / 1 x 147 (all 7 properties)", "speed": "Fast (single) / Moderate (all)", @@ -680,6 +688,116 @@ "doi": "10.1006/bbrc.2000.3815", "in_default_set": True, }, + "cksaap": { + "description": "Composition of k-spaced amino acid pairs: CKSAAP(XY,g) = N_g(XY) / (L-g-1) x 100 for g = 0..gap.", + "module": "composition", + "category": "Composition", + "abbreviation": "CKSAAP", + "parameters": { + "sequence": "str — protein sequence", + "gap": "int (default=5) — maximum spacing", + }, + "output_shape": "1 x 400·(gap+1) (default 1 x 2400)", + "speed": "Moderate", + "reference": "Chen, Kurgan & Ruan (2007). BMC Struct. Biol., 7, 25.", + "doi": "10.1186/1472-6807-7-25", + "descriptor_sets": ["all"], + "in_default_set": False, + }, + "cksaagp": { + "description": "Composition of k-spaced amino acid group pairs over the iFeature 5-group alphabet.", + "module": "composition", + "category": "Composition", + "abbreviation": "CKSAAGP", + "parameters": { + "sequence": "str — protein sequence", + "gap": "int (default=5) — maximum spacing", + }, + "output_shape": "1 x 25·(gap+1) (default 1 x 150)", + "speed": "Fast", + "reference": "Chen et al. (2018). Bioinformatics, 34(14), 2499-2502 (iFeature).", + "doi": "10.1093/bioinformatics/bty140", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "dde": { + "description": "Dipeptide deviation from expected mean: (Dc - Tm) / sqrt(Tv), with Tm from codon counts.", + "module": "composition", + "category": "Composition", + "abbreviation": "DDE", + "parameters": { + "sequence": "str — protein sequence (length >= 2)", + }, + "output_shape": "1 x 400", + "speed": "Fast", + "reference": "Saravanan & Gautham (2015). OMICS, 19(10), 648-658.", + "doi": "10.1089/omi.2015.0095", + "descriptor_sets": ["all"], + "in_default_set": False, + }, + "asdc": { + "description": "Adaptive skip dipeptide composition: composition of all ordered pairs (s_i, s_j), i < j.", + "module": "composition", + "category": "Composition", + "abbreviation": "ASDC", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 400", + "speed": "Fast", + "reference": "Wei et al. (2018). Bioinformatics, 34(23), 4007-4016 (ACPred-FL).", + "doi": "10.1093/bioinformatics/bty451", + "descriptor_sets": ["all"], + "in_default_set": False, + }, + "distance_pair": { + "description": "PseAAC of distance-pairs over a reduced cluster profile (cp13/cp14/cp19/cp20).", + "module": "composition", + "category": "Composition", + "abbreviation": "DP", + "parameters": { + "sequence": "str — protein sequence", + "rng": "int (default=3) — maximum pair distance", + "alphabet": "str (default='cp14') — cluster profile", + }, + "output_shape": "1 x (G + G²·rng) (default 1 x 602)", + "speed": "Fast", + "reference": "Liu et al. (2014). PLoS ONE, 9(9), e106691 (iDNA-Prot|dis).", + "doi": "10.1371/journal.pone.0106691", + "descriptor_sets": ["all"], + "in_default_set": False, + }, + "natural_vector": { + "description": "Natural vector: count, mean position and normalised second central moment of each amino acid.", + "module": "composition", + "category": "Composition", + "abbreviation": "NV", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 60", + "speed": "Fast", + "reference": "Yu et al. (2013). J. Theor. Biol., 318, 197-204.", + "doi": "10.1016/j.jtbi.2012.11.005", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "atomic_composition": { + "description": "Percentage of C, H, N, O and S atoms over the residues' free amino-acid formulas.", + "module": "composition", + "category": "Composition", + "abbreviation": "ATC", + "parameters": { + "sequence": "str — protein sequence", + "subtract_water": "bool (default=False) — remove (L-1) H2O", + }, + "output_shape": "1 x 5", + "speed": "Fast", + "reference": "Pande et al. (2023). J. Comput. Biol., 30(2), 204-222 (Pfeature).", + "doi": "10.1089/cmb.2022.0241", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_composition_extended.py b/tests/test_composition_extended.py new file mode 100644 index 0000000..fe8e12a --- /dev/null +++ b/tests/test_composition_extended.py @@ -0,0 +1,183 @@ +########################################################################### +######### protPy - v1.5.0 Composition Descriptor Tests (Phase 1) ####### +########################################################################### + +import itertools +import math +import unittest + +import numpy as np + +import protpy +from protpy.ctd import ctd_property_sets + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQ" +AA = protpy.amino_acids + + +class ProtpyCompositionExtendedTests(unittest.TestCase): + + def _check_invalid(self, func, **bad): + with self.assertRaises(TypeError): + func(123) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("ACDXZ") + for k, v in bad.items(): + with self.assertRaises(ValueError, msg=f"{func.__name__}({k}={v})"): + func(SEQ, **{k: v}) + + def test_cksaap(self): + df = protpy.cksaap(SEQ) + self.assertEqual(df.shape, (1, 2400)) + self.assertEqual(df.columns[0], "CKSAAP_AA_gap0") + self.assertEqual(df.columns[-1], "CKSAAP_YY_gap5") + self.assertTrue(all(dt == np.float64 for dt in df.dtypes)) + #gap=0 equals the dipeptide composition (which is rounded to 2 dp) + dpc = protpy.dipeptide_composition(SEQ) + gap0 = protpy.cksaap(SEQ, gap=0) + np.testing.assert_allclose(gap0.values[0], dpc.values[0], atol=0.006) + self.assertEqual(protpy.cksaap("AAAA", gap=1)["CKSAAP_AA_gap1"].iloc[0], 100.0) + #spacing with no pairs -> zeros + self.assertEqual(protpy.cksaap("AC", gap=2).filter(like="_gap2").values.sum(), 0) + self._check_invalid(protpy.cksaap, gap=-1) + + def test_cksaagp(self): + df = protpy.cksaagp(SEQ) + self.assertEqual(df.shape, (1, 150)) + self.assertEqual(df.columns[0], "CKSAAGP_aliphatic.aliphatic_gap0") + for g in range(6): + self.assertAlmostEqual(df.filter(like=f"_gap{g}").values.sum(), 100, delta=0.05) + #G and A are both aliphatic + self.assertEqual(protpy.cksaagp("GAG", gap=0)["CKSAAGP_aliphatic.aliphatic_gap0"].iloc[0], 100.0) + self._check_invalid(protpy.cksaagp, gap="x") + + def test_dde(self): + df = protpy.dde("ACDEFGHIKL") + self.assertEqual(df.shape, (1, 400)) + self.assertEqual(df.columns[1], "DDE_AC") + #hand computation of the AC cell: one AC dipeptide, A has 4 codons, C has 2 + dc, tm = 1 / 9, (4 / 61) * (2 / 61) + expected = (dc - tm) / math.sqrt(tm * (1 - tm) / 9) + self.assertAlmostEqual(df["DDE_AC"].iloc[0], round(expected, 3)) + #an absent dipeptide is negative + self.assertLess(df["DDE_AA"].iloc[0], 0) + with self.assertRaises(ValueError): + protpy.dde("A") + self._check_invalid(protpy.dde) + + def test_asdc(self): + df = protpy.asdc("ACA") + self.assertEqual(df.shape, (1, 400)) + for col in ("ASDC_AC", "ASDC_AA", "ASDC_CA"): + self.assertEqual(df[col].iloc[0], 33.333) + self.assertAlmostEqual(protpy.asdc(SEQ).values.sum(), 100, delta=0.05) + #brute force on SEQ + counts = {} + for i, j in itertools.combinations(range(len(SEQ)), 2): + counts[SEQ[i] + SEQ[j]] = counts.get(SEQ[i] + SEQ[j], 0) + 1 + total = len(SEQ) * (len(SEQ) - 1) / 2 + self.assertEqual(protpy.asdc(SEQ)["ASDC_KV"].iloc[0], round(counts["KV"] / total * 100, 3)) + self.assertEqual(protpy.asdc("A").values.sum(), 0) + self._check_invalid(protpy.asdc) + + def test_distance_pair(self): + df = protpy.distance_pair(SEQ) + self.assertEqual(df.shape, (1, 14 + 14 * 14 * 3)) + self.assertEqual(df.columns[0], "DP_IMV") + self.assertIn("DP_IMV.L_d1", df.columns) + self.assertEqual(protpy.distance_pair(SEQ, rng=3, alphabet="cp13").shape, (1, 13 + 169 * 3)) + self.assertEqual(protpy.distance_pair(SEQ, rng=1, alphabet="cp20").shape, (1, 420)) + #composition block sums to 100; each distance block sums to 100 + self.assertAlmostEqual(df.iloc[0, :14].sum(), 100, delta=0.05) + self.assertAlmostEqual(df.filter(like="_d2").values.sum(), 100, delta=0.05) + #I, M and V share a cp14 cluster + self.assertEqual(protpy.distance_pair("IMV", rng=2)["DP_IMV.IMV_d2"].iloc[0], 100.0) + self._check_invalid(protpy.distance_pair, rng=-1, alphabet="cp15") + + def test_reduced_alphabets_partition(self): + from protpy.composition import _named_alphabet + for name in ("ifeature5", "cp13", "cp14", "cp19", "cp20"): + residues = "".join(g for _, g in _named_alphabet(name)) + self.assertEqual(sorted(residues), AA, name) + self.assertEqual(len(_named_alphabet("cp14")), 14) + + def test_natural_vector(self): + df = protpy.natural_vector("ACA") + self.assertEqual(df.shape, (1, 60)) + #A at positions 1 and 3: n=2, mu=2, D2 = ((1-2)^2 + (3-2)^2) / (2*3) + self.assertEqual(df["NV_n_A"].iloc[0], 2) + self.assertEqual(df["NV_mu_A"].iloc[0], 2.0) + self.assertEqual(df["NV_D2_A"].iloc[0], round(2 / 6, 3)) + self.assertEqual(df["NV_mu_C"].iloc[0], 2.0) + self.assertEqual(df["NV_D2_C"].iloc[0], 0.0) + self.assertEqual(df["NV_mu_W"].iloc[0], 0.0) + self._check_invalid(protpy.natural_vector) + + def test_atomic_composition(self): + df = protpy.atomic_composition("GG", subtract_water=True) + self.assertEqual(list(df.columns), ["ATC_C", "ATC_H", "ATC_N", "ATC_O", "ATC_S"]) + #C4H8N2O3 -> 17 atoms + np.testing.assert_allclose(df.values[0], np.round(np.array([4, 8, 2, 3, 0]) / 17 * 100, 3)) + #default: free amino acids, no water removed (Pfeature convention): 2 x C2H5NO2 + np.testing.assert_allclose(protpy.atomic_composition("GG").values[0], [20, 50, 10, 20, 0]) + self.assertGreater(protpy.atomic_composition("MC")["ATC_S"].iloc[0], 0) + self.assertAlmostEqual(protpy.atomic_composition(SEQ).values.sum(), 100, delta=0.01) + self._check_invalid(protpy.atomic_composition) + + #extensions to existing functions + + def test_reduced_alphabet_extension(self): + #defaults unchanged + self.assertEqual(list(protpy.reduced_alphabet_composition(SEQ).columns), [f"ReducedAlphabet_{i}" for i in range(1, 7)]) + gaac = protpy.reduced_alphabet_composition("GAFKD", alphabet="ifeature5") + self.assertEqual(list(gaac.columns), ["GAAC_aliphatic", "GAAC_aromatic", "GAAC_positive", "GAAC_negative", "GAAC_uncharged"]) + self.assertEqual(gaac.values[0].tolist(), [40.0, 20.0, 20.0, 20.0, 0.0]) + self.assertEqual(protpy.reduced_alphabet_composition(SEQ, alphabet="ifeature5", k=2).shape, (1, 25)) + gtpc = protpy.reduced_alphabet_composition(SEQ, alphabet="ifeature5", k=3) + self.assertEqual(gtpc.shape, (1, 125)) + self.assertEqual(gtpc.columns[0], "GTPC_aliphatic.aliphatic.aliphatic") + self.assertAlmostEqual(gtpc.values.sum(), 100, delta=0.1) + with self.assertRaises(ValueError): + protpy.reduced_alphabet_composition(SEQ, k=4) + with self.assertRaises(ValueError): + protpy.reduced_alphabet_composition(SEQ, alphabet="nope") + + def test_ksctriad(self): + self.assertEqual(protpy.conjoint_triad(SEQ).shape, (1, 343)) + df = protpy.conjoint_triad("AAAAAAA", gap=1) + self.assertEqual(df.shape, (1, 686)) + #gap 0: 5 contiguous triads; gap 1: (i, i+2, i+4) -> 3 triads + self.assertEqual(df["conj_triad_111_gap0"].iloc[0], 5) + self.assertEqual(df["conj_triad_111_gap1"].iloc[0], 3) + with self.assertRaises(ValueError): + protpy.conjoint_triad(SEQ, gap=-1) + + def test_ctd_ifeature13(self): + for name, prop in ctd_property_sets["ifeature13"].items(): + groups = prop["1"] + prop["2"] + prop["3"] + self.assertEqual(sorted(groups), AA, name) + self.assertEqual(len(ctd_property_sets["ifeature13"]), 13) + self.assertEqual(protpy.ctd_(SEQ).shape, (1, 147)) + self.assertEqual(protpy.ctd_(SEQ, property_set="ifeature13").shape, (1, 273)) + self.assertEqual(protpy.ctd_composition(SEQ, property="hydrophobicity_ENGD860101").shape, (1, 3)) + with self.assertRaises(ValueError): + protpy.ctd_(SEQ, property_set="nope") + + def test_region(self): + seq = "A" * 25 + "C" * 10 + "D" * 25 + df = protpy.amino_acid_composition(seq, region="split") + self.assertEqual(df.shape, (1, 60)) + self.assertEqual((df["NT_A"].iloc[0], df["MID_C"].iloc[0], df["CT_D"].iloc[0]), (100.0, 100.0, 100.0)) + self.assertEqual(protpy.amino_acid_composition(seq, region="nterm", n=5)["NT_A"].iloc[0], 100.0) + self.assertEqual(protpy.dipeptide_composition(seq, region="cterm").shape, (1, 400)) + self.assertEqual(protpy.dipeptide_composition(seq, region="split").columns[400], "MID_AA") + with self.assertRaises(ValueError): + protpy.amino_acid_composition("A" * 50, region="split") + with self.assertRaises(ValueError): + protpy.amino_acid_composition(seq, region="middle") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_protpy.py b/tests/test_protpy.py index a2862ae..331f7ba 100644 --- a/tests/test_protpy.py +++ b/tests/test_protpy.py @@ -47,8 +47,13 @@ def test_protpy_metadata(self): def test_valid_descriptors(self): """ Testing correct list of available descriptors is included in package attribute. """ - self.assertEqual(len(protpy.descriptor_names), 36, - "Expected there to be 36 total descriptors, got {}.".format(len(protpy.descriptor_names))) + from protpy.utils import _DESCRIPTOR_REGISTRY + #every exported descriptor is registered (and vice versa) and callable + self.assertEqual(sorted(protpy.descriptor_names), sorted(_DESCRIPTOR_REGISTRY), + "descriptor_names and the descriptor registry should list the same descriptors.") + self.assertEqual(len(set(protpy.descriptor_names)), len(protpy.descriptor_names)) + for name in protpy.descriptor_names: + self.assertTrue(callable(getattr(protpy, name)), name) self.assertIsInstance(protpy.descriptor_names, list, "all_descriptors should be of type list, got {}.".format(type(protpy.descriptor_names))) # composition descriptors From 1c9e8c8287bf5721beb0987c08847bcddda31692 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:19:27 +0100 Subject: [PATCH 05/17] v1.5.0 phase 2: amino-acid factor scales, auto/cross covariance, PDT --- planning/BLOCKED.md | 15 + protpy/__init__.py | 3 + protpy/data/aa_scales.json | 1592 ++++++++++++++++++++++++++++++++++++ protpy/scales.py | 259 ++++++ protpy/utils.py | 179 ++++ tests/test_scales.py | 103 +++ tests/test_utils.py | 3 +- 7 files changed, 2153 insertions(+), 1 deletion(-) create mode 100644 protpy/scales.py create mode 100644 tests/test_scales.py diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md index 4c7566a..de836d6 100644 --- a/planning/BLOCKED.md +++ b/planning/BLOCKED.md @@ -6,3 +6,18 @@ Each entry names the table that is missing and where it would have to come from. | # | Item | Missing data | Notes | |---|---|---|---| | 6 | `psekraac` | The 16 PseKRAAC reduced-alphabet type tables (types 1–16, every cluster size) | The Zuo et al. 2017 supplement (`bioinformatics_33_1_122_s1.doc`, fetched 2026-10-12) only lists the 16 types and their source papers; it does not contain the cluster tables. They would have to be transcribed from the 16 source papers (Liang 2015, Ogul 2007, Kosiol 2004, Zuo 2010, Melo 2006, Rakshit 2008, Susko 2007, Liu 2002, Li 2007, Peterson 2009, Cannata 2002, Li 2003) or obtained from the PseKRAAC server authors. The iFeature `PseKRAAC/` copies have no licence. | +| 9 | `z_scales` (Hellberg z1–z3) | Hellberg et al. 1987 Table (J Med Chem, 10.1021/jm00390a003) | Paywalled; no permissively licensed reproduction found. van Westen 2013's "Z-scales (3)" is a subset of Sandberg's 5, not Hellberg's, so it can't be substituted. `z_scales_5` is shipped. | +| 12 | `kidera_factors` | Kidera et al. 1985 10-factor table (J Protein Chem, 10.1007/BF01025492) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | +| 20 | `cruciani_properties` | Cruciani et al. 2004 3-property table (J Chemometrics, 10.1002/cem.856) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | + +## Shipped with caveats + +- **Factor scales (Phase 2).** `z_scales_5`, `vhse`, `t_scales`, `st_scales`, `fasgai`, `blosum_indices` and `ms_whim` were transcribed from van Westen et al. 2013 Additional file 2 (CC-BY), because the primary papers are paywalled. Seven cells differ from peptides.py and are flagged `UNRESOLVED` in `protpy/data/aa_scales.json`: + - FASGAI: V F1, P F6 + - ST: Q ST1, E ST2 + - VHSE: L VHSE3 + - z5: C Z3, L Z4 + + Check these against the primary tables before release. +- **`distance_pair`.** The plan specified G + G²·(rng+1) outputs over distances 0..rng. Liu 2014 (Eqs. 5–12) defines d = 1..rng, giving G + G²·rng (602 for cp14, d = 3, as the paper states), and that is what is implemented. The paper's printed cp(14) lists E twice; the stray E in the first cluster is dropped, the only 14-cluster reading. +- **`atomic_composition`.** By default it matches Pfeature (no water removed); `subtract_water=True` gives the true peptide formula. Pfeature's W and Y formulas each have one H too many, and protpy uses the correct formulas. diff --git a/protpy/__init__.py b/protpy/__init__.py index 5cd8931..b898092 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -3,6 +3,7 @@ from .conjoint_triad import * from .ctd import * from .sequence_order import * +from .scales import * from .utils import get_descriptor_info, list_descriptors, print_descriptor_info from .encode import all_descriptors, encode_dataset, read_sequences_from_file @@ -42,4 +43,6 @@ "shannon_entropy", #v1.5.0 "cksaap", "cksaagp", "dde", "asdc", "distance_pair", "natural_vector", "atomic_composition", + "z_scales_5", "vhse", "atchley_factors", "t_scales", "st_scales", "protfp", "fasgai", + "blosum_indices", "ms_whim", "auto_cross_covariance", "physicochemical_distance_transformation", ] \ No newline at end of file diff --git a/protpy/data/aa_scales.json b/protpy/data/aa_scales.json index d97e1d7..32b7678 100644 --- a/protpy/data/aa_scales.json +++ b/protpy/data/aa_scales.json @@ -194,5 +194,1597 @@ "reference": "Cosic, I. 'Macromolecular bioactivity: is it resonant interaction between macromolecules?--Theory and applications' IEEE Trans Biomed Eng. 41, 1101-1114 (1994) (values are cited from Protein Eng. 15:193-203)", "doi": "10.1109/10.335859", "source_note": "AAindex1 record COSI940101 (Kawashima et al. 2008), which reproduces the published table; copied 2026-10-12." + }, + "z_scales_5": { + "name": "z_scales_5", + "values": { + "A": [ + 0.24, + -2.32, + 0.6, + -0.14, + 1.3 + ], + "C": [ + 0.84, + -1.67, + 3.71, + 0.18, + -2.65 + ], + "D": [ + 3.98, + 0.93, + 1.93, + -2.46, + 0.75 + ], + "E": [ + 3.11, + 0.26, + -0.11, + -3.04, + -0.25 + ], + "F": [ + -4.22, + 1.94, + 1.06, + 0.54, + -0.62 + ], + "G": [ + 2.05, + -4.06, + 0.36, + -0.82, + -0.38 + ], + "H": [ + 2.47, + 1.95, + 0.26, + 3.9, + 0.09 + ], + "I": [ + -3.89, + -1.73, + -1.71, + -0.84, + 0.26 + ], + "K": [ + 2.29, + 0.89, + -2.49, + 1.49, + 0.31 + ], + "L": [ + -4.28, + -1.3, + -1.49, + 0.72, + 0.84 + ], + "M": [ + -2.85, + -0.22, + 0.47, + 1.94, + -0.98 + ], + "N": [ + 3.05, + 1.62, + 1.04, + -1.15, + 1.61 + ], + "P": [ + -1.66, + 0.27, + 1.84, + 0.7, + 2.0 + ], + "Q": [ + 1.75, + 0.5, + -1.44, + -1.34, + 0.66 + ], + "R": [ + 3.52, + 2.5, + -3.5, + 1.99, + -0.17 + ], + "S": [ + 2.39, + -1.07, + 1.15, + -1.39, + 0.67 + ], + "T": [ + 0.75, + -2.18, + -1.12, + -1.46, + -0.4 + ], + "V": [ + -2.59, + -2.64, + -1.54, + -0.85, + -0.02 + ], + "W": [ + -4.36, + 3.94, + 0.59, + 3.44, + -1.59 + ], + "Y": [ + -2.54, + 2.44, + 0.43, + 0.04, + -1.47 + ] + }, + "reference": "Sandberg M. et al. (1998). New chemical descriptors relevant for the design of biologically active peptides. A multivariate characterization of 87 amino acids. J Med Chem, 41(14), 2481-2491.", + "doi": "10.1021/jm9700575", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties Z1..Z5. Secondary source: the primary paper is paywalled and was not available to check. UNRESOLVED: differs from peptides.py at C Z3 (3.71 here, 3.75 in peptides.py), L Z4 (0.72 vs -0.72); check against the primary paper." + }, + "vhse": { + "name": "vhse", + "values": { + "A": [ + 0.15, + -1.11, + -1.35, + -0.92, + 0.02, + -0.91, + 0.36, + -0.48 + ], + "C": [ + 0.18, + -1.67, + -0.46, + -0.21, + 0.0, + 1.2, + -1.61, + -0.19 + ], + "D": [ + -1.15, + 0.67, + -0.41, + -0.01, + -2.68, + 1.31, + 0.03, + 0.56 + ], + "E": [ + -1.18, + 0.4, + 0.1, + 0.36, + -2.16, + -0.17, + 0.91, + 0.02 + ], + "F": [ + 1.52, + 0.61, + 0.96, + -0.16, + 0.25, + 0.28, + -1.33, + -0.2 + ], + "G": [ + -0.2, + -1.53, + -2.63, + 2.28, + -0.53, + -1.18, + 2.01, + -1.34 + ], + "H": [ + -0.43, + -0.25, + 0.37, + 0.19, + 0.51, + 1.28, + 0.93, + 0.65 + ], + "I": [ + 1.27, + -0.14, + 0.3, + -1.8, + 0.3, + -1.61, + -0.16, + -0.13 + ], + "K": [ + -1.17, + 0.7, + 0.7, + 0.8, + 1.64, + 0.67, + 1.63, + 0.13 + ], + "L": [ + 1.36, + 0.07, + 0.26, + -0.8, + 0.22, + -1.37, + 0.08, + -0.62 + ], + "M": [ + 1.01, + -0.53, + 0.43, + 0.0, + 0.23, + 0.1, + -0.86, + -0.68 + ], + "N": [ + -0.99, + 0.0, + -0.37, + 0.69, + -0.55, + 0.85, + 0.73, + -0.8 + ], + "P": [ + 0.22, + -0.17, + -0.5, + 0.05, + -0.01, + -1.34, + -0.19, + 3.56 + ], + "Q": [ + -0.96, + 0.12, + 0.18, + 0.16, + 0.09, + 0.42, + -0.2, + -0.41 + ], + "R": [ + -1.47, + 1.45, + 1.24, + 1.27, + 1.55, + 1.47, + 1.3, + 0.83 + ], + "S": [ + -0.67, + -0.86, + -1.07, + -0.41, + -0.32, + 0.27, + -0.64, + 0.11 + ], + "T": [ + -0.34, + -0.51, + -0.55, + -1.06, + -0.06, + -0.01, + -0.79, + 0.39 + ], + "V": [ + 0.76, + -0.92, + -0.17, + -1.91, + 0.22, + -1.4, + -0.24, + -0.03 + ], + "W": [ + 1.5, + 2.06, + 1.79, + 0.75, + 0.75, + -0.13, + -1.01, + -0.85 + ], + "Y": [ + 0.61, + 1.6, + 1.17, + 0.73, + 0.53, + 0.25, + -0.96, + -0.52 + ] + }, + "reference": "Mei H. et al. (2005). A new set of amino acid descriptors and its application in peptide QSARs. Biopolymers, 80(6), 775-786.", + "doi": "10.1002/bip.20296", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties VHSE1..VHSE8. Secondary source: the primary paper is paywalled and was not available to check. UNRESOLVED: differs from peptides.py at L VHSE3 (0.26 here, 0.36 in peptides.py); check against the primary paper." + }, + "st_scales": { + "name": "st_scales", + "values": { + "A": [ + -1.552, + -0.791, + -0.627, + 0.237, + -0.461, + -2.229, + 0.283, + 1.221 + ], + "C": [ + -1.276, + -0.401, + 0.134, + 0.859, + -0.196, + -0.72, + 0.639, + -0.857 + ], + "D": [ + -0.907, + -0.054, + -0.781, + -0.248, + 1.12, + 0.101, + -0.245, + -0.075 + ], + "E": [ + -0.629, + 0.39, + -0.38, + -0.366, + 0.635, + 0.514, + 0.175, + 0.367 + ], + "F": [ + -0.019, + 0.024, + 1.08, + -0.22, + -0.937, + 0.57, + -0.357, + 0.278 + ], + "G": [ + -1.844, + -0.018, + -0.184, + 0.573, + -0.728, + -3.317, + 0.166, + 2.522 + ], + "H": [ + -0.225, + 0.361, + 0.079, + -1.037, + 0.568, + 0.273, + 1.208, + -0.001 + ], + "I": [ + -0.785, + -1.01, + -0.349, + -0.097, + -0.402, + 1.091, + -0.139, + -0.764 + ], + "K": [ + -0.504, + 0.245, + 0.297, + -0.065, + -0.387, + 1.011, + 0.525, + 0.553 + ], + "L": [ + -0.826, + -0.379, + 0.038, + -0.059, + -0.625, + 1.025, + -0.229, + -0.129 + ], + "M": [ + -0.693, + 0.498, + 0.658, + 0.457, + -0.231, + 1.064, + 0.248, + -0.778 + ], + "N": [ + -0.888, + -0.057, + -0.651, + -0.214, + 0.917, + 0.164, + -0.14, + -0.166 + ], + "P": [ + -1.049, + -0.407, + -0.067, + -0.066, + -0.813, + -0.89, + 0.021, + -0.894 + ], + "Q": [ + -0.622, + 0.228, + -0.193, + -0.105, + 0.418, + 0.474, + 0.172, + 0.408 + ], + "R": [ + -0.059, + 0.731, + -0.013, + -0.096, + -0.253, + 0.3, + 1.256, + 0.854 + ], + "S": [ + -1.343, + -0.311, + -0.917, + -0.049, + 0.549, + -1.533, + 0.166, + 0.28 + ], + "T": [ + -1.061, + -0.928, + -0.911, + -0.063, + 0.538, + -0.775, + -0.147, + -0.717 + ], + "V": [ + -1.133, + -0.893, + -0.325, + 0.303, + -0.561, + -0.175, + -0.02, + -0.311 + ], + "W": [ + 0.853, + 0.039, + 0.26, + -1.163, + 0.16, + -0.202, + 1.01, + 0.195 + ], + "Y": [ + 0.308, + 0.569, + 1.1, + -0.464, + -0.144, + -0.354, + -1.099, + 0.162 + ] + }, + "reference": "Yang L. et al. (2010). ST-scale as a novel amino acid descriptor and its application in QSAM of peptides and analogues. Amino Acids, 38(3), 805-816.", + "doi": "10.1007/s00726-009-0287-y", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties ST1..ST8. Secondary source: the primary paper is paywalled and was not available to check. UNRESOLVED: differs from peptides.py at Q ST1 (-0.622 here, -0.662 in peptides.py), E ST2 (0.39 vs -0.39); check against the primary paper." + }, + "t_scales": { + "name": "t_scales", + "values": { + "A": [ + -9.11, + -1.63, + 0.63, + 1.04, + 2.26 + ], + "C": [ + -7.35, + -0.86, + -0.33, + 0.8, + 0.98 + ], + "D": [ + -4.65, + 0.75, + 1.39, + -0.4, + 1.05 + ], + "E": [ + -3.03, + 1.82, + 0.51, + -0.58, + 0.43 + ], + "F": [ + 0.49, + -0.94, + -0.63, + -1.27, + -0.44 + ], + "G": [ + -10.61, + -1.21, + -0.12, + 0.75, + 3.25 + ], + "H": [ + -1.01, + -1.31, + 0.01, + -1.81, + -0.21 + ], + "I": [ + -4.25, + -0.28, + -0.15, + 1.4, + -0.21 + ], + "K": [ + -2.59, + 2.34, + -1.69, + 0.41, + -0.21 + ], + "L": [ + -4.38, + 0.28, + -0.49, + 1.45, + 0.02 + ], + "M": [ + -4.08, + 0.98, + -2.34, + 1.64, + -0.79 + ], + "N": [ + -4.62, + 0.66, + 1.16, + -0.22, + 0.93 + ], + "P": [ + -5.11, + -3.54, + -0.53, + -0.36, + -0.29 + ], + "Q": [ + -3.0, + 1.72, + 0.28, + -0.39, + 0.33 + ], + "R": [ + 0.23, + 3.89, + -1.16, + -0.39, + -0.06 + ], + "S": [ + -7.44, + -0.65, + 0.68, + -0.17, + 1.58 + ], + "T": [ + -5.97, + -0.62, + 1.11, + 0.31, + 0.95 + ], + "V": [ + -5.87, + -0.94, + 0.28, + 1.1, + 0.48 + ], + "W": [ + 5.73, + -2.67, + -0.07, + -1.96, + -0.54 + ], + "Y": [ + 2.08, + -0.47, + 0.07, + -1.67, + -0.35 + ] + }, + "reference": "Tian F., Zhou P. & Li Z. (2007). T-scale as a novel vector of topological descriptors for amino acids and its application in QSARs of peptides. J Mol Struct, 830, 106-115.", + "doi": "10.1016/j.molstruc.2006.07.004", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties T1..T5. Secondary source: the primary paper is paywalled and was not available to check. Cross-checked: identical to peptides.py." + }, + "ms_whim": { + "name": "ms_whim", + "values": { + "A": [ + -0.73, + 0.2, + -0.62 + ], + "C": [ + -0.66, + 0.26, + -0.27 + ], + "D": [ + 0.11, + -1.0, + -0.96 + ], + "E": [ + 0.24, + -0.39, + -0.04 + ], + "F": [ + 0.76, + 0.85, + -0.34 + ], + "G": [ + -0.31, + -0.28, + -0.75 + ], + "H": [ + 0.84, + 0.67, + -0.78 + ], + "I": [ + -0.91, + 0.83, + -0.25 + ], + "K": [ + -0.51, + 0.08, + 0.6 + ], + "L": [ + -0.74, + 0.72, + -0.16 + ], + "M": [ + -0.7, + 1.0, + -0.32 + ], + "N": [ + 0.14, + 0.2, + -0.66 + ], + "P": [ + -0.43, + 0.73, + -0.6 + ], + "Q": [ + 0.3, + 1.0, + -0.3 + ], + "R": [ + -0.22, + 0.27, + 1.0 + ], + "S": [ + -0.8, + 0.61, + -1.0 + ], + "T": [ + -0.58, + 0.85, + -0.89 + ], + "V": [ + -1.0, + 0.79, + -0.58 + ], + "W": [ + 1.0, + 0.98, + -0.47 + ], + "Y": [ + 0.97, + 0.66, + -0.16 + ] + }, + "reference": "Zaliani A. & Gancia E. (1999). MS-WHIM scores for amino acids: a new 3D-description for peptide QSAR and QSPR studies. J Chem Inf Comput Sci, 39(3), 525-533.", + "doi": "10.1021/ci980211b", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties MW1..MW3. Secondary source: the primary paper is paywalled and was not available to check. Cross-checked: identical to peptides.py." + }, + "fasgai": { + "name": "fasgai", + "values": { + "A": [ + 0.207, + 0.821, + -1.009, + 1.387, + 0.063, + -0.6 + ], + "C": [ + 0.997, + 0.021, + -1.419, + -2.08, + -0.799, + 0.502 + ], + "D": [ + -1.298, + -0.444, + -0.584, + -0.175, + -0.259, + -1.762 + ], + "E": [ + -1.349, + 1.388, + -0.361, + 0.213, + 0.424, + -1.303 + ], + "F": [ + 1.247, + 0.293, + 1.336, + -0.026, + 0.012, + -0.015 + ], + "G": [ + -0.205, + -2.219, + -1.656, + 1.229, + -1.115, + -1.146 + ], + "H": [ + -0.27, + 0.461, + -0.024, + -1.407, + 0.001, + 0.169 + ], + "I": [ + 1.524, + 0.536, + 0.809, + 0.734, + -0.196, + 0.427 + ], + "K": [ + -1.387, + 0.572, + 0.285, + 0.333, + -0.169, + 1.157 + ], + "L": [ + 1.2, + 1.128, + 0.703, + 1.904, + 0.536, + -0.141 + ], + "M": [ + 0.886, + 1.346, + 0.277, + -0.913, + 0.007, + -0.265 + ], + "N": [ + -1.009, + -0.939, + -0.428, + -0.397, + -0.539, + -0.605 + ], + "P": [ + -0.407, + -2.038, + -0.564, + -0.128, + 3.847, + -1.008 + ], + "Q": [ + -0.88, + 0.381, + -0.044, + -0.455, + -0.04, + 0.405 + ], + "R": [ + -1.229, + 0.378, + 0.516, + -0.328, + -0.052, + 2.728 + ], + "S": [ + -0.495, + -0.847, + -1.079, + 0.582, + 0.035, + -0.068 + ], + "T": [ + -0.032, + -0.45, + -0.61, + 0.341, + 0.117, + 0.577 + ], + "V": [ + 1.332, + 0.545, + 0.029, + 1.026, + -0.229, + 1.038 + ], + "W": [ + 0.844, + -0.075, + 2.069, + -1.36, + -0.81, + -0.38 + ], + "Y": [ + 0.329, + -0.858, + 1.753, + -0.479, + -0.835, + 0.289 + ] + }, + "reference": "Liang G. & Li Z. (2007). Factor analysis scale of generalized amino acid information as the source of a new set of descriptors for elucidating the structure and activity relationships of cationic antimicrobial peptides. QSAR Comb Sci, 26(6), 754-763.", + "doi": "10.1002/qsar.200630145", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties F1..F6. Secondary source: the primary paper is paywalled and was not available to check. UNRESOLVED: differs from peptides.py at V F1 (1.332 here, -1.332 in peptides.py), P F6 (-1.008 vs -1.108); check against the primary paper." + }, + "blosum_indices": { + "name": "blosum_indices", + "values": { + "A": [ + 0.077, + -0.916, + 0.526, + 0.004, + 0.24, + 0.19, + 0.656, + -0.047, + 1.357, + 0.333 + ], + "C": [ + -1.084, + -1.112, + 1.562, + 0.814, + 1.828, + -1.048, + -0.742, + 0.379, + -0.121, + -0.102 + ], + "D": [ + 1.551, + 0.005, + 0.323, + 0.493, + -0.991, + 0.01, + -1.615, + 0.526, + -0.15, + -0.282 + ], + "E": [ + 1.477, + 0.229, + -0.67, + -0.355, + -0.284, + -0.075, + -1.014, + 0.363, + 0.769, + 0.298 + ], + "F": [ + -1.619, + 1.007, + -0.311, + 0.623, + -0.549, + 0.29, + -0.021, + 0.098, + 0.433, + -1.288 + ], + "G": [ + 0.849, + 0.174, + 1.726, + 0.093, + -0.548, + 1.186, + 1.213, + 0.874, + 0.009, + 0.242 + ], + "H": [ + 0.716, + 1.548, + -0.802, + 1.547, + 0.35, + -0.785, + 0.655, + -0.076, + -0.186, + 0.99 + ], + "I": [ + -1.462, + -1.126, + -0.761, + 0.382, + -0.599, + 0.276, + -0.132, + 0.198, + -0.216, + 0.207 + ], + "K": [ + 1.135, + -0.039, + -0.802, + -0.849, + 0.819, + 0.097, + 0.213, + 0.129, + 0.176, + -0.85 + ], + "L": [ + -1.406, + -0.856, + -0.879, + -0.172, + 0.032, + 0.344, + 0.109, + 0.146, + -0.436, + -0.021 + ], + "M": [ + -0.963, + -0.585, + -0.972, + -0.528, + 0.236, + 0.365, + 0.062, + 0.208, + -0.56, + 0.361 + ], + "N": [ + 1.511, + 0.215, + -0.046, + 1.009, + 0.12, + 0.834, + -0.033, + -0.57, + -1.2, + -0.139 + ], + "P": [ + 0.883, + -0.675, + 0.382, + -0.869, + -1.243, + -2.023, + 0.845, + -0.352, + -0.421, + -0.298 + ], + "Q": [ + 1.094, + 0.296, + -0.871, + -0.718, + 0.5, + -0.08, + -0.442, + 0.202, + 0.384, + 0.667 + ], + "R": [ + 1.014, + 0.189, + -0.86, + -0.609, + 1.277, + 0.195, + 0.661, + 0.175, + -0.219, + -0.52 + ], + "S": [ + 0.844, + -0.448, + 0.423, + 0.317, + 0.2, + 0.541, + 0.009, + -0.797, + 0.624, + -0.129 + ], + "T": [ + 0.188, + -0.733, + 0.178, + -0.012, + 0.022, + 0.378, + -0.304, + -1.958, + 0.149, + 0.063 + ], + "V": [ + -1.127, + -1.227, + -0.633, + 0.064, + -0.596, + 0.158, + 0.014, + 0.016, + 0.251, + 0.607 + ], + "W": [ + -1.577, + 2.281, + 1.166, + -1.61, + 0.122, + 0.239, + -0.542, + -0.398, + -0.349, + 0.499 + ], + "Y": [ + -1.142, + 1.74, + -0.582, + 0.747, + -0.119, + -0.475, + 0.241, + -0.251, + 0.713, + -0.251 + ] + }, + "reference": "Georgiev A.G. (2009). Interpretable numerical descriptors of amino acid space. J Comput Biol, 16(5), 703-723.", + "doi": "10.1089/cmb.2008.0173", + "source_note": "Transcribed 2026-10-12 programmatically from van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41. Additional file 2 (CC-BY Pipeline Pilot component 'Protein Descriptor (Published)'), which embeds the published table; block appending properties B1..B10. Secondary source: the primary paper is paywalled and was not available to check. Cross-checked: agrees with peptides.py (2 dp) after rounding." + }, + "protfp": { + "name": "protfp", + "values": { + "A": [ + -0.1, + -4.94, + -2.13, + 1.7, + -0.39, + 1.06, + -1.39, + 0.97 + ], + "C": [ + 4.62, + -3.54, + 1.5, + -1.26, + 3.27, + -0.34, + -0.47, + -0.23 + ], + "D": [ + -6.61, + 0.94, + -3.04, + -4.58, + 0.48, + -1.31, + 0.1, + 0.94 + ], + "E": [ + -5.1, + 2.2, + -3.59, + -2.26, + -2.14, + 1.35, + -0.45, + -1.31 + ], + "F": [ + 6.76, + 0.88, + 0.89, + -1.12, + -0.49, + -0.55, + -0.87, + 1.05 + ], + "G": [ + -5.7, + -8.72, + 4.18, + -1.35, + -0.31, + 2.91, + 0.32, + -0.11 + ], + "H": [ + 0.17, + 2.14, + 1.2, + 0.71, + 1.16, + -0.38, + -1.85, + -2.79 + ], + "I": [ + 6.58, + -1.73, + -2.49, + 1.09, + -0.34, + -0.28, + 1.97, + -0.92 + ], + "K": [ + -4.99, + 5.0, + 0.7, + 3.0, + -1.23, + 1.41, + 0.19, + 0.87 + ], + "L": [ + 5.76, + -1.33, + -1.71, + 0.63, + -1.7, + 0.71, + -0.05, + -0.51 + ], + "M": [ + 5.11, + 0.19, + -1.02, + 0.15, + 0.13, + -0.3, + -2.95, + 0.5 + ], + "N": [ + -4.88, + 0.81, + 0.14, + -0.14, + 1.23, + -0.65, + 1.02, + -1.94 + ], + "P": [ + -3.82, + -2.31, + 3.45, + 1.0, + -3.22, + -3.54, + -0.36, + -0.3 + ], + "Q": [ + -3.95, + 2.88, + -0.83, + 0.52, + 0.9, + 0.55, + -0.08, + 0.64 + ], + "R": [ + -2.79, + 6.6, + 1.21, + 2.07, + 1.67, + 0.76, + 0.0, + 0.32 + ], + "S": [ + -4.57, + -2.55, + -0.67, + 1.11, + 0.99, + -1.02, + 0.11, + 0.65 + ], + "T": [ + -2.0, + -1.77, + -0.7, + 1.02, + 1.06, + -1.2, + 0.74, + 1.65 + ], + "V": [ + 5.04, + -2.9, + -2.29, + 1.38, + 0.06, + 0.08, + 1.79, + -0.38 + ], + "W": [ + 7.33, + 4.55, + 2.77, + -2.41, + -1.08, + 1.04, + 0.23, + 0.59 + ], + "Y": [ + 3.14, + 3.59, + 2.45, + -1.27, + -0.06, + -0.29, + 1.99, + 0.3 + ] + }, + "reference": "van Westen G.J.P. et al. (2013). Benchmarking of protein descriptor sets in proteochemometric modeling (part 1): comparative study of 13 amino acid descriptor sets. J Cheminform, 5, 41.", + "doi": "10.1186/1758-2946-5-41", + "source_note": "ProtFP (PCA, 8 components) from Additional file 2 of the primary paper (block appending PCA1..PCA8); transcribed 2026-10-12. Cross-checked: identical to peptides.py." + }, + "atchley_factors": { + "name": "atchley_factors", + "values": { + "A": [ + -0.591, + -1.302, + -0.733, + 1.57, + -0.146 + ], + "C": [ + -1.343, + 0.465, + -0.862, + -1.02, + -0.255 + ], + "D": [ + 1.05, + 0.302, + -3.656, + -0.259, + -3.242 + ], + "E": [ + 1.357, + -1.453, + 1.477, + 0.113, + -0.837 + ], + "F": [ + -1.006, + -0.59, + 1.891, + -0.397, + 0.412 + ], + "G": [ + -0.384, + 1.652, + 1.33, + 1.045, + 2.064 + ], + "H": [ + 0.336, + -0.417, + -1.673, + -1.474, + -0.078 + ], + "I": [ + -1.239, + -0.547, + 2.131, + 0.393, + 0.816 + ], + "K": [ + 1.831, + -0.561, + 0.533, + -0.277, + 1.648 + ], + "L": [ + -1.019, + -0.987, + -1.505, + 1.266, + -0.912 + ], + "M": [ + -0.663, + -1.524, + 2.219, + -1.005, + 1.212 + ], + "N": [ + 0.945, + 0.828, + 1.299, + -0.169, + 0.933 + ], + "P": [ + 0.189, + 2.081, + -1.628, + 0.421, + -1.392 + ], + "Q": [ + 0.931, + -0.179, + -3.005, + -0.503, + -1.853 + ], + "R": [ + 1.538, + -0.055, + 1.502, + 0.44, + 2.897 + ], + "S": [ + -0.228, + 1.399, + -4.76, + 0.67, + -2.647 + ], + "T": [ + -0.032, + 0.326, + 2.213, + 0.908, + 1.313 + ], + "V": [ + -1.337, + -0.279, + -0.544, + 1.242, + -1.262 + ], + "W": [ + -0.595, + 0.009, + 0.672, + -2.128, + -0.184 + ], + "Y": [ + 0.26, + 0.83, + 3.097, + -0.838, + 1.512 + ] + }, + "reference": "Atchley W.R. et al. (2005). Solving the protein sequence metric problem. PNAS, 102(18), 6395-6400.", + "doi": "10.1073/pnas.0408677102", + "source_note": "Table 2 of the primary paper (PMC1088356), transcribed programmatically 2026-10-12. Cross-checked: agrees with peptides.py to the 3 dp published." } } \ No newline at end of file diff --git a/protpy/scales.py b/protpy/scales.py new file mode 100644 index 0000000..f568a35 --- /dev/null +++ b/protpy/scales.py @@ -0,0 +1,259 @@ +################################################################################ +############# Amino Acid Scales ############## +################################################################################ + +from __future__ import annotations + +""" +Amino-acid factor scales (z-scales, VHSE, Atchley factors, T-/ST-scales, +ProtFP, FASGAI, BLOSUM indices, MS-WHIM) and the auto/cross-covariance and +physicochemical distance transformations built on them. + +References +========== +[1] Sandberg, M. et al. (1998). New chemical descriptors relevant for the design + of biologically active peptides. A multivariate characterization of 87 amino + acids. J Med Chem, 41(14), 2481-2491. https://doi.org/10.1021/jm9700575 +[2] Mei, H. et al. (2005). A new set of amino acid descriptors and its application + in peptide QSARs. Biopolymers, 80(6), 775-786. https://doi.org/10.1002/bip.20296 +[3] Atchley, W. R. et al. (2005). Solving the protein sequence metric problem. + PNAS, 102(18), 6395-6400. https://doi.org/10.1073/pnas.0408677102 +[4] Tian, F., Zhou, P., & Li, Z. (2007). T-scale as a novel vector of topological + descriptors for amino acids and its application in QSARs of peptides. J Mol + Struct, 830, 106-115. https://doi.org/10.1016/j.molstruc.2006.07.004 +[5] Yang, L. et al. (2010). ST-scale as a novel amino acid descriptor and its + application in QSAM of peptides and analogues. Amino Acids, 38(3), 805-816. + https://doi.org/10.1007/s00726-009-0287-y +[6] van Westen, G. J. P. et al. (2013). Benchmarking of protein descriptor sets in + proteochemometric modeling (part 1): comparative study of 13 amino acid + descriptor sets. J Cheminform, 5, 41. https://doi.org/10.1186/1758-2946-5-41 +[7] Liang, G., & Li, Z. (2007). Factor analysis scale of generalized amino acid + information as the source of a new set of descriptors for elucidating the + structure and activity relationships of cationic antimicrobial peptides. QSAR + Comb Sci, 26(6), 754-763. https://doi.org/10.1002/qsar.200630145 +[8] Georgiev, A. G. (2009). Interpretable numerical descriptors of amino acid + space. J Comput Biol, 16(5), 703-723. https://doi.org/10.1089/cmb.2008.0173 +[9] Zaliani, A., & Gancia, E. (1999). MS-WHIM scores for amino acids: a new + 3D-description for peptide QSAR and QSPR studies. J Chem Inf Comput Sci, + 39(3), 525-533. https://doi.org/10.1021/ci980211b +[10] Wold, S. et al. (1993). DNA and peptide sequences and chemical processes + multivariately modelled by principal component analysis and partial + least-squares projections to latent structures. Anal Chim Acta, 277(2), + 239-253. https://doi.org/10.1016/0003-2670(93)80437-P +[11] Lapinsh, M. et al. (2002). Development of proteo-chemometrics: a novel + technology for the analysis of drug-receptor interactions. Mol Pharmacol, + 61(6), 1465. https://doi.org/10.1124/mol.61.6.1465 +[12] Liu, B. et al. (2012). Using amino acid physicochemical distance + transformation for fast protein remote homology detection. PLoS ONE, 7(9), + e46633. https://doi.org/10.1371/journal.pone.0046633 + +The values of [1, 2, 4, 5, 7, 8, 9] were transcribed from the CC-BY descriptor +component published with [6]; see protpy/data/aa_scales.json for provenance. +""" + +import itertools + +import numpy as np +import pandas as pd + +from ._constants import _validate_sequence, _validate_int +from ._helpers import _property_matrix, _read_json, _to_df + +__all__ = [ + "aa_scale_descriptors", "z_scales_5", "vhse", "atchley_factors", "t_scales", "st_scales", + "protfp", "fasgai", "blosum_indices", "ms_whim", "auto_cross_covariance", + "physicochemical_distance_transformation", +] + +#factor-scale families: name -> column abbreviation +factor_scales_ = { + "z_scales_5": "Z5", + "vhse": "VHSE", + "atchley_factors": "AF", + "t_scales": "T", + "st_scales": "ST", + "protfp": "ProtFP", + "fasgai": "FASGAI", + "blosum_indices": "BLOSUM", + "ms_whim": "MSWHIM", +} + +#default AAindex properties for PDT (same 8 as the autocorrelation descriptors) +_default_pdt_properties = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"] + +################################## Factor Scale Descriptors ############################### + +def aa_scale_descriptors(sequence: str, scale: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate the mean of an amino-acid factor scale over a protein sequence. Each + residue is replaced by its d-dimensional scale vector and each dimension is + averaged: + + S_j = (1/L) sum_i z_j(s_i), j = 1..d + + With ``domains > 1`` the sequence is split into ``domains`` near-equal + contiguous segments and the per-segment means are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str + factor-scale family: "z_scales_5", "vhse", "atchley_factors", "t_scales", + "st_scales", "protfp", "fasgai", "blosum_indices" or "ms_whim". + :domains: int (default=1) + number of segments, at most the sequence length. + + Returns + ======= + :scale_df: pd.DataFrame + pandas DataFrame of shape 1 x d·domains, columns ``{ABBR}_{j}`` or + ``{ABBR}_{j}_d{k}`` when ``domains > 1``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + if scale not in factor_scales_: + raise ValueError(f"Invalid scale '{scale}'. Valid scales are: {list(factor_scales_)}.") + domains = _validate_int("domains", domains, 1) + if domains > len(sequence): + raise ValueError(f"domains ({domains}) cannot exceed the sequence length ({len(sequence)}).") + + abbr = factor_scales_[scale] + matrix = _property_matrix(sequence, scale) + values, columns = [], [] + for k, segment in enumerate(np.array_split(matrix, domains), start=1): + values += list(segment.mean(axis=0)) + suffix = f"_d{k}" if domains > 1 else "" + columns += [f"{abbr}_{j}{suffix}" for j in range(1, matrix.shape[1] + 1)] + return _to_df(values, columns) + + +def _scale_wrapper(scale: str, ref: str, dim: int): + def wrapper(sequence: str, domains: int = 1) -> pd.DataFrame: + return aa_scale_descriptors(sequence, scale, domains) + wrapper.__name__ = scale + wrapper.__qualname__ = scale + wrapper.__doc__ = f""" + Calculate the mean {scale} factor-scale descriptor of a protein sequence {ref}. + See ``aa_scale_descriptors`` for the formula. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments whose means are concatenated. + + Returns + ======= + :scale_df: pd.DataFrame + pandas DataFrame of shape 1 x {dim}·domains, columns ``{factor_scales_[scale]}_{{j}}``. + """ + return wrapper + +z_scales_5 = _scale_wrapper("z_scales_5", "(Sandberg 5 z-scales) [1]", 5) +vhse = _scale_wrapper("vhse", "(principal components score Vectors of Hydrophobic, Steric and Electronic properties) [2]", 8) +atchley_factors = _scale_wrapper("atchley_factors", "(Atchley factors I-V) [3]", 5) +t_scales = _scale_wrapper("t_scales", "(topological T-scales) [4]", 5) +st_scales = _scale_wrapper("st_scales", "(structural topology ST-scales) [5]", 8) +protfp = _scale_wrapper("protfp", "(ProtFP PCA, 8 components) [6]", 8) +fasgai = _scale_wrapper("fasgai", "(factor analysis scales of generalized amino acid information) [7]", 6) +blosum_indices = _scale_wrapper("blosum_indices", "(BLOSUM62-derived indices) [8]", 10) +ms_whim = _scale_wrapper("ms_whim", "(MS-WHIM scores) [9]", 3) + +############################## Auto/Cross Covariance ###################################### + +def auto_cross_covariance(sequence: str, scale: str | list[str] = "z_scales_5", lag: int = 5, + mode: str = "acc") -> pd.DataFrame: + """ + Calculate the auto- and cross-covariance (ACC) transform of an amino-acid + scale [10, 11]. For lag l and scale dimensions j, k: + + ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) + + ``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto + covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a + protpy factor-scale family (used as published) or an AAindex1 code / list + of codes, which are standardised over the 20 amino acids. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str | list[str] (default="z_scales_5") + factor-scale family, single-value scale name or AAindex1 code(s). + :lag: int (default=5) + maximum lag, less than the sequence length. + :mode: str (default="acc") + "acc", "ac" or "cc". + + Returns + ======= + :acc_df: pd.DataFrame + pandas DataFrame of shape 1 x d²·lag ("acc"), d·lag ("ac") or + d(d-1)·lag ("cc"), columns ``ACC_{j}_{k}_lag{l}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + lag = _validate_int("lag", lag, 1) + if lag >= len(sequence): + raise ValueError(f"lag ({lag}) must be less than the sequence length ({len(sequence)}).") + if mode not in ("acc", "ac", "cc"): + raise ValueError(f"Invalid mode '{mode}'. Valid modes are: ['acc', 'ac', 'cc'].") + + #published scale tables are used as-is; raw AAindex properties are standardised + standardize = not (isinstance(scale, str) and scale in _read_json("aa_scales.json")) + z = _property_matrix(sequence, scale, standardize=standardize) + d = z.shape[1] + pairs = [(j, k) for j, k in itertools.product(range(d), repeat=2) + if mode == "acc" or (mode == "ac") == (j == k)] + values, columns = [], [] + for l in range(1, lag + 1): + #cov[j, k] = mean over i of z[i, j] * z[i + l, k] + cov = z[:-l].T @ z[l:] / (len(sequence) - l) + values += [cov[j, k] for j, k in pairs] + columns += [f"ACC_{j + 1}_{k + 1}_lag{l}" for j, k in pairs] + return _to_df(values, columns) + +########################## Physicochemical Distance Transformation ######################## + +def physicochemical_distance_transformation(sequence: str, lag: int = 3, + properties: list[str] | None = None) -> pd.DataFrame: + """ + Calculate the Physicochemical Distance Transformation (PDT) [12]. For each + AAindex property P_j (standardised over the 20 amino acids) and lag l: + + PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) + + Parameters + ========== + :sequence: str + protein sequence. + :lag: int (default=3) + maximum lag, less than the sequence length. + :properties: list[str] | None (default=None) + AAindex1 accession codes; defaults to the 8 properties used by the + autocorrelation descriptors. + + Returns + ======= + :pdt_df: pd.DataFrame + pandas DataFrame of shape 1 x len(properties)·lag, columns ``PDT_{code}_{l}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + lag = _validate_int("lag", lag, 1) + if lag >= len(sequence): + raise ValueError(f"lag ({lag}) must be less than the sequence length ({len(sequence)}).") + properties = list(properties) if properties is not None else _default_pdt_properties + if not properties: + raise ValueError("At least one property must be given.") + + p = _property_matrix(sequence, properties, standardize=True) + values, columns = [], [] + for j, code in enumerate(properties): + for l in range(1, lag + 1): + values.append(np.mean((p[:-l, j] - p[l:, j]) ** 2)) + columns.append(f"PDT_{code}_{l}") + return _to_df(values, columns) diff --git a/protpy/utils.py b/protpy/utils.py index 42b4b52..6de41cd 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -798,6 +798,185 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "z_scales_5": { + "description": "Mean of the Sandberg 5 z-scales over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "Z5", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 5·domains (default 1 x 5)", + "speed": "Fast", + "reference": "Sandberg et al. (1998). J. Med. Chem., 41(14), 2481-2491.", + "doi": "10.1021/jm9700575", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "vhse": { + "description": "Mean of the VHSE (hydrophobic, steric, electronic) scales over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "VHSE", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 8·domains (default 1 x 8)", + "speed": "Fast", + "reference": "Mei et al. (2005). Biopolymers, 80(6), 775-786.", + "doi": "10.1002/bip.20296", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "atchley_factors": { + "description": "Mean of the Atchley factors I-V over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "AF", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 5·domains (default 1 x 5)", + "speed": "Fast", + "reference": "Atchley et al. (2005). PNAS, 102(18), 6395-6400.", + "doi": "10.1073/pnas.0408677102", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "t_scales": { + "description": "Mean of the topological T-scales over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "T", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 5·domains (default 1 x 5)", + "speed": "Fast", + "reference": "Tian, Zhou & Li (2007). J. Mol. Struct., 830, 106-115.", + "doi": "10.1016/j.molstruc.2006.07.004", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "st_scales": { + "description": "Mean of the structural topology ST-scales over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "ST", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 8·domains (default 1 x 8)", + "speed": "Fast", + "reference": "Yang et al. (2010). Amino Acids, 38(3), 805-816.", + "doi": "10.1007/s00726-009-0287-y", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "protfp": { + "description": "Mean of the ProtFP (PCA, 8 components) over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "ProtFP", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 8·domains (default 1 x 8)", + "speed": "Fast", + "reference": "van Westen et al. (2013). J. Cheminform., 5, 41.", + "doi": "10.1186/1758-2946-5-41", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "fasgai": { + "description": "Mean of the FASGAI factor scales over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "FASGAI", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 6·domains (default 1 x 6)", + "speed": "Fast", + "reference": "Liang & Li (2007). QSAR Comb. Sci., 26(6), 754-763.", + "doi": "10.1002/qsar.200630145", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "blosum_indices": { + "description": "Mean of the BLOSUM62-derived indices over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "BLOSUM", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 10·domains (default 1 x 10)", + "speed": "Fast", + "reference": "Georgiev (2009). J. Comput. Biol., 16(5), 703-723.", + "doi": "10.1089/cmb.2008.0173", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "ms_whim": { + "description": "Mean of the MS-WHIM scores over the sequence (optionally per domain).", + "module": "scales", + "category": "Scales", + "abbreviation": "MSWHIM", + "parameters": { + "sequence": "str — protein sequence", + "domains": "int (default=1) — number of near-equal segments", + }, + "output_shape": "1 x 3·domains (default 1 x 3)", + "speed": "Fast", + "reference": "Zaliani & Gancia (1999). J. Chem. Inf. Comput. Sci., 39(3), 525-533.", + "doi": "10.1021/ci980211b", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "auto_cross_covariance": { + "description": "Auto/cross-covariance of an amino-acid scale: sum z_j(i) z_k(i+l) / (L-l).", + "module": "scales", + "category": "Scales", + "abbreviation": "ACC", + "parameters": { + "sequence": "str — protein sequence", + "scale": "str | list[str] (default='z_scales_5') — factor scale or AAindex code(s)", + "lag": "int (default=5) — maximum lag", + "mode": "str (default='acc') — 'acc', 'ac' or 'cc'", + }, + "output_shape": "1 x d²·lag (default 1 x 125)", + "speed": "Fast", + "reference": "Wold et al. (1993). Anal. Chim. Acta, 277(2), 239-253.", + "doi": "10.1016/0003-2670(93)80437-P", + "descriptor_sets": ["all"], + "in_default_set": False, + }, + "physicochemical_distance_transformation": { + "description": "Physicochemical distance transformation: mean squared difference of standardised AAindex properties at lag l.", + "module": "scales", + "category": "Scales", + "abbreviation": "PDT", + "parameters": { + "sequence": "str — protein sequence", + "lag": "int (default=3) — maximum lag", + "properties": "list[str] | None — AAindex codes (default: 8 autocorrelation properties)", + }, + "output_shape": "1 x len(properties)·lag (default 1 x 24)", + "speed": "Fast", + "reference": "Liu et al. (2012). PLoS ONE, 7(9), e46633.", + "doi": "10.1371/journal.pone.0046633", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_scales.py b/tests/test_scales.py new file mode 100644 index 0000000..89825cc --- /dev/null +++ b/tests/test_scales.py @@ -0,0 +1,103 @@ +########################################################################### +############### protPy - Scales Module Tests (Phase 2) ############# +########################################################################### + +import unittest + +import numpy as np + +import protpy +from protpy._helpers import _read_json +from protpy.scales import factor_scales_ + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" +DIMS = {"z_scales_5": 5, "vhse": 8, "atchley_factors": 5, "t_scales": 5, "st_scales": 8, + "protfp": 8, "fasgai": 6, "blosum_indices": 10, "ms_whim": 3} + + +class ProtpyScalesTests(unittest.TestCase): + + def test_tables(self): + tables = _read_json("aa_scales.json") + for name, d in DIMS.items(): + values = tables[name]["values"] + self.assertEqual(sorted(values), protpy.amino_acids, name) + self.assertTrue(all(len(v) == d for v in values.values()), name) + + def test_wrappers(self): + tables = _read_json("aa_scales.json") + for name, d in DIMS.items(): + func = getattr(protpy, name) + df = func(SEQ) + abbr = factor_scales_[name] + self.assertEqual(df.shape, (1, d), name) + self.assertEqual(list(df.columns), [f"{abbr}_{j}" for j in range(1, d + 1)]) + self.assertTrue(all(dt == np.float64 for dt in df.dtypes)) + #poly-A gives A's row + np.testing.assert_allclose(func("AAAAA").values[0], np.round(tables[name]["values"]["A"], 3)) + #domains + df3 = func(SEQ[:30], domains=3) + self.assertEqual(df3.shape, (1, 3 * d)) + self.assertEqual(df3.columns[d], f"{abbr}_1_d2") + #mean of the 3 equal domains is the whole-sequence mean + np.testing.assert_allclose(df3.values[0].reshape(3, d).mean(axis=0), func(SEQ[:30]).values[0], atol=2e-3) + + def test_invalid(self): + for func in (protpy.vhse, protpy.auto_cross_covariance, protpy.physicochemical_distance_transformation): + with self.assertRaises(TypeError): + func(1) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("AXC") + with self.assertRaises(ValueError): + protpy.vhse("ACD", domains=4) + with self.assertRaises(ValueError): + protpy.vhse("ACD", domains=0) + with self.assertRaises(ValueError): + protpy.aa_scale_descriptors(SEQ, "kidera") + + def test_auto_cross_covariance(self): + df = protpy.auto_cross_covariance(SEQ) + self.assertEqual(df.shape, (1, 25 * 5)) + self.assertEqual(protpy.auto_cross_covariance(SEQ, mode="ac").shape, (1, 25)) + self.assertEqual(protpy.auto_cross_covariance(SEQ, mode="cc").shape, (1, 100)) + #brute force + z = _read_json("aa_scales.json")["z_scales_5"]["values"] + L, lag, j, k = len(SEQ), 3, 1, 4 + ref = sum(z[SEQ[i]][j] * z[SEQ[i + lag]][k] for i in range(L - lag)) / (L - lag) + self.assertAlmostEqual(df[f"ACC_{j + 1}_{k + 1}_lag{lag}"].iloc[0], round(ref, 3)) + #AAindex codes are accepted and standardised + self.assertEqual(protpy.auto_cross_covariance(SEQ, scale=["KYTJ820101", "HOPT810101"], lag=2).shape, (1, 8)) + with self.assertRaises(ValueError): + protpy.auto_cross_covariance("ACD", lag=3) + with self.assertRaises(ValueError): + protpy.auto_cross_covariance(SEQ, mode="x") + + def test_pdt(self): + df = protpy.physicochemical_distance_transformation(SEQ) + self.assertEqual(df.shape, (1, 24)) + self.assertEqual(df.columns[0], "PDT_CIDH920105_1") + self.assertTrue((protpy.physicochemical_distance_transformation("A" * 10).values == 0).all()) + self.assertTrue((df.values >= 0).all()) + with self.assertRaises(ValueError): + protpy.physicochemical_distance_transformation("AC", lag=2) + with self.assertRaises(ValueError): + protpy.physicochemical_distance_transformation(SEQ, properties=[]) + + def test_cross_check_peptides(self): + """Optional cross-check against peptides.py (GPL; test-only, never imported by protpy).""" + try: + import peptides.tables as T + except ImportError: + self.skipTest("peptides not installed") + tables = _read_json("aa_scales.json") + for name, ref in (("ms_whim", T.MSWHIM), ("t_scales", T.T_SCALES), ("protfp", T.PROTFP)): + keys = sorted(ref, key=lambda k: int("".join(c for c in k if c.isdigit()))) + for i, key in enumerate(keys): + for aa, row in tables[name]["values"].items(): + self.assertAlmostEqual(row[i], ref[key][aa], places=6, msg=f"{name} {key} {aa}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index 160c0ef..a00c201 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -79,7 +79,8 @@ class ProtpyUtilsTests(unittest.TestCase): "abbreviation", "parameters", "output_shape", "speed", "reference", } - VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order"} + VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order", + "Scales"} VALID_SPEEDS = {"Fast", "Moderate", "Slow"} # ── Registry integrity ──────────────────────────────────────────────────────── From 67e3ea08dc3169128563a9474e2da9088fdca6cf Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:24:23 +0100 Subject: [PATCH 06/17] v1.5.0 phase 3: charge fractions, NCPR, kappa, SCD, charge density; pKa scales for pI/charge --- planning/BLOCKED.md | 3 + protpy/__init__.py | 2 + protpy/composition.py | 51 +++++-- protpy/data/residue_tables.json | 65 +++++++++ protpy/physicochemical.py | 249 ++++++++++++++++++++++++++++++++ protpy/utils.py | 79 ++++++++++ tests/test_physicochemical.py | 112 ++++++++++++++ tests/test_utils.py | 2 +- 8 files changed, 551 insertions(+), 12 deletions(-) create mode 100644 protpy/physicochemical.py create mode 100644 tests/test_physicochemical.py diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md index de836d6..633fa47 100644 --- a/planning/BLOCKED.md +++ b/planning/BLOCKED.md @@ -9,6 +9,7 @@ Each entry names the table that is missing and where it would have to come from. | 9 | `z_scales` (Hellberg z1–z3) | Hellberg et al. 1987 Table (J Med Chem, 10.1021/jm00390a003) | Paywalled; no permissively licensed reproduction found. van Westen 2013's "Z-scales (3)" is a subset of Sandberg's 5, not Hellberg's, so it can't be substituted. `z_scales_5` is shipped. | | 12 | `kidera_factors` | Kidera et al. 1985 10-factor table (J Protein Chem, 10.1007/BF01025492) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | | 20 | `cruciani_properties` | Cruciani et al. 2004 3-property table (J Chemometrics, 10.1002/cem.856) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | +| 26 | `omega` | The exact Ω definition: binary alphabet encoding, blob size(s) and δ_max construction | Martin et al. 2016 only says Ω is "analogous to κ" and puts the calculation in its Supporting Information. The SI isn't open access (Europe PMC has no supplementary bundle; ACS and PMC downloads are behind browser challenges), so the definition and a reproducible reference value couldn't be checked. | ## Shipped with caveats @@ -20,4 +21,6 @@ Each entry names the table that is missing and where it would have to come from. Check these against the primary tables before release. - **`distance_pair`.** The plan specified G + G²·(rng+1) outputs over distances 0..rng. Liu 2014 (Eqs. 5–12) defines d = 1..rng, giving G + G²·rng (602 for cp14, d = 3, as the paper states), and that is what is implemented. The paper's printed cp(14) lists E twice; the stray E in the first cluster is dropped, the only 14-cluster reading. +- **`kappa`.** δ_max is found by enumerating the segregated arrangements (positive block, negative block, neutral residues split over the three gaps), as the plan specifies, because the Das & Pappu SI is behind a browser challenge. It reproduces the published κ of the patterned (Glu-Lys)25 variants sv1, sv2, sv7, sv8, sv19 and sv30 to within 0.002. The irregular variants could not be read reliably from the Fig. 1 image and are not tested. +- **`isoelectric_point(pka_scale="emboss")`.** Uses EMBOSS 6.6.0 `Epk.dat`, whose N-terminal pKa differs from the 8.6 that Kozlowski 2016 Table 1 attributes to EMBOSS. - **`atomic_composition`.** By default it matches Pfeature (no water removed); `subtract_water=True` gives the true peptide formula. Pfeature's W and Y formulas each have one H too many, and protpy uses the correct formulas. diff --git a/protpy/__init__.py b/protpy/__init__.py index b898092..fc0fd4e 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -4,6 +4,7 @@ from .ctd import * from .sequence_order import * from .scales import * +from .physicochemical import * from .utils import get_descriptor_info, list_descriptors, print_descriptor_info from .encode import all_descriptors, encode_dataset, read_sequences_from_file @@ -45,4 +46,5 @@ "cksaap", "cksaagp", "dde", "asdc", "distance_pair", "natural_vector", "atomic_composition", "z_scales_5", "vhse", "atchley_factors", "t_scales", "st_scales", "protfp", "fasgai", "blosum_indices", "ms_whim", "auto_cross_covariance", "physicochemical_distance_transformation", + "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", ] \ No newline at end of file diff --git a/protpy/composition.py b/protpy/composition.py index ac1ac12..4e6f9a9 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -494,16 +494,32 @@ def instability_index(sequence: str) -> pd.DataFrame: "N_term": 8.0, "C_term": 3.1 } -def isoelectric_point(sequence: str) -> pd.DataFrame: +def _pka_set(pka_scale: str) -> dict: + """Return the pKa set named ``pka_scale`` from residue_tables.json.""" + sets = _load_json_table("residue_tables.json", "pka_sets")["values"] + if pka_scale not in sets: + raise ValueError(f"Invalid pka_scale '{pka_scale}'. Valid scales are: {list(sets)}.") + return sets[pka_scale] + +def isoelectric_point(sequence: str, pka_scale: str = "protpy") -> pd.DataFrame: """ Calculate the theoretical Isoelectric Point (pI) of a protein sequence. pI is the pH at which the net charge of the protein is zero. It is computed by bisecting the pH range 0-14 until positive and negative charges balance [11]. + ``pka_scale`` selects the pKa values: "protpy" (default, unchanged), "emboss" + (EMBOSS Epk.dat), "bjellqvist" (Bjellqvist 1993/1994, including the + residue-specific N-terminal pKa values for A, M, S, P, T, V, E and C-terminal + values for D and E) or "ipc_protein" (Kozlowski 2016). The same charge model + (K, R, H, N-terminus positive; D, E, C, Y, C-terminus negative) is used for + every scale. + Parameters ========== :sequence: str protein sequence. + :pka_scale: str (default="protpy") + pKa value set: "protpy", "emboss", "bjellqvist" or "ipc_protein". Returns ======= @@ -513,19 +529,27 @@ def isoelectric_point(sequence: str) -> pd.DataFrame: #validate input protein sequence sequence = _validate_sequence(sequence) + if pka_scale == "protpy": + pka = pka_values_ + else: + pka = dict(_pka_set(pka_scale)) + #residue-specific terminal pKa values (Bjellqvist) + pka["N_term"] = pka.get("N_term_by_residue", {}).get(sequence[0], pka["N_term"]) + pka["C_term"] = pka.get("C_term_by_residue", {}).get(sequence[-1], pka["C_term"]) + #count ionisable residues counts = {aa: sequence.count(aa) for aa in "DEHCYKR"} def _net_charge(ph: float) -> float: - pos = (counts["K"] / (1 + 10 ** (ph - pka_values_["K"])) + - counts["R"] / (1 + 10 ** (ph - pka_values_["R"])) + - counts["H"] / (1 + 10 ** (ph - pka_values_["H"])) + - 1.0 / (1 + 10 ** (ph - pka_values_["N_term"]))) - neg = (counts["D"] / (1 + 10 ** (pka_values_["D"] - ph)) + - counts["E"] / (1 + 10 ** (pka_values_["E"] - ph)) + - counts["C"] / (1 + 10 ** (pka_values_["C"] - ph)) + - counts["Y"] / (1 + 10 ** (pka_values_["Y"] - ph)) + - 1.0 / (1 + 10 ** (pka_values_["C_term"] - ph))) + pos = (counts["K"] / (1 + 10 ** (ph - pka["K"])) + + counts["R"] / (1 + 10 ** (ph - pka["R"])) + + counts["H"] / (1 + 10 ** (ph - pka["H"])) + + 1.0 / (1 + 10 ** (ph - pka["N_term"]))) + neg = (counts["D"] / (1 + 10 ** (pka["D"] - ph)) + + counts["E"] / (1 + 10 ** (pka["E"] - ph)) + + counts["C"] / (1 + 10 ** (pka["C"] - ph)) + + counts["Y"] / (1 + 10 ** (pka["Y"] - ph)) + + 1.0 / (1 + 10 ** (pka["C_term"] - ph))) return pos - neg #net charge decreases monotonically with pH, so bisect over the full 0-14 range @@ -582,7 +606,7 @@ def molecular_weight(sequence: str) -> pd.DataFrame: ############################## Charge Distribution ######################################## -def charge_distribution(sequence: str, ph: float = 7.4) -> pd.DataFrame: +def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy") -> pd.DataFrame: """ Calculate Charge Distribution of a protein sequence at a given pH. Positive charge is contributed by Lys (K), Arg (R) and His (H); negative charge by @@ -598,6 +622,9 @@ def charge_distribution(sequence: str, ph: float = 7.4) -> pd.DataFrame: protein sequence. :ph: float (default=7.4) pH at which to evaluate charges. + :pka_scale: str (default="protpy") + pKa value set for K, R, H, D and E: "protpy", "emboss", "bjellqvist" or + "ipc_protein" (see ``isoelectric_point``). Termini are not included. Returns ======= @@ -610,6 +637,8 @@ def charge_distribution(sequence: str, ph: float = 7.4) -> pd.DataFrame: #pKa values for charged residues pka_charged_ = {"K": 10.5, "R": 12.5, "H": 6.0, "D": 3.9, "E": 4.1} + if pka_scale != "protpy": + pka_charged_ = {aa: _pka_set(pka_scale)[aa] for aa in pka_charged_} #compute positive charge (K, R, H) using Henderson-Hasselbalch pos = sum(sequence.count(aa) / (1 + 10 ** (ph - pka_charged_[aa])) for aa in ("K", "R", "H")) diff --git a/protpy/data/residue_tables.json b/protpy/data/residue_tables.json index 50ea04f..6140c17 100644 --- a/protpy/data/residue_tables.json +++ b/protpy/data/residue_tables.json @@ -174,5 +174,70 @@ "reference": "Pande A. et al. (2023). Computing wide range of protein/peptide features from their sequence and structure. J Comput Biol, 30(2), 204-222.", "doi": "10.1089/cmb.2022.0241", "source_note": "Standard free amino-acid formulas; cross-checked against Pfeature's Standalone/Data/atom.csv. Pfeature sums free amino-acid formulas without removing water (protpy's default convention). Its table over-counts H by one for W (C11H13N2O2) and Y (C9H12NO3); the correct formulas C11H12N2O2 and C9H11NO3 are used here, so W/Y-containing sequences differ slightly from Pfeature output. Transcribed 2026-10-12." + }, + "pka_sets": { + "name": "pKa sets for isoelectric point / charge calculations", + "values": { + "protpy": { + "D": 3.9, + "E": 4.1, + "H": 6.0, + "C": 8.3, + "Y": 10.1, + "K": 10.5, + "R": 12.5, + "N_term": 8.0, + "C_term": 3.1 + }, + "emboss": { + "N_term": 7.5, + "C_term": 3.6, + "C": 8.5, + "D": 3.9, + "E": 4.1, + "H": 6.5, + "K": 10.8, + "R": 12.5, + "Y": 10.1 + }, + "bjellqvist": { + "N_term": 7.5, + "C_term": 3.55, + "K": 10.0, + "R": 12.0, + "H": 5.98, + "D": 4.05, + "E": 4.45, + "C": 9.0, + "Y": 10.0, + "N_term_by_residue": { + "A": 7.59, + "M": 7.0, + "S": 6.93, + "P": 8.36, + "T": 6.82, + "V": 7.44, + "E": 7.7 + }, + "C_term_by_residue": { + "D": 4.55, + "E": 4.75 + } + }, + "ipc_protein": { + "N_term": 9.094, + "C_term": 2.869, + "C": 7.555, + "D": 3.872, + "E": 4.412, + "H": 5.637, + "K": 9.052, + "R": 11.84, + "Y": 10.85 + } + }, + "reference": "protpy: v1.4 defaults. emboss: Rice P. et al. (2000) EMBOSS, Trends Genet 16(6):276-277. bjellqvist: Bjellqvist B. et al. (1993) Electrophoresis 14:1023-1031 and (1994) Electrophoresis 15:529-539. ipc_protein: Kozlowski L.P. (2016) IPC - Isoelectric Point Calculator. Biol Direct 11:55.", + "doi": "10.1186/s13062-016-0159-9", + "source_note": "Transcribed programmatically 2026-10-12. emboss: EMBOSS 6.6.0 data file emboss/data/Epk.dat (values only; N_term 7.5, whereas Kozlowski 2016 Table 1 lists 8.6 for EMBOSS). ipc_protein: Table 1 of Kozlowski 2016 (PMC5075173). bjellqvist: Biopython IsoelectricPoint.py tables (attribution; BSD-3), cross-checked against the Bjellqvist row of Kozlowski 2016 Table 1; residue-specific terminal values from Bjellqvist 1994 via Biopython." } } \ No newline at end of file diff --git a/protpy/physicochemical.py b/protpy/physicochemical.py new file mode 100644 index 0000000..e3be2c2 --- /dev/null +++ b/protpy/physicochemical.py @@ -0,0 +1,249 @@ +################################################################################ +############# Physicochemical ############## +################################################################################ + +from __future__ import annotations + +""" +Charge and charge-patterning descriptors (FCR, NCPR, kappa, SCD, charge +density). + +Charges follow the Das & Pappu convention: K, R = +1; D, E = -1; H = 0. + +References +========== +[1] Mao, A. H., Crick, S. L., Vitalis, A., Chicoine, C. L., & Pappu, R. V. + (2010). Net charge per residue modulates conformational ensembles of + intrinsically disordered proteins. PNAS, 107(18), 8183-8188. + https://doi.org/10.1073/pnas.0911107107 +[2] Das, R. K., & Pappu, R. V. (2013). Conformations of intrinsically disordered + proteins are influenced by linear sequence distributions of oppositely + charged residues. PNAS, 110(33), 13392-13397. + https://doi.org/10.1073/pnas.1304749110 +[3] Sawle, L., & Ghosh, K. (2015). A theoretical method to compute sequence + dependent configurational properties in charged polymers and proteins. + J Chem Phys, 143(8), 085101. https://doi.org/10.1063/1.4929391 +[4] Müller, A. T., Gabernet, G., Hiss, J. A., & Schneider, G. (2017). modlAMP: + Python for antimicrobial peptides. Bioinformatics, 33(17), 2753-2755. + https://doi.org/10.1093/bioinformatics/btx285 +""" + +import numpy as np +import pandas as pd + +from ._constants import _validate_sequence, _validate_int +from ._helpers import _charge_vector, _to_df +from .composition import charge_distribution, molecular_weight + +__all__ = [ + "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", +] + +################################## Charge Fractions ####################################### + +def charge_fractions(sequence: str) -> pd.DataFrame: + """ + Calculate the charge fractions of a protein sequence [1, 2]: + + FCR = (n+ + n-) / L fraction of charged residues + FPlus = n+ / L, FMinus = n- / L + Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) + + where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :charge_fractions_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``FCR``, ``FPlus``, ``FMinus``, ``Sigma``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + q = _charge_vector(sequence) + f_plus, f_minus = np.mean(q > 0), np.mean(q < 0) + fcr, ncpr_ = f_plus + f_minus, f_plus - f_minus + sigma = ncpr_ ** 2 / fcr if fcr else 0.0 + return _to_df([fcr, f_plus, f_minus, sigma], ["FCR", "FPlus", "FMinus", "Sigma"]) + +#################################### NCPR ################################################# + +def ncpr(sequence: str) -> pd.DataFrame: + """ + Calculate the Net Charge Per Residue (NCPR) of a protein sequence [1]: + + NCPR = (n+ - n-) / L + + with n+ counting K/R and n- counting D/E. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :ncpr_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``NCPR``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + return _to_df([_charge_vector(sequence).mean()], ["NCPR"]) + +#################################### Kappa ################################################ + +def _blob_sigma(pos: np.ndarray, neg: np.ndarray, g: int) -> np.ndarray: + """Charge asymmetry (f+ - f-)^2 / (f+ + f-) of every overlapping blob of size g (rows = candidates).""" + kernel = np.ones(g) + if pos.ndim == 1: + p = np.convolve(pos, kernel, mode="valid") + m = np.convolve(neg, kernel, mode="valid") + else: + #blob counts from prefix sums, one row per candidate sequence + cp = np.concatenate([np.zeros((pos.shape[0], 1)), np.cumsum(pos, axis=1)], axis=1) + cm = np.concatenate([np.zeros((neg.shape[0], 1)), np.cumsum(neg, axis=1)], axis=1) + p, m = cp[:, g:] - cp[:, :-g], cm[:, g:] - cm[:, :-g] + total = p + m + return np.divide((p - m) ** 2, total * g, out=np.zeros_like(total, dtype=float), where=total > 0) + + +def _delta(pos: np.ndarray, neg: np.ndarray, g: int, sigma: float) -> np.ndarray: + return np.mean((_blob_sigma(pos, neg, g) - sigma) ** 2, axis=-1) + + +def _delta_max(n_pos: int, n_neg: int, length: int, g: int, sigma: float) -> float: + """ + Maximal delta over the most segregated arrangements of the composition: a block of + positives and a block of negatives, with the neutral residues split across the three + gaps around them. Every split is tried (O(n0^2) candidates), in chunks. + """ + n0 = length - n_pos - n_neg + best = 0.0 + idx = np.arange(length) + for a in range(n0 + 1): + #a neutrals, positive block, b neutrals, negative block, remaining neutrals + b = np.arange(n0 - a + 1)[:, None] + pos = (idx >= a) & (idx < a + n_pos) + neg = (idx >= a + n_pos + b) & (idx < a + n_pos + b + n_neg) + pos = np.broadcast_to(pos, neg.shape).astype(float) + best = max(best, float(_delta(pos, neg.astype(float), g, sigma).max())) + return best + + +def kappa(sequence: str, blob_sizes: tuple[int, ...] = (5, 6)) -> pd.DataFrame: + """ + Calculate the kappa charge-patterning parameter of Das & Pappu [2]. For blob + size g, the sequence is split into the L - g + 1 overlapping blobs and + + sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) + delta_g = mean over blobs of (sigma_b - sigma)^2 + kappa_g = delta_g / delta_max,g + + where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the + largest delta over the maximally segregated arrangements of the same + composition (a positive block and a negative block with the neutral residues + distributed over the three surrounding gaps). kappa is the mean of kappa_g + over ``blob_sizes`` (5 and 6 in [2]); 0 means well mixed and 1 fully + segregated. Charges: K, R = +1; D, E = -1; H = 0. + + kappa is reported as 0 (not NaN) when the sequence has no charges or only one + charge type, where oppositely charged residues cannot be segregated. A blob + size longer than the sequence is reduced to the sequence length. + + Parameters + ========== + :sequence: str + protein sequence. + :blob_sizes: tuple[int, ...] (default=(5, 6)) + blob sizes g to average over. + + Returns + ======= + :kappa_df: pd.DataFrame + pandas DataFrame of shape 1 x (1 + len(blob_sizes)), columns ``Kappa`` and + ``Kappa_{g}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + blob_sizes = [_validate_int("blob size", g, 1) for g in blob_sizes] + if not blob_sizes: + raise ValueError("At least one blob size must be given.") + + q = _charge_vector(sequence) + pos, neg = (q > 0).astype(float), (q < 0).astype(float) + n_pos, n_neg, L = int(pos.sum()), int(neg.sum()), len(sequence) + values = [] + for g in blob_sizes: + if n_pos == 0 or n_neg == 0: + values.append(0.0) + continue + g = min(g, L) + sigma = (n_pos - n_neg) ** 2 / (L * (n_pos + n_neg)) + d_max = _delta_max(n_pos, n_neg, L, g, sigma) + values.append(min(float(_delta(pos, neg, g, sigma)) / d_max, 1.0) if d_max else 0.0) + return _to_df([float(np.mean(values))] + values, ["Kappa"] + [f"Kappa_{g}" for g in blob_sizes]) + +########################### Sequence Charge Decoration #################################### + +def sequence_charge_decoration(sequence: str) -> pd.DataFrame: + """ + Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh [3]: + + SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} + + with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values + indicate more segregated opposite charges; uncharged sequences give 0. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :scd_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``SCD``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + q = _charge_vector(sequence) + idx = np.flatnonzero(q) + #only charged residues contribute; pairwise over them is O(n_charged^2) + m, n = np.triu_indices(len(idx), k=1) + scd = np.sum(q[idx[m]] * q[idx[n]] * np.sqrt(idx[n] - idx[m])) / len(sequence) + return _to_df([scd], ["SCD"]) + +#################################### Charge Density ####################################### + +def charge_density(sequence: str, ph: float = 7.4) -> pd.DataFrame: + """ + Calculate the charge density of a protein sequence [4]: the net charge at + ``ph`` (from ``charge_distribution``) divided by the molecular weight (from + ``molecular_weight``), in charges per Da. + + ChargeDensity = NetCharge(pH) / MW + + Parameters + ========== + :sequence: str + protein sequence. + :ph: float (default=7.4) + pH at which the net charge is evaluated. + + Returns + ======= + :charge_density_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``ChargeDensity``. The value is + rounded to 6 dp because it is typically of order 1e-4. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + net = charge_distribution(sequence, ph=ph)["NetCharge"].iloc[0] + mw = molecular_weight(sequence)["MolecularWeight"].iloc[0] + return pd.DataFrame([[round(net / mw, 6)]], columns=["ChargeDensity"]) diff --git a/protpy/utils.py b/protpy/utils.py index 6de41cd..48c260e 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -126,6 +126,7 @@ "abbreviation": "pI", "parameters": { "sequence": "str — protein sequence", + "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", }, "output_shape": "1 x 1", "speed": "Fast", @@ -161,6 +162,7 @@ "parameters": { "sequence": "str — protein sequence", "ph": "float (default=7.4) — pH for charge calculation", + "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", }, "output_shape": "1 x 3", "speed": "Fast", @@ -977,6 +979,83 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "charge_fractions": { + "description": "Fraction of charged residues (FCR), positive/negative fractions and charge asymmetry sigma = NCPR²/FCR.", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "FCR", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Das & Pappu (2013). PNAS, 110(33), 13392-13397.", + "doi": "10.1073/pnas.1304749110", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "ncpr": { + "description": "Net charge per residue: (n+ - n-) / L with K/R positive and D/E negative.", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "NCPR", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Mao et al. (2010). PNAS, 107(18), 8183-8188.", + "doi": "10.1073/pnas.0911107107", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "kappa": { + "description": "Das-Pappu kappa: normalised blob charge-asymmetry deviation measuring segregation of opposite charges (0 mixed, 1 segregated).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "Kappa", + "parameters": { + "sequence": "str — protein sequence", + "blob_sizes": "tuple[int] (default=(5, 6)) — blob sizes averaged", + }, + "output_shape": "1 x (1 + len(blob_sizes)) (default 1 x 3)", + "speed": "Moderate", + "reference": "Das & Pappu (2013). PNAS, 110(33), 13392-13397.", + "doi": "10.1073/pnas.1304749110", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "sequence_charge_decoration": { + "description": "Sequence charge decoration: (1/L) sum_{m>n} q_m q_n sqrt(m-n).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "SCD", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Sawle & Ghosh (2015). J. Chem. Phys., 143(8), 085101.", + "doi": "10.1063/1.4929391", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "charge_density": { + "description": "Net charge at the given pH divided by molecular weight (charges per Da).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "ChargeDensity", + "parameters": { + "sequence": "str — protein sequence", + "ph": "float (default=7.4) — pH", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Müller et al. (2017). Bioinformatics, 33(17), 2753-2755 (modlAMP).", + "doi": "10.1093/bioinformatics/btx285", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_physicochemical.py b/tests/test_physicochemical.py new file mode 100644 index 0000000..6c91a5a --- /dev/null +++ b/tests/test_physicochemical.py @@ -0,0 +1,112 @@ +########################################################################### +########### protPy - Physicochemical Module Tests (Phases 3-4) ######## +########################################################################### + +import itertools +import math +import unittest + +import numpy as np + +import protpy + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" + +#(Glu-Lys)25 sequence variants and kappa values from Das & Pappu 2013, Fig. 1 +#(the regularly patterned variants, each 25 E + 25 K) +DAS_PAPPU_SV = { + "sv1": ("EK" * 25, 0.0009), + "sv2": ("EEEKKK" * 8 + "EK", 0.0025), + "sv7": ("EEEEKKKK" * 6 + "EK", 0.0450), + "sv8": ("KKKKEEEE" * 6 + "KE", 0.0450), + "sv19": ("EEEEEKKKKK" * 5, 0.1941), + "sv30": ("E" * 25 + "K" * 25, 1.0000), +} + + +class ProtpyChargeTests(unittest.TestCase): + + def _check_invalid(self, func): + with self.assertRaises(TypeError): + func(5) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("AXB") + + def test_charge_fractions(self): + df = protpy.charge_fractions("KKDD") + self.assertEqual(list(df.columns), ["FCR", "FPlus", "FMinus", "Sigma"]) + self.assertEqual(df.values[0].tolist(), [1.0, 0.5, 0.5, 0.0]) + #H is neutral; sigma = NCPR^2 / FCR + df = protpy.charge_fractions("KKKDAAHH") + self.assertEqual(df["FCR"].iloc[0], 0.5) + self.assertEqual(df["Sigma"].iloc[0], round(0.25 ** 2 / 0.5, 3)) + self.assertEqual(protpy.charge_fractions("AAAA")["Sigma"].iloc[0], 0.0) + self._check_invalid(protpy.charge_fractions) + + def test_ncpr(self): + self.assertEqual(protpy.ncpr("KKKA")["NCPR"].iloc[0], 0.75) + self.assertEqual(protpy.ncpr("KRDE")["NCPR"].iloc[0], 0.0) + self._check_invalid(protpy.ncpr) + + def test_kappa_published_values(self): + for name, (seq, ref) in DAS_PAPPU_SV.items(): + with self.subTest(sv=name): + self.assertEqual((len(seq), seq.count("E"), seq.count("K")), (50, 25, 25)) + self.assertAlmostEqual(protpy.kappa(seq)["Kappa"].iloc[0], ref, delta=0.02) + + def test_kappa(self): + df = protpy.kappa(SEQ) + self.assertEqual(list(df.columns), ["Kappa", "Kappa_5", "Kappa_6"]) + self.assertTrue(0 <= df["Kappa"].iloc[0] <= 1) + #documented edge cases: no charge / one charge type -> 0 + self.assertEqual(protpy.kappa("AAAAAAA")["Kappa"].iloc[0], 0.0) + self.assertEqual(protpy.kappa("KKAAKK")["Kappa"].iloc[0], 0.0) + #short sequence: blob reduced to the length, no crash + self.assertFalse(protpy.kappa("KD").isnull().any().any()) + self.assertEqual(protpy.kappa(SEQ, blob_sizes=(5,)).shape, (1, 2)) + with self.assertRaises(ValueError): + protpy.kappa(SEQ, blob_sizes=()) + self._check_invalid(protpy.kappa) + + def test_scd(self): + self.assertEqual(protpy.sequence_charge_decoration("AAAA")["SCD"].iloc[0], 0.0) + #brute force + q = [1 if aa in "KR" else -1 if aa in "DE" else 0 for aa in SEQ] + ref = sum(q[m] * q[n] * math.sqrt(m - n) for m in range(len(q)) for n in range(m)) / len(q) + self.assertAlmostEqual(protpy.sequence_charge_decoration(SEQ)["SCD"].iloc[0], round(ref, 3)) + #segregated charges are more negative than mixed + self.assertLess(protpy.sequence_charge_decoration("E" * 25 + "K" * 25)["SCD"].iloc[0], + protpy.sequence_charge_decoration("EK" * 25)["SCD"].iloc[0]) + self._check_invalid(protpy.sequence_charge_decoration) + + def test_charge_density(self): + df = protpy.charge_density(SEQ, ph=7.0) + net = protpy.charge_distribution(SEQ, ph=7.0)["NetCharge"].iloc[0] + mw = protpy.molecular_weight(SEQ)["MolecularWeight"].iloc[0] + self.assertAlmostEqual(df["ChargeDensity"].iloc[0], net / mw, places=6) + self._check_invalid(protpy.charge_density) + + def test_pka_scale(self): + #defaults unchanged + self.assertEqual(protpy.isoelectric_point(SEQ).iloc[0, 0], protpy.isoelectric_point(SEQ, pka_scale="protpy").iloc[0, 0]) + values = {s: protpy.isoelectric_point(SEQ, pka_scale=s).iloc[0, 0] for s in ("emboss", "bjellqvist", "ipc_protein")} + self.assertEqual(len(set(values.values())), 3) + for s in ("emboss", "bjellqvist", "ipc_protein"): + self.assertEqual(protpy.charge_distribution(SEQ, pka_scale=s).shape, (1, 3)) + with self.assertRaises(ValueError): + protpy.isoelectric_point(SEQ, pka_scale="nope") + with self.assertRaises(ValueError): + protpy.charge_distribution(SEQ, pka_scale="nope") + + def test_bjellqvist_matches_biopython(self): + from Bio.SeqUtils.IsoelectricPoint import IsoelectricPoint + for seq in (SEQ, "ACDEFGHIKLMNPQRSTVWY", "PEPTIDEKRD", "MAAAKE"): + with self.subTest(seq=seq): + self.assertAlmostEqual(protpy.isoelectric_point(seq, pka_scale="bjellqvist").iloc[0, 0], + IsoelectricPoint(seq).pi(), delta=0.01) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index a00c201..334bbfb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -80,7 +80,7 @@ class ProtpyUtilsTests(unittest.TestCase): } VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order", - "Scales"} + "Scales", "Physicochemical"} VALID_SPEEDS = {"Fast", "Moderate", "Slow"} # ── Registry integrity ──────────────────────────────────────────────────────── From 5c2835979b676550828f3691ac5f0437ca72a3e5 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:29:31 +0100 Subject: [PATCH 07/17] v1.5.0 phase 4: hydrophobicity profile, hydrophobic ratio, half-life, IVYWREL, CvP bias, flexibility; scale options for GRAVY / hydrophobic moment, A280 absorbance --- docs/references.bib | 4 + planning/BLOCKED.md | 6 + protpy/__init__.py | 1 + protpy/composition.py | 65 ++++++++-- protpy/data/aa_scales.json | 28 +++++ protpy/data/residue_tables.json | 108 ++++++++++++++++ protpy/physicochemical.py | 215 +++++++++++++++++++++++++++++++- protpy/utils.py | 100 ++++++++++++++- tests/test_physicochemical.py | 70 +++++++++++ 9 files changed, 583 insertions(+), 14 deletions(-) diff --git a/docs/references.bib b/docs/references.bib index 35eb87e..64499d8 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -2297,3 +2297,7 @@ @article{Guruprasad_1990 stability of a protein from its primary sequence}, volume={4}, ISSN={1741-0134}, url={http://dx.doi.org/10.1093/protein/4.2.155}, DOI={10.1093/protein/4.2.155}, number={2}, journal={“Protein Engineering, Design and Selection”}, publisher={Oxford University Press (OUP)}, author={Guruprasad, Kunchur and Reddy, B.V.Bhasker and Pandit, Madhusudan W.}, year={1990}, pages={155–161} } @article{Ong_2007, title={Efficacy of different protein descriptors in predicting protein functional families}, volume={8}, ISSN={1471-2105}, url={http://dx.doi.org/10.1186/1471-2105-8-300}, DOI={10.1186/1471-2105-8-300}, number={1}, journal={BMC Bioinformatics}, publisher={Springer Science and Business Media LLC}, author={Ong, Serene AK and Lin, Hong Huang and Chen, Yu Zong and Li, Ze Rong and Cao, Zhiwei}, year={2007}, month=Aug } + + +% ===== added from Crossref (protpy registry) ===== +@article{Wang_2019, title={The antimicrobial peptide database provides a platform for decoding the design principles of naturally occurring antimicrobial peptides}, volume={29}, ISSN={1469-896X}, url={http://dx.doi.org/10.1002/pro.3702}, DOI={10.1002/pro.3702}, number={1}, journal={Protein Science}, publisher={Wiley}, author={Wang, Guangshun}, year={2019}, month=Aug, pages={8–18} } diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md index 633fa47..ad832d3 100644 --- a/planning/BLOCKED.md +++ b/planning/BLOCKED.md @@ -10,6 +10,9 @@ Each entry names the table that is missing and where it would have to come from. | 12 | `kidera_factors` | Kidera et al. 1985 10-factor table (J Protein Chem, 10.1007/BF01025492) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | | 20 | `cruciani_properties` | Cruciani et al. 2004 3-property table (J Chemometrics, 10.1002/cem.856) | Paywalled; the only copy found was peptides.py (GPL, cross-check only). | | 26 | `omega` | The exact Ω definition: binary alphabet encoding, blob size(s) and δ_max construction | Martin et al. 2016 only says Ω is "analogous to κ" and puts the calculation in its Supporting Information. The SI isn't open access (Europe PMC has no supplementary bundle; ACS and PMC downloads are behind browser challenges), so the definition and a reproducible reference value couldn't be checked. | +| 31 | `membrane_position` | The Eisenberg 1984 hydrophobic-moment-plot boundary lines (globular / surface / transmembrane) | Eisenberg, Schwarz et al. 1984 J Mol Biol (10.1016/0022-2836(84)90309-7) is paywalled (403). | +| 35 | `wilkinson_harrison_solubility` | λ1, λ2 (and the sign of λ2) and CV′ of the revised model | Davis et al. 1999 (Wiley) and Wilkinson & Harrison 1991 are paywalled. The Harrison lab calculator (biotech.ou.edu) fails TLS and the Internet Archive was offline. | +| Phase 4 scales | `hessa`, `moon_fleming` and `aasi` single-value scales (used by `hydrophobicity_profile`, E6 `gravy(scale=)` and E7 `hydrophobic_moment(scale=)`) | Hessa 2005 ΔG_app (Nature, paywalled); Moon & Fleming 2011 (values are in SI Table S1, not available); Juretić 2009 AASI (paywalled). Available scales: kyte_doolittle, hopp_woods, eisenberg, wimley_white, engelman, domcut, plus any AAindex1 code. | ## Shipped with caveats @@ -23,4 +26,7 @@ Each entry names the table that is missing and where it would have to come from. - **`distance_pair`.** The plan specified G + G²·(rng+1) outputs over distances 0..rng. Liu 2014 (Eqs. 5–12) defines d = 1..rng, giving G + G²·rng (602 for cp14, d = 3, as the paper states), and that is what is implemented. The paper's printed cp(14) lists E twice; the stray E in the first cluster is dropped, the only 14-cluster reading. - **`kappa`.** δ_max is found by enumerating the segregated arrangements (positive block, negative block, neutral residues split over the three gaps), as the plan specifies, because the Das & Pappu SI is behind a browser challenge. It reproduces the published κ of the patterned (Glu-Lys)25 variants sv1, sv2, sv7, sv8, sv19 and sv30 to within 0.002. The irregular variants could not be read reliably from the Fig. 1 image and are not tested. - **`isoelectric_point(pka_scale="emboss")`.** Uses EMBOSS 6.6.0 `Epk.dat`, whose N-terminal pKa differs from the 8.6 that Kozlowski 2016 Table 1 attributes to EMBOSS. +- **`flexibility`.** Uses Vihinen's average parameter set (AAindex VINM940101, the same table Biopython uses) with the window-9, 0.25-edge weighting. The neighbour-dependent sets (VINM940102–104) need Vihinen's definition of a rigid neighbour, which is paywalled, so they are not used. +- **`hydrophobic_moment(scale=None)`.** Keeps protpy's existing table (A = 0.62, the "Eisenberg consensus" values used by most moment tools), so the default output is unchanged. `scale="eisenberg"` is AAindex EISD840101 (Eisenberg 1984 Ann Rev, A = 0.25); the two published "consensus" scales differ. +- **`half_life`.** The E. coli value for an N-terminal Pro is "?" in ProtParam and is returned as NaN. - **`atomic_composition`.** By default it matches Pfeature (no water removed); `subtract_water=True` gives the true peptide formula. Pfeature's W and Y formulas each have one H too many, and protpy uses the correct formulas. diff --git a/protpy/__init__.py b/protpy/__init__.py index fc0fd4e..827f87b 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -47,4 +47,5 @@ "z_scales_5", "vhse", "atchley_factors", "t_scales", "st_scales", "protfp", "fasgai", "blosum_indices", "ms_whim", "auto_cross_covariance", "physicochemical_distance_transformation", "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", + "hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility", ] \ No newline at end of file diff --git a/protpy/composition.py b/protpy/composition.py index 4e6f9a9..57088e6 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -98,7 +98,7 @@ class prediction: new methods of statistical classification. Proc Int Conf """ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight -from ._helpers import _load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df +from ._helpers import _load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df, _property_matrix #physicochemical values for all amino acids for hydrophobicity, hydrophilicty and residue mass #values taken from http://www.csbio.sjtu.edu.cn/bioinf/PseAAC/ParaValue.htm @@ -319,7 +319,7 @@ def tripeptide_composition(sequence: str) -> pd.DataFrame: "W": -0.9, "Y": -1.3 } -def gravy(sequence: str) -> pd.DataFrame: +def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: """ Calculate the Grand Average of Hydropathy (GRAVY) for a protein sequence. GRAVY is the sum of the Kyte-Doolittle hydropathy values of all residues @@ -330,10 +330,16 @@ def gravy(sequence: str) -> pd.DataFrame: A positive GRAVY value indicates an overall hydrophobic protein; a negative value indicates an overall hydrophilic protein. + Another single-value scale can be averaged with ``scale`` ("hopp_woods", + "eisenberg", "wimley_white", "engelman", "domcut" or any AAindex1 accession + code); the column is then named ``GRAVY_{scale}``. + Parameters ========== :sequence: str protein sequence. + :scale: str (default="kyte_doolittle") + hydrophobicity scale to average. Returns ======= @@ -344,6 +350,12 @@ def gravy(sequence: str) -> pd.DataFrame: #validate input protein sequence sequence = _validate_sequence(sequence) + if scale != "kyte_doolittle": + values = _property_matrix(sequence, scale) + if values.shape[1] != 1: + raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") + return _to_df([values.mean()], [f"GRAVY_{scale}"]) + #sum Kyte-Doolittle hydropathy values and divide by sequence length gravy_score = round(sum(kyte_doolittle_[aa] for aa in sequence) / len(sequence), 3) @@ -1027,7 +1039,7 @@ def aliphatic_index(sequence: str) -> pd.DataFrame: ############################### Extinction Coefficient #################################### -def extinction_coefficient(sequence: str) -> pd.DataFrame: +def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFrame: """ Calculate the molar Extinction Coefficient of a protein sequence at 280 nm. The coefficient is derived from the number of Trp (W), Tyr (Y) and @@ -1038,15 +1050,20 @@ def extinction_coefficient(sequence: str) -> pd.DataFrame: where SS is the number of disulfide bonds (Cys_count // 2) for the oxidised form, and 0 for the reduced form. Both are returned. + With ``absorbance=True`` the A280 of a 1 g/L (0.1%) solution is added, + Abs 0.1% = EC / MW, for both forms (Pace et al. 1995, Gill & von Hippel 1989). + Parameters ========== :sequence: str protein sequence. + :absorbance: bool (default=False) + also return ``Abs01_Reduced`` and ``Abs01_Oxidized``. Returns ======= :extinction_coefficient_df: pd.DataFrame - pandas DataFrame of shape 1 x 2 with columns + pandas DataFrame of shape 1 x 2 (1 x 4 with ``absorbance``) with columns ``ExtCoeff_Reduced`` and ``ExtCoeff_Oxidized`` (M⁻¹cm⁻¹). """ #validate input protein sequence @@ -1066,6 +1083,11 @@ def extinction_coefficient(sequence: str) -> pd.DataFrame: columns=["ExtCoeff_Reduced", "ExtCoeff_Oxidized"] ) + if absorbance: + mw = molecular_weight(sequence)["MolecularWeight"].iloc[0] + extinction_coefficient_df["Abs01_Reduced"] = round(ec_reduced / mw, 3) + extinction_coefficient_df["Abs01_Oxidized"] = round(ec_oxidized / mw, 3) + return extinction_coefficient_df ################################ Boman Index ############################################## @@ -1217,7 +1239,8 @@ def shannon_entropy(sequence: str) -> pd.DataFrame: ############################### Hydrophobic Moment ######################################## -def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> pd.DataFrame: +def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100, + scale: str | None = None) -> pd.DataFrame: """ Calculate the mean and maximum Hydrophobic Moment of a protein sequence. The hydrophobic moment measures the amphiphilicity of a helical segment — @@ -1228,9 +1251,14 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> p mu = sqrt( (sum H_i * sin(i * angle))^2 + (sum H_i * cos(i * angle))^2 ) / window - where H_i is the Eisenberg hydrophobicity of residue i and angle is in - degrees. Default angle of 100° corresponds to an alpha-helix; 160° is - commonly used for beta-strands. + where H_i is the hydrophobicity of residue i and angle is in degrees. + Default angle of 100° corresponds to an alpha-helix; 160° is used for + beta-strands (Eisenberg et al. 1984). + + By default H is protpy's normalised hydrophobicity table (the values used + for PseAAC, A = 0.62, also widely quoted as the "Eisenberg consensus" scale). + ``scale`` selects another single-value scale, e.g. "eisenberg" (AAindex + EISD840101, Eisenberg 1984, A = 0.25), "kyte_doolittle" or any AAindex1 code. Parameters ========== @@ -1238,8 +1266,10 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> p protein sequence. :window: int (default=11) sliding window length in residues. - :angle: float (default=100) - residue rotation angle in degrees (100° = alpha-helix). + :angle: float | str (default=100) + residue rotation angle in degrees, or "helix" (100) / "sheet" (160). + :scale: str | None (default=None) + hydrophobicity scale; None uses protpy's default table. Returns ======= @@ -1251,6 +1281,17 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> p sequence = _validate_sequence(sequence) window = _validate_int("window", window, 1) + if isinstance(angle, str): + if angle not in ("helix", "sheet"): + raise ValueError(f"angle must be a number, 'helix' or 'sheet', got '{angle}'.") + angle = {"helix": 100, "sheet": 160}[angle] + if scale is None: + h_scale = hydrophobicity_ + else: + values = _property_matrix("".join(amino_acids), scale) + if values.shape[1] != 1: + raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") + h_scale = dict(zip(amino_acids, values[:, 0])) n = len(sequence) #use full sequence as single window when shorter than default window @@ -1261,8 +1302,8 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float = 100) -> p #compute hydrophobic moment for each window position using Eisenberg scale for i in range(n - effective_window + 1): seg = sequence[i:i + effective_window] - hsin = sum(hydrophobicity_[aa] * math.sin(j * angle_rad) for j, aa in enumerate(seg)) - hcos = sum(hydrophobicity_[aa] * math.cos(j * angle_rad) for j, aa in enumerate(seg)) + hsin = sum(h_scale[aa] * math.sin(j * angle_rad) for j, aa in enumerate(seg)) + hcos = sum(h_scale[aa] * math.cos(j * angle_rad) for j, aa in enumerate(seg)) mu = math.sqrt(hsin ** 2 + hcos ** 2) / effective_window moments.append(mu) diff --git a/protpy/data/aa_scales.json b/protpy/data/aa_scales.json index 32b7678..95f9e70 100644 --- a/protpy/data/aa_scales.json +++ b/protpy/data/aa_scales.json @@ -1786,5 +1786,33 @@ "reference": "Atchley W.R. et al. (2005). Solving the protein sequence metric problem. PNAS, 102(18), 6395-6400.", "doi": "10.1073/pnas.0408677102", "source_note": "Table 2 of the primary paper (PMC1088356), transcribed programmatically 2026-10-12. Cross-checked: agrees with peptides.py to the 3 dp published." + }, + "vihinen_flexibility": { + "name": "Normalized flexibility parameters (B-values), average (Vihinen et al., 1994)", + "values": { + "A": 0.984, + "C": 0.906, + "D": 1.068, + "E": 1.094, + "F": 0.915, + "G": 1.031, + "H": 0.95, + "I": 0.927, + "K": 1.102, + "L": 0.935, + "M": 0.952, + "N": 1.048, + "P": 1.049, + "Q": 1.037, + "R": 1.008, + "S": 1.046, + "T": 0.997, + "V": 0.931, + "W": 0.904, + "Y": 0.929 + }, + "reference": "Vihinen, M., Torkkila, E. and Riikonen, P. 'Accuracy of protein flexibility predictions' Proteins 19, 141-149 (1994)", + "doi": "10.1002/prot.340190207", + "source_note": "AAindex1 record VINM940101 (average normalised B-values of Vihinen et al. 1994; identical to Biopython ProtParamData.Flex); copied 2026-10-12." } } \ No newline at end of file diff --git a/protpy/data/residue_tables.json b/protpy/data/residue_tables.json index 6140c17..90516eb 100644 --- a/protpy/data/residue_tables.json +++ b/protpy/data/residue_tables.json @@ -239,5 +239,113 @@ "reference": "protpy: v1.4 defaults. emboss: Rice P. et al. (2000) EMBOSS, Trends Genet 16(6):276-277. bjellqvist: Bjellqvist B. et al. (1993) Electrophoresis 14:1023-1031 and (1994) Electrophoresis 15:529-539. ipc_protein: Kozlowski L.P. (2016) IPC - Isoelectric Point Calculator. Biol Direct 11:55.", "doi": "10.1186/s13062-016-0159-9", "source_note": "Transcribed programmatically 2026-10-12. emboss: EMBOSS 6.6.0 data file emboss/data/Epk.dat (values only; N_term 7.5, whereas Kozlowski 2016 Table 1 lists 8.6 for EMBOSS). ipc_protein: Table 1 of Kozlowski 2016 (PMC5075173). bjellqvist: Biopython IsoelectricPoint.py tables (attribution; BSD-3), cross-checked against the Bjellqvist row of Kozlowski 2016 Table 1; residue-specific terminal values from Bjellqvist 1994 via Biopython." + }, + "n_end_half_life": { + "name": "N-end rule half-life (hours) by N-terminal residue", + "values": { + "A": { + "mammal": 4.4, + "yeast": 20.0, + "ecoli": 10.0 + }, + "R": { + "mammal": 1.0, + "yeast": 0.03333333333333333, + "ecoli": 0.03333333333333333 + }, + "N": { + "mammal": 1.4, + "yeast": 0.05, + "ecoli": 10.0 + }, + "D": { + "mammal": 1.1, + "yeast": 0.05, + "ecoli": 10.0 + }, + "C": { + "mammal": 1.2, + "yeast": 20.0, + "ecoli": 10.0 + }, + "Q": { + "mammal": 0.8, + "yeast": 0.16666666666666666, + "ecoli": 10.0 + }, + "E": { + "mammal": 1.0, + "yeast": 0.5, + "ecoli": 10.0 + }, + "G": { + "mammal": 30.0, + "yeast": 20.0, + "ecoli": 10.0 + }, + "H": { + "mammal": 3.5, + "yeast": 0.16666666666666666, + "ecoli": 10.0 + }, + "I": { + "mammal": 20.0, + "yeast": 0.5, + "ecoli": 10.0 + }, + "L": { + "mammal": 5.5, + "yeast": 0.05, + "ecoli": 0.03333333333333333 + }, + "K": { + "mammal": 1.3, + "yeast": 0.05, + "ecoli": 0.03333333333333333 + }, + "M": { + "mammal": 30.0, + "yeast": 20.0, + "ecoli": 10.0 + }, + "F": { + "mammal": 1.1, + "yeast": 0.05, + "ecoli": 0.03333333333333333 + }, + "P": { + "mammal": 20.0, + "yeast": 20.0, + "ecoli": null + }, + "S": { + "mammal": 1.9, + "yeast": 20.0, + "ecoli": 10.0 + }, + "T": { + "mammal": 7.2, + "yeast": 20.0, + "ecoli": 10.0 + }, + "W": { + "mammal": 2.8, + "yeast": 0.05, + "ecoli": 0.03333333333333333 + }, + "Y": { + "mammal": 2.8, + "yeast": 0.16666666666666666, + "ecoli": 0.03333333333333333 + }, + "V": { + "mammal": 100.0, + "yeast": 20.0, + "ecoli": 10.0 + } + }, + "reference": "Bachmair A. et al. (1986) Science 234:179-186 (mammalian reticulocytes, yeast); Gonda D.K. et al. (1989) J Biol Chem 264:16700-16712; Tobias J.W. et al. (1991) Science 254:1374-1377 (E. coli); as tabulated by ExPASy ProtParam (Gasteiger et al. 2005).", + "doi": "10.1126/science.3018930", + "source_note": "Table 'amino acids and the corresponding half-life' of https://web.expasy.org/protparam/protparam-doc.html, parsed 2026-10-12. Minutes converted to hours. Lower bounds ('>20 hour', '>10 hour') are stored as the bound; the E. coli value for Pro is '?' in the source and stored as null." } } \ No newline at end of file diff --git a/protpy/physicochemical.py b/protpy/physicochemical.py index e3be2c2..3b609c0 100644 --- a/protpy/physicochemical.py +++ b/protpy/physicochemical.py @@ -26,17 +26,42 @@ [4] Müller, A. T., Gabernet, G., Hiss, J. A., & Schneider, G. (2017). modlAMP: Python for antimicrobial peptides. Bioinformatics, 33(17), 2753-2755. https://doi.org/10.1093/bioinformatics/btx285 +[5] Kyte, J., & Doolittle, R. F. (1982). A simple method for displaying the + hydropathic character of a protein. J Mol Biol, 157(1), 105-132. + https://doi.org/10.1016/0022-2836(82)90515-0 +[6] Gasteiger, E. et al. (2005). Protein identification and analysis tools on the + ExPASy server. In The Proteomics Protocols Handbook (pp. 571-607). Humana + Press. https://doi.org/10.1385/1-59259-890-0:571 +[7] Wang, G. (2020). The antimicrobial peptide database provides a platform for + decoding the design principles of naturally occurring antimicrobial peptides. + Protein Sci, 29(1), 8-18. https://doi.org/10.1002/pro.3702 +[8] Bachmair, A., Finley, D., & Varshavsky, A. (1986). In vivo half-life of a + protein is a function of its amino-terminal residue. Science, 234(4773), + 179-186. https://doi.org/10.1126/science.3018930 +[9] Gonda, D. K. et al. (1989). Universality and structure of the N-end rule. + J Biol Chem, 264(28), 16700-16712. https://doi.org/10.1016/S0021-9258(19)84762-2 +[10] Tobias, J. W. et al. (1991). The N-end rule in bacteria. Science, 254(5036), + 1374-1377. https://doi.org/10.1126/science.1962196 +[11] Zeldovich, K. B., Berezovsky, I. N., & Shakhnovich, E. I. (2007). Protein and + DNA sequence determinants of thermophilic adaptation. PLoS Comput Biol, 3(1), + e5. https://doi.org/10.1371/journal.pcbi.0030005 +[12] Suhre, K., & Claverie, J.-M. (2003). Genomic correlates of hyperthermostability, + an update. J Biol Chem, 278(19), 17198-17202. https://doi.org/10.1074/jbc.M301327200 +[13] Vihinen, M., Torkkila, E., & Riikonen, P. (1994). Accuracy of protein + flexibility predictions. Proteins, 19(2), 141-149. + https://doi.org/10.1002/prot.340190207 """ import numpy as np import pandas as pd from ._constants import _validate_sequence, _validate_int -from ._helpers import _charge_vector, _to_df +from ._helpers import _charge_vector, _to_df, _property_matrix, _sliding_windows, _profile_stats, _load_json_table from .composition import charge_distribution, molecular_weight __all__ = [ "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", + "hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility", ] ################################## Charge Fractions ####################################### @@ -247,3 +272,191 @@ def charge_density(sequence: str, ph: float = 7.4) -> pd.DataFrame: net = charge_distribution(sequence, ph=ph)["NetCharge"].iloc[0] mw = molecular_weight(sequence)["MolecularWeight"].iloc[0] return pd.DataFrame([[round(net / mw, 6)]], columns=["ChargeDensity"]) + +############################### Hydrophobicity Profile #################################### + +def hydrophobicity_profile(sequence: str, scale: str = "kyte_doolittle", window: int = 9, edge: float = 1.0, + threshold: float | None = None) -> pd.DataFrame: + """ + Summarise a sliding-window hydrophobicity profile [5, 6]. Each residue is + replaced by its scale value and a window of length ``window`` is slid along + the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` + (ExPASy ProtScale semantics). The profile is reduced to its mean, max, min + and standard deviation, plus the fraction of windows above ``threshold`` + when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). + Windows longer than the sequence use the whole sequence. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str (default="kyte_doolittle") + single-value scale: "kyte_doolittle", "hopp_woods", "eisenberg", + "wimley_white", "engelman", "domcut", or any AAindex1 accession code. + :window: int (default=9) + window length. + :edge: float (default=1.0) + relative weight of the window ends, 0-1. + :threshold: float | None (default=None) + if set, add the fraction of windows with a value above it. + + Returns + ======= + :profile_df: pd.DataFrame + pandas DataFrame of shape 1 x 4 (1 x 5 with ``threshold``), columns + ``HP_{scale}_w{window}_{mean,max,min,std[,frac_above]}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + if not 0 <= edge <= 1: + raise ValueError(f"edge must be between 0 and 1, got {edge}.") + + values = _property_matrix(sequence, scale) + if values.shape[1] != 1: + raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") + profile = _sliding_windows(values[:, 0], window, edge) + stats = _profile_stats(profile, f"HP_{scale}_w{window}", threshold=threshold) + return _to_df(list(stats.values()), list(stats)) + +################################ Hydrophobic Ratio ######################################## + +#hydrophobic residues as defined by the APD [7] +_apd_hydrophobic = frozenset("LIVFMACW") + +def hydrophobic_ratio(sequence: str) -> pd.DataFrame: + """ + Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) + [7]: the percentage of residues that are L, I, V, F, M, A, C or W. + + HydrophobicRatio = N(LIVFMACW) / L x 100 + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :hydrophobic_ratio_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``HydrophobicRatio`` (percentage). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + return _to_df([sum(aa in _apd_hydrophobic for aa in sequence) / len(sequence) * 100], ["HydrophobicRatio"]) + +#################################### Half Life ############################################ + +def half_life(sequence: str) -> pd.DataFrame: + """ + Estimate the in vivo half-life of a protein from its N-terminal residue by + the N-end rule [8, 9, 10], as tabulated by ExPASy ProtParam [6]: mammalian + reticulocytes (in vitro), yeast and E. coli (in vivo). + + Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" + in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli + value for an N-terminal Pro is unknown in the source and returned as NaN. + Not applicable to N-terminally modified proteins. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :half_life_df: pd.DataFrame + pandas DataFrame of shape 1 x 3, columns ``HalfLife_Mammal_h``, + ``HalfLife_Yeast_h`` and ``HalfLife_Ecoli_h``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + + row = _load_json_table("residue_tables.json", "n_end_half_life")["values"][sequence[0]] + values = [np.nan if row[k] is None else row[k] for k in ("mammal", "yeast", "ecoli")] + return _to_df(values, ["HalfLife_Mammal_h", "HalfLife_Yeast_h", "HalfLife_Ecoli_h"]) + +################################## IVYWREL Index ########################################## + +def ivywrel_index(sequence: str) -> pd.DataFrame: + """ + Calculate the IVYWREL index of Zeldovich et al. [11]: the fraction of residues + that are I, V, Y, W, R, E or L, which correlates with optimal growth + temperature. + + IVYWREL = N(IVYWREL) / L + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :ivywrel_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``IVYWREL`` (fraction). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + return _to_df([sum(aa in "IVYWREL" for aa in sequence) / len(sequence)], ["IVYWREL"]) + +#################################### CvP Bias ############################################# + +def cvp_bias(sequence: str) -> pd.DataFrame: + """ + Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie [12], an + indicator of hyperthermostability: + + CvP = %(D + E + K + R) - %(N + Q + S + T) + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :cvp_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``CvP_Bias`` (percentage points). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + charged = sum(aa in "DEKR" for aa in sequence) + polar = sum(aa in "NQST" for aa in sequence) + return _to_df([(charged - polar) / len(sequence) * 100], ["CvP_Bias"]) + +################################### Flexibility ########################################### + +def flexibility(sequence: str, window: int = 9) -> pd.DataFrame: + """ + Summarise the flexibility profile of Vihinen et al. [13]. Average normalised + B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged + over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, + 0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced + to its mean, max, min and standard deviation. + + The neighbour-dependent parameter sets of [13] (residues with 0, 1 or 2 rigid + neighbours) are not used. Compared with Biopython's + ``ProteinAnalysis.flexibility``, which uses the same values and weights, + protpy centres each window on its middle residue and includes the last + window; Biopython takes the residue after the centre and omits the last window. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=9) + window length (the parameters were optimised for 9). + + Returns + ======= + :flexibility_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``Flex_{mean,max,min,std}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + + profile = _sliding_windows(_property_matrix(sequence, "vihinen_flexibility")[:, 0], window, edge=0.25) + stats = _profile_stats(profile, "Flex") + return _to_df(list(stats.values()), list(stats)) diff --git a/protpy/utils.py b/protpy/utils.py index 48c260e..9914f82 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -76,6 +76,7 @@ "abbreviation": "GRAVY", "parameters": { "sequence": "str — protein sequence", + "scale": "str (default='kyte_doolittle') — single-value scale or AAindex code (column GRAVY_{scale})", }, "output_shape": "1 x 1", "speed": "Fast", @@ -307,6 +308,7 @@ "abbreviation": "ExtCoeff", "parameters": { "sequence": "str — protein sequence", + "absorbance": "bool (default=False) — add Abs01_Reduced / Abs01_Oxidized (A280 of 1 g/L)", }, "output_shape": "1 x 2 [ExtCoeff_Reduced, ExtCoeff_Oxidized]", "speed": "Fast", @@ -365,7 +367,8 @@ "parameters": { "sequence": "str — protein sequence", "window": "int (default=11) — helical window size", - "angle": "float (default=100) — residue rotation angle in degrees (100° for alpha-helix)", + "angle": "float | str (default=100) — rotation angle in degrees, or 'helix' (100) / 'sheet' (160)", + "scale": "str | None (default=None) — hydrophobicity scale; None keeps protpy's default table", }, "output_shape": "1 x 2 [HydrophobicMoment_Mean, HydrophobicMoment_Max]", "speed": "Fast", @@ -1056,6 +1059,101 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "hydrophobicity_profile": { + "description": "Summary statistics of a sliding-window hydrophobicity profile (ProtScale-style edge weighting).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "HP", + "parameters": { + "sequence": "str — protein sequence", + "scale": "str (default='kyte_doolittle') — single-value scale or AAindex code", + "window": "int (default=9) — window length", + "edge": "float (default=1.0) — relative end weight", + "threshold": "float | None (default=None) — adds fraction of windows above threshold", + }, + "output_shape": "1 x 4 (1 x 5 with threshold)", + "speed": "Fast", + "reference": "Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132.", + "doi": "10.1016/0022-2836(82)90515-0", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "hydrophobic_ratio": { + "description": "APD hydrophobic ratio: percentage of L, I, V, F, M, A, C, W residues.", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "HydrophobicRatio", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Wang (2020). Protein Sci., 29(1), 8-18 (APD); Wang, Li & Wang (2016). Nucleic Acids Res., 44, D1087 (APD3).", + "doi": "10.1002/pro.3702", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "half_life": { + "description": "N-end rule half-life (hours) in mammalian, yeast and E. coli systems from the N-terminal residue.", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "HalfLife", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 3", + "speed": "Fast", + "reference": "Bachmair, Finley & Varshavsky (1986). Science, 234(4773), 179-186.", + "doi": "10.1126/science.3018930", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "ivywrel_index": { + "description": "Fraction of I, V, Y, W, R, E, L residues (thermophilic adaptation index).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "IVYWREL", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Zeldovich, Berezovsky & Shakhnovich (2007). PLoS Comput. Biol., 3(1), e5.", + "doi": "10.1371/journal.pcbi.0030005", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "cvp_bias": { + "description": "Charged versus polar bias: %(D+E+K+R) - %(N+Q+S+T).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "CvP", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Suhre & Claverie (2003). J. Biol. Chem., 278(19), 17198-17202.", + "doi": "10.1074/jbc.M301327200", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "flexibility": { + "description": "Summary statistics of the Vihinen average-B-value flexibility profile (window 9, end weight 0.25).", + "module": "physicochemical", + "category": "Physicochemical", + "abbreviation": "Flex", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=9) — window length", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Vihinen, Torkkila & Riikonen (1994). Proteins, 19(2), 141-149.", + "doi": "10.1002/prot.340190207", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_physicochemical.py b/tests/test_physicochemical.py index 6c91a5a..6177b6c 100644 --- a/tests/test_physicochemical.py +++ b/tests/test_physicochemical.py @@ -108,5 +108,75 @@ def test_bjellqvist_matches_biopython(self): IsoelectricPoint(seq).pi(), delta=0.01) +class ProtpyHydrophobicityStabilityTests(unittest.TestCase): + + def test_hydrophobicity_profile(self): + df = protpy.hydrophobicity_profile(SEQ) + self.assertEqual(list(df.columns), [f"HP_kyte_doolittle_w9_{s}" for s in ("mean", "max", "min", "std")]) + self.assertEqual(protpy.hydrophobicity_profile(SEQ, threshold=1.6).shape, (1, 5)) + self.assertEqual(protpy.hydrophobicity_profile(SEQ, scale="HOPT810101").shape, (1, 4)) + #window longer than sequence -> one window = GRAVY + self.assertEqual(protpy.hydrophobicity_profile("ACD", window=9)["HP_kyte_doolittle_w9_mean"].iloc[0], + protpy.gravy("ACD")["GRAVY"].iloc[0]) + with self.assertRaises(ValueError): + protpy.hydrophobicity_profile(SEQ, edge=2) + with self.assertRaises(ValueError): + protpy.hydrophobicity_profile(SEQ, scale="vhse") + with self.assertRaises(ValueError): + protpy.hydrophobicity_profile(SEQ, window=0) + + def test_hydrophobicity_profile_matches_biopython(self): + from Bio.SeqUtils.ProtParam import ProteinAnalysis + from Bio.SeqUtils.ProtParamData import kd + for edge in (1.0, 0.4): + ref = ProteinAnalysis(SEQ).protein_scale(kd, 9, edge) + df = protpy.hydrophobicity_profile(SEQ, window=9, edge=edge) + self.assertAlmostEqual(df["HP_kyte_doolittle_w9_mean"].iloc[0], round(np.mean(ref), 3), places=3) + self.assertAlmostEqual(df["HP_kyte_doolittle_w9_max"].iloc[0], round(max(ref), 3), places=3) + + def test_hydrophobic_ratio(self): + #APD example: TP10 is 61% hydrophobic (13 of 21 residues) + self.assertEqual(protpy.hydrophobic_ratio("AGYLLGKINLKALAALAKKIL")["HydrophobicRatio"].iloc[0], round(13 / 21 * 100, 3)) + self.assertEqual(protpy.hydrophobic_ratio("GGKK")["HydrophobicRatio"].iloc[0], 0.0) + + def test_half_life(self): + df = protpy.half_life("MKT") + self.assertEqual(df.values[0].tolist(), [30.0, 20.0, 10.0]) + self.assertEqual(protpy.half_life("RAA").values[0].tolist(), [1.0, round(2 / 60, 3), round(2 / 60, 3)]) + self.assertTrue(np.isnan(protpy.half_life("PAA")["HalfLife_Ecoli_h"].iloc[0])) + + def test_ivywrel_cvp(self): + self.assertEqual(protpy.ivywrel_index("IVYWRELA")["IVYWREL"].iloc[0], round(7 / 8, 3)) + self.assertEqual(protpy.cvp_bias("DEKRNQST")["CvP_Bias"].iloc[0], 0.0) + self.assertEqual(protpy.cvp_bias("DDAA")["CvP_Bias"].iloc[0], 50.0) + + def test_flexibility(self): + df = protpy.flexibility(SEQ) + self.assertEqual(list(df.columns), ["Flex_mean", "Flex_max", "Flex_min", "Flex_std"]) + #Biopython uses the same table and weights (but an off-by-one centre and drops the last window) + from Bio.SeqUtils.ProtParam import ProteinAnalysis + ref = ProteinAnalysis(SEQ).flexibility() + self.assertAlmostEqual(df["Flex_mean"].iloc[0], np.mean(ref), delta=0.01) + + def test_extensions(self): + self.assertEqual(list(protpy.gravy(SEQ).columns), ["GRAVY"]) + hw = protpy.gravy("AD", scale="hopp_woods") + self.assertEqual(list(hw.columns), ["GRAVY_hopp_woods"]) + self.assertEqual(hw.iloc[0, 0], (-0.5 + 3.0) / 2) + self.assertEqual(protpy.hydrophobic_moment(SEQ, angle="helix").values.tolist(), + protpy.hydrophobic_moment(SEQ).values.tolist()) + self.assertEqual(protpy.hydrophobic_moment(SEQ, angle="sheet").values.tolist(), + protpy.hydrophobic_moment(SEQ, angle=160).values.tolist()) + self.assertNotEqual(protpy.hydrophobic_moment(SEQ, scale="eisenberg").values.tolist(), + protpy.hydrophobic_moment(SEQ).values.tolist()) + with self.assertRaises(ValueError): + protpy.hydrophobic_moment(SEQ, angle="coil") + ext = protpy.extinction_coefficient(SEQ + "W", absorbance=True) + self.assertEqual(ext.shape, (1, 4)) + mw = protpy.molecular_weight(SEQ + "W").iloc[0, 0] + self.assertAlmostEqual(ext["Abs01_Reduced"].iloc[0], round(ext["ExtCoeff_Reduced"].iloc[0] / mw, 3)) + self.assertEqual(protpy.extinction_coefficient(SEQ).shape, (1, 2)) + + if __name__ == "__main__": unittest.main() From edb715ea93079a15398e07522bb9ee92437f336d Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:31:29 +0100 Subject: [PATCH 08/17] v1.5.0 phase 5: Wootton-Federhen, linguistic and Lempel-Ziv complexity --- planning/BLOCKED.md | 2 + protpy/__init__.py | 2 + protpy/complexity.py | 193 +++++++++++++++++++++++++++++++++++++++ protpy/utils.py | 48 ++++++++++ tests/test_complexity.py | 67 ++++++++++++++ tests/test_utils.py | 2 +- 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 protpy/complexity.py create mode 100644 tests/test_complexity.py diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md index ad832d3..513ba2a 100644 --- a/planning/BLOCKED.md +++ b/planning/BLOCKED.md @@ -29,4 +29,6 @@ Each entry names the table that is missing and where it would have to come from. - **`flexibility`.** Uses Vihinen's average parameter set (AAindex VINM940101, the same table Biopython uses) with the window-9, 0.25-edge weighting. The neighbour-dependent sets (VINM940102–104) need Vihinen's definition of a rigid neighbour, which is paywalled, so they are not used. - **`hydrophobic_moment(scale=None)`.** Keeps protpy's existing table (A = 0.62, the "Eisenberg consensus" values used by most moment tools), so the default output is unchanged. `scale="eisenberg"` is AAindex EISD840101 (Eisenberg 1984 Ann Rev, A = 0.25); the two published "consensus" scales differ. - **`half_life`.** The E. coli value for an N-terminal Pro is "?" in ProtParam and is returned as NaN. +- **`linguistic_complexity`.** The Troyanskaya 2002 full text is behind a Cloudflare check. The ratio-of-sums form in the plan is used: Σ distinct k-mers / Σ min(20^k, L−k+1). This is consistent with the abstract, which says the method counts "the number of subwords present" with suffix trees. Some sources use a product-of-ratios form instead. +- **`wootton_federhen_complexity`.** Implements the SEG trigger stage only (K2 ≤ trigger windows), not SEG's extension threshold or interval optimisation, as the plan specifies. - **`atomic_composition`.** By default it matches Pfeature (no water removed); `subtract_water=True` gives the true peptide formula. Pfeature's W and Y formulas each have one H too many, and protpy uses the correct formulas. diff --git a/protpy/__init__.py b/protpy/__init__.py index 827f87b..08fe3b6 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -5,6 +5,7 @@ from .sequence_order import * from .scales import * from .physicochemical import * +from .complexity import * from .utils import get_descriptor_info, list_descriptors, print_descriptor_info from .encode import all_descriptors, encode_dataset, read_sequences_from_file @@ -48,4 +49,5 @@ "blosum_indices", "ms_whim", "auto_cross_covariance", "physicochemical_distance_transformation", "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", "hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility", + "wootton_federhen_complexity", "linguistic_complexity", "lempel_ziv_complexity", ] \ No newline at end of file diff --git a/protpy/complexity.py b/protpy/complexity.py new file mode 100644 index 0000000..cdcf242 --- /dev/null +++ b/protpy/complexity.py @@ -0,0 +1,193 @@ +################################################################################ +############# Sequence Complexity ############## +################################################################################ + +from __future__ import annotations + +""" +Sequence complexity descriptors: Wootton-Federhen (SEG-like) complexity, +linguistic complexity and Lempel-Ziv complexity. ``shannon_entropy`` lives in +the composition module and is re-exported here. + +References +========== +[1] Wootton, J. C., & Federhen, S. (1993). Statistics of local complexity in amino + acid sequences and sequence databases. Comput Chem, 17(2), 149-163. + https://doi.org/10.1016/0097-8485(93)85006-X +[2] Wootton, J. C., & Federhen, S. (1996). Analysis of compositionally biased + regions in sequence databases. Methods Enzymol, 266, 554-571. + https://doi.org/10.1016/S0076-6879(96)66035-2 +[3] Troyanskaya, O. G., Arbell, O., Koren, Y., Landau, G. M., & Bolshoy, A. + (2002). Sequence complexity profiles of prokaryotic genomic sequences: a fast + algorithm for calculating linguistic complexity. Bioinformatics, 18(5), + 679-688. https://doi.org/10.1093/bioinformatics/18.5.679 +[4] Lempel, A., & Ziv, J. (1976). On the complexity of finite sequences. IEEE + Trans Inf Theory, 22(1), 75-81. https://doi.org/10.1109/TIT.1976.1055501 +[5] Diao, Y. et al. (2008). Using pseudo amino acid composition to predict + transmembrane regions in protein: cellular automata and Lempel-Ziv + complexity. Amino Acids, 34(1), 111-117. https://doi.org/10.1007/s00726-007-0550-z +""" + +import math +from collections import Counter + +import numpy as np +import pandas as pd + +from ._constants import _validate_sequence, _validate_int +from ._helpers import _to_df +from .composition import shannon_entropy + +__all__ = [ + "wootton_federhen_complexity", "linguistic_complexity", "lempel_ziv_complexity", "shannon_entropy", +] + +########################### Wootton-Federhen Complexity ################################### + +def wootton_federhen_complexity(sequence: str, window: int = 12, k2_trigger: float = 2.2) -> pd.DataFrame: + """ + Calculate Wootton-Federhen local compositional complexity [1, 2]. For every + window of length W with residue counts n_i: + + K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) + K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) + + The low-complexity fraction is the fraction of residues covered by at least + one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: + SEG's extension to the K2 extension threshold and its interval merging and + optimisation are not performed. A window longer than the sequence uses the + whole sequence. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=12) + window length W (SEG default 12). + :k2_trigger: float (default=2.2) + K2 trigger complexity in bits (SEG default 2.2). + + Returns + ======= + :wf_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``WF_K1_mean``, ``WF_K2_mean``, + ``WF_K2_min`` and ``WF_LowComplexity_frac``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = min(_validate_int("window", window, 1), len(sequence)) + + k1, k2 = [], [] + covered = np.zeros(len(sequence), dtype=bool) + counts = Counter(sequence[:window]) + for start in range(len(sequence) - window + 1): + if start: + #slide the window by one residue + counts[sequence[start - 1]] -= 1 + counts[sequence[start + window - 1]] += 1 + n = [c for c in counts.values() if c] + log_states = math.lgamma(window + 1) - sum(math.lgamma(c + 1) for c in n) + k1.append(log_states / math.log(20) / window) + entropy = 0.0 - sum(c / window * math.log2(c / window) for c in n) + k2.append(entropy) + if entropy <= k2_trigger: + covered[start:start + window] = True + return _to_df([np.mean(k1), np.mean(k2), min(k2), covered.mean()], + ["WF_K1_mean", "WF_K2_mean", "WF_K2_min", "WF_LowComplexity_frac"]) + +############################### Linguistic Complexity ##################################### + +def linguistic_complexity(sequence: str, max_k: int | None = None) -> pd.DataFrame: + """ + Calculate the linguistic complexity (LC) of a protein sequence [3]: the + number of distinct subwords (k-mers) observed, over the maximum number + possible for a sequence of that length over the 20-letter alphabet: + + LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) + + where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. + all subwords). This is the ratio-of-sums form, counting all subwords as in + [3]; repetitive sequences have low LC. + + Parameters + ========== + :sequence: str + protein sequence. + :max_k: int | None (default=None) + longest subword length; None uses the sequence length. + + Returns + ======= + :lc_df: pd.DataFrame + pandas DataFrame of shape 1 x 1, column ``LinguisticComplexity`` (0-1). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + L = len(sequence) + max_k = L if max_k is None else min(_validate_int("max_k", max_k, 1), L) + + observed = possible = 0 + for k in range(1, max_k + 1): + n_kmers = L - k + 1 + observed += len({sequence[i:i + k] for i in range(n_kmers)}) + #20^k overtakes L - k + 1 quickly; avoid huge integers + possible += n_kmers if k > math.log(n_kmers, 20) else min(20 ** k, n_kmers) + return _to_df([observed / possible], ["LinguisticComplexity"]) + +############################### Lempel-Ziv Complexity ##################################### + +def _lz76(sequence: str) -> int: + """Number of components in the LZ76 exhaustive-history parsing (Kaspar-Schuster scheme).""" + n = len(sequence) + if n == 1: + return 1 + c, i, k, l, k_max = 1, 0, 1, 1, 1 + while True: + if sequence[i + k - 1] == sequence[l + k - 1]: + k += 1 + if l + k > n: + c += 1 + break + else: + k_max = max(k, k_max) + i += 1 + if i == l: + #no earlier copy: the component ends here + c += 1 + l += k_max + if l + 1 > n: + break + i, k, k_max = 0, 1, 1 + else: + k = 1 + return c + + +def lempel_ziv_complexity(sequence: str) -> pd.DataFrame: + """ + Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence [4, 5]: the + number c(n) of new components found when the sequence is parsed from left to + right, each component being the shortest substring not producible by + copying from the preceding history. The normalised value for a 20-letter + alphabet is + + LZ_Normalized = c(n) x log_20(n) / n + + (0 for a single residue). + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :lz_df: pd.DataFrame + pandas DataFrame of shape 1 x 2, columns ``LZ_Complexity`` (int) and + ``LZ_Normalized``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + n = len(sequence) + c = _lz76(sequence) + return _to_df([c, c * math.log(n, 20) / n if n > 1 else 0.0], ["LZ_Complexity", "LZ_Normalized"]) diff --git a/protpy/utils.py b/protpy/utils.py index 9914f82..331f7f2 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -1154,6 +1154,54 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "wootton_federhen_complexity": { + "description": "Wootton-Federhen local complexity (K1, K2) over sliding windows and the SEG-trigger low-complexity fraction.", + "module": "complexity", + "category": "Complexity", + "abbreviation": "WF", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=12) — window length", + "k2_trigger": "float (default=2.2) — K2 trigger (bits)", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Wootton & Federhen (1993). Comput. Chem., 17(2), 149-163.", + "doi": "10.1016/0097-8485(93)85006-X", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "linguistic_complexity": { + "description": "Linguistic complexity: distinct subwords observed / maximum possible (ratio of sums over word lengths).", + "module": "complexity", + "category": "Complexity", + "abbreviation": "LC", + "parameters": { + "sequence": "str — protein sequence", + "max_k": "int | None (default=None) — longest word length (default L)", + }, + "output_shape": "1 x 1", + "speed": "Fast", + "reference": "Troyanskaya et al. (2002). Bioinformatics, 18(5), 679-688.", + "doi": "10.1093/bioinformatics/18.5.679", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "lempel_ziv_complexity": { + "description": "Lempel-Ziv (LZ76) complexity c(n) and its normalised value c(n)·log20(n)/n.", + "module": "complexity", + "category": "Complexity", + "abbreviation": "LZ", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 2", + "speed": "Fast", + "reference": "Lempel & Ziv (1976). IEEE Trans. Inf. Theory, 22(1), 75-81.", + "doi": "10.1109/TIT.1976.1055501", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_complexity.py b/tests/test_complexity.py new file mode 100644 index 0000000..2ce8e96 --- /dev/null +++ b/tests/test_complexity.py @@ -0,0 +1,67 @@ +########################################################################### +############### protPy - Complexity Module Tests (Phase 5) ############ +########################################################################### + +import math +import random +import unittest + +import protpy +from protpy.complexity import _lz76 + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" + + +class ProtpyComplexityTests(unittest.TestCase): + + def _check_invalid(self, func): + with self.assertRaises(TypeError): + func(None) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("AJX") + + def test_wootton_federhen(self): + df = protpy.wootton_federhen_complexity(SEQ) + self.assertEqual(list(df.columns), ["WF_K1_mean", "WF_K2_mean", "WF_K2_min", "WF_LowComplexity_frac"]) + #poly-A: K1 = K2 = 0, everything low complexity + self.assertEqual(protpy.wootton_federhen_complexity("A" * 30).values[0].tolist(), [0.0, 0.0, 0.0, 1.0]) + #12 distinct residues in one window: K2 = log2(12), K1 = log20(12!)/12 + df = protpy.wootton_federhen_complexity("ACDEFGHIKLMN") + self.assertEqual(df["WF_K2_mean"].iloc[0], round(math.log2(12), 3)) + self.assertEqual(df["WF_K1_mean"].iloc[0], round(math.log(math.factorial(12), 20) / 12, 3)) + self.assertEqual(df["WF_LowComplexity_frac"].iloc[0], 0.0) + #a poly-Q tract inside a complex sequence is partly flagged + frac = protpy.wootton_federhen_complexity(SEQ + "Q" * 20 + SEQ)["WF_LowComplexity_frac"].iloc[0] + self.assertTrue(0 < frac < 1) + self._check_invalid(protpy.wootton_federhen_complexity) + + def test_linguistic_complexity(self): + random.seed(0) + rand = "".join(random.choice(protpy.amino_acids) for _ in range(50)) + self.assertGreater(protpy.linguistic_complexity(rand).iloc[0, 0], 0.95) + self.assertLess(protpy.linguistic_complexity("A" * 50).iloc[0, 0], 0.1) + #"ACA": k=1 {A,C} of 2 possible (min(20,3)=3)... observed 2+2+1=5, possible 3+2+1=6 + self.assertEqual(protpy.linguistic_complexity("ACA").iloc[0, 0], round(5 / 6, 3)) + self.assertEqual(protpy.linguistic_complexity("ACA", max_k=1).iloc[0, 0], round(2 / 3, 3)) + self._check_invalid(protpy.linguistic_complexity) + + def test_lempel_ziv(self): + #Lempel & Ziv (1976) example 0001101001000101 parses as 0.001.10.100.1000.101 -> 6 + self.assertEqual(_lz76("0001101001000101".replace("0", "A").replace("1", "C")), 6) + self.assertEqual(_lz76("A" * 20), 2) + df = protpy.lempel_ziv_complexity(SEQ) + self.assertEqual(list(df.columns), ["LZ_Complexity", "LZ_Normalized"]) + c = df["LZ_Complexity"].iloc[0] + self.assertEqual(df["LZ_Normalized"].iloc[0], round(c * math.log(len(SEQ), 20) / len(SEQ), 3)) + self.assertEqual(protpy.lempel_ziv_complexity("A").values[0].tolist(), [1, 0.0]) + self._check_invalid(protpy.lempel_ziv_complexity) + + def test_shannon_reexport(self): + from protpy import complexity + self.assertIs(complexity.shannon_entropy, protpy.shannon_entropy) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index 334bbfb..efe46ab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -80,7 +80,7 @@ class ProtpyUtilsTests(unittest.TestCase): } VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order", - "Scales", "Physicochemical"} + "Scales", "Physicochemical", "Complexity"} VALID_SPEEDS = {"Fast", "Moderate", "Slow"} # ── Registry integrity ──────────────────────────────────────────────────────── From a931339a3e0ac48bc513fdcc6f5fc7c164841495 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:32:39 +0100 Subject: [PATCH 09/17] v1.5.0 phase 6: FFT and DCT signal descriptors --- protpy/__init__.py | 2 + protpy/scales.py | 3 +- protpy/signal.py | 121 +++++++++++++++++++++++++++++++++++++++++++ protpy/utils.py | 34 ++++++++++++ tests/test_signal.py | 67 ++++++++++++++++++++++++ tests/test_utils.py | 2 +- 6 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 protpy/signal.py create mode 100644 tests/test_signal.py diff --git a/protpy/__init__.py b/protpy/__init__.py index 08fe3b6..20f05f1 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -6,6 +6,7 @@ from .scales import * from .physicochemical import * from .complexity import * +from .signal import * from .utils import get_descriptor_info, list_descriptors, print_descriptor_info from .encode import all_descriptors, encode_dataset, read_sequences_from_file @@ -50,4 +51,5 @@ "charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density", "hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility", "wootton_federhen_complexity", "linguistic_complexity", "lempel_ziv_complexity", + "fft_descriptors", "dct_descriptors", ] \ No newline at end of file diff --git a/protpy/scales.py b/protpy/scales.py index f568a35..bd8e6f8 100644 --- a/protpy/scales.py +++ b/protpy/scales.py @@ -211,7 +211,8 @@ def auto_cross_covariance(sequence: str, scale: str | list[str] = "z_scales_5", values, columns = [], [] for l in range(1, lag + 1): #cov[j, k] = mean over i of z[i, j] * z[i + l, k] - cov = z[:-l].T @ z[l:] / (len(sequence) - l) + #einsum instead of matmul: macOS Accelerate BLAS raises spurious float warnings + cov = np.einsum("ij,ik->jk", z[:-l], z[l:]) / (len(sequence) - l) values += [cov[j, k] for j, k in pairs] columns += [f"ACC_{j + 1}_{k + 1}_lag{l}" for j, k in pairs] return _to_df(values, columns) diff --git a/protpy/signal.py b/protpy/signal.py new file mode 100644 index 0000000..a7e2aea --- /dev/null +++ b/protpy/signal.py @@ -0,0 +1,121 @@ +################################################################################ +############# Signal Processing ############## +################################################################################ + +from __future__ import annotations + +""" +Signal-processing descriptors of amino-acid property signals: binned FFT power +spectrum and DCT coefficients. numpy only. + +References +========== +[1] Cosic, I. (1994). Macromolecular bioactivity: is it resonant interaction + between macromolecules? Theory and applications. IEEE Trans Biomed Eng, + 41(12), 1101-1114. https://doi.org/10.1109/10.335859 +[2] Wang, Y., Ding, Y., Guo, F., Wei, L., & Tang, J. (2017). Improved detection + of DNA-binding proteins via compression technology on PSSM information. + PLoS ONE, 12(9), e0185587. https://doi.org/10.1371/journal.pone.0185587 + (DCT used as a sequence-length-invariant compression of a residue signal) +""" + +import numpy as np +import pandas as pd + +from ._constants import _validate_sequence, _validate_int +from ._helpers import _property_matrix, _to_df + +__all__ = ["fft_descriptors", "dct_descriptors"] + + +def _signal(sequence: str, scale: str | dict) -> tuple[np.ndarray, str]: + """Mean-centred single-value property signal and the name used in column labels.""" + values = _property_matrix(sequence, scale) + if values.shape[1] != 1: + raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") + x = values[:, 0] + return x - x.mean(), scale if isinstance(scale, str) else "custom" + +#################################### FFT ################################################## + +def fft_descriptors(sequence: str, scale: str | dict = "eiip", n_bins: int = 10) -> pd.DataFrame: + """ + Calculate a length-invariant binned power spectrum of a property signal, as + in the resonant recognition model of Cosic [1]. Each residue is replaced by + its scale value (default: electron-ion interaction potential, EIIP), the + signal is mean-centred, and its power spectrum is + + P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue + + The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width + bins (the last bin includes 0.5) and the mean power in each bin is returned; + bins containing no frequency are 0. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str | dict (default="eiip") + single-value scale name, AAindex1 accession code or {aa: value} dict. + :n_bins: int (default=10) + number of frequency bins. + + Returns + ======= + :fft_df: pd.DataFrame + pandas DataFrame of shape 1 x n_bins, columns ``FFT_{scale}_bin{1..n}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + n_bins = _validate_int("n_bins", n_bins, 1) + + x, name = _signal(sequence, scale) + power = np.abs(np.fft.rfft(x)) ** 2 / len(x) + freqs = np.fft.rfftfreq(len(x)) + #bin index of each frequency; f = 0.5 belongs to the last bin + bins = np.minimum((freqs / 0.5 * n_bins).astype(int), n_bins - 1) + sums = np.bincount(bins, weights=power, minlength=n_bins) + counts = np.bincount(bins, minlength=n_bins) + values = np.divide(sums, counts, out=np.zeros(n_bins), where=counts > 0) + return _to_df(list(values), [f"FFT_{name}_bin{i}" for i in range(1, n_bins + 1)]) + +#################################### DCT ################################################## + +def dct_descriptors(sequence: str, scale: str | dict = "kyte_doolittle", n_coeffs: int = 10) -> pd.DataFrame: + """ + Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II + discrete cosine transform (DCT-II) of a mean-centred property signal [2]: + + X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), + s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 + + X_0 is 0 because the signal is mean-centred. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str | dict (default="kyte_doolittle") + single-value scale name, AAindex1 accession code or {aa: value} dict. + :n_coeffs: int (default=10) + number of coefficients, at most the sequence length. + + Returns + ======= + :dct_df: pd.DataFrame + pandas DataFrame of shape 1 x n_coeffs, columns ``DCT_{scale}_{0..n-1}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + n_coeffs = _validate_int("n_coeffs", n_coeffs, 1) + if n_coeffs > len(sequence): + raise ValueError(f"n_coeffs ({n_coeffs}) cannot exceed the sequence length ({len(sequence)}).") + + x, name = _signal(sequence, scale) + L = len(x) + k = np.arange(n_coeffs)[:, None] + basis = np.cos(np.pi * k * (2 * np.arange(L) + 1) / (2 * L)) + scale_factors = np.where(k[:, 0] == 0, np.sqrt(1 / L), np.sqrt(2 / L)) + #elementwise sum instead of matmul: macOS Accelerate BLAS raises spurious float warnings + coeffs = scale_factors * (basis * x).sum(axis=1) + return _to_df(list(coeffs), [f"DCT_{name}_{i}" for i in range(n_coeffs)]) diff --git a/protpy/utils.py b/protpy/utils.py index 331f7f2..83ced37 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -1202,6 +1202,40 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "fft_descriptors": { + "description": "Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) in equal-width frequency bins.", + "module": "signal", + "category": "Signal", + "abbreviation": "FFT", + "parameters": { + "sequence": "str — protein sequence", + "scale": "str | dict (default='eiip') — single-value scale, AAindex code or dict", + "n_bins": "int (default=10) — number of frequency bins", + }, + "output_shape": "1 x n_bins (default 1 x 10)", + "speed": "Fast", + "reference": "Cosic (1994). IEEE Trans. Biomed. Eng., 41(12), 1101-1114.", + "doi": "10.1109/10.335859", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "dct_descriptors": { + "description": "First n orthonormal DCT-II coefficients of a mean-centred property signal (default Kyte-Doolittle).", + "module": "signal", + "category": "Signal", + "abbreviation": "DCT", + "parameters": { + "sequence": "str — protein sequence", + "scale": "str | dict (default='kyte_doolittle') — single-value scale, AAindex code or dict", + "n_coeffs": "int (default=10) — number of coefficients (<= L)", + }, + "output_shape": "1 x n_coeffs (default 1 x 10)", + "speed": "Fast", + "reference": "Wang et al. (2017). PLoS ONE, 12(9), e0185587.", + "doi": "10.1371/journal.pone.0185587", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_signal.py b/tests/test_signal.py new file mode 100644 index 0000000..de0b9e8 --- /dev/null +++ b/tests/test_signal.py @@ -0,0 +1,67 @@ +########################################################################### +############### protPy - Signal Module Tests (Phase 6) ########### +########################################################################### + +import math +import unittest + +import numpy as np + +import protpy + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" +#a two-valued scale turns A/C patterns into square waves +WAVE = {aa: (1.0 if aa == "A" else -1.0) for aa in protpy.amino_acids} + + +class ProtpySignalTests(unittest.TestCase): + + def test_fft(self): + df = protpy.fft_descriptors(SEQ) + self.assertEqual(list(df.columns), [f"FFT_eiip_bin{i}" for i in range(1, 11)]) + self.assertTrue((df.values >= 0).all()) + #period-2 signal: all energy at f = 0.5 (last bin) + df = protpy.fft_descriptors("AC" * 32, scale=WAVE) + self.assertEqual(list(df.columns)[0], "FFT_custom_bin1") + self.assertGreater(df["FFT_custom_bin10"].iloc[0], 0) + self.assertEqual(df.iloc[0, :9].sum(), 0) + #period-8 square wave: fundamental at f = 0.125 -> bin 3 holds the most power + df = protpy.fft_descriptors("AAAACCCC" * 8, scale=WAVE) + self.assertEqual(int(np.argmax(df.values[0])), 2) + #constant signal has no power + self.assertEqual(protpy.fft_descriptors("A" * 20).values.sum(), 0) + self.assertEqual(protpy.fft_descriptors(SEQ, n_bins=4).shape, (1, 4)) + with self.assertRaises(ValueError): + protpy.fft_descriptors(SEQ, n_bins=0) + with self.assertRaises(ValueError): + protpy.fft_descriptors(SEQ, scale="vhse") + + def test_dct(self): + df = protpy.dct_descriptors(SEQ) + self.assertEqual(list(df.columns), [f"DCT_kyte_doolittle_{i}" for i in range(10)]) + #explicit O(L^2) DCT-II + kd = protpy.composition.kyte_doolittle_ + x = np.array([kd[aa] for aa in SEQ]); x = x - x.mean() + L = len(x) + for k in range(10): + ref = sum(x[n] * math.cos(math.pi * k * (2 * n + 1) / (2 * L)) for n in range(L)) + ref *= math.sqrt((1 if k == 0 else 2) / L) + self.assertAlmostEqual(df.iloc[0, k], round(ref, 3), places=3) + #orthonormal: all L coefficients preserve the signal energy + full = protpy.dct_descriptors(SEQ, n_coeffs=L) + self.assertAlmostEqual((full.values ** 2).sum(), (x ** 2).sum(), delta=0.05) + with self.assertRaises(ValueError): + protpy.dct_descriptors("ACD", n_coeffs=4) + + def test_invalid(self): + for func in (protpy.fft_descriptors, protpy.dct_descriptors): + with self.assertRaises(TypeError): + func(3.0) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("AZ") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index efe46ab..ae43436 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -80,7 +80,7 @@ class ProtpyUtilsTests(unittest.TestCase): } VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order", - "Scales", "Physicochemical", "Complexity"} + "Scales", "Physicochemical", "Complexity", "Signal"} VALID_SPEEDS = {"Fast", "Moderate", "Slow"} # ── Registry integrity ──────────────────────────────────────────────────────── From f49af89a2d6d9802efa80bcaf64371745015d021 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:45:49 +0100 Subject: [PATCH 10/17] v1.5.0 phase 7: AGGRESCAN, Uversky/FoldIndex, disorder propensity, TM and signal-peptide heuristics, functional sites, PLAAC, antigenicity, cysteine features --- planning/BLOCKED.md | 13 +- protpy/__init__.py | 3 + protpy/composition.py | 9 +- protpy/data/aa_scales.json | 56 +++ protpy/data/structural_params.json | 135 ++++++ protpy/structural.py | 521 +++++++++++++++++++++ protpy/utils.py | 157 +++++++ tests/test_backwards_compat.py | 6 +- tests/test_data/structural_reference.fasta | 6 + tests/test_structural.py | 118 +++++ tests/test_utils.py | 2 +- 11 files changed, 1018 insertions(+), 8 deletions(-) create mode 100644 protpy/data/structural_params.json create mode 100644 protpy/structural.py create mode 100644 tests/test_data/structural_reference.fasta create mode 100644 tests/test_structural.py diff --git a/planning/BLOCKED.md b/planning/BLOCKED.md index 513ba2a..68dd1e8 100644 --- a/planning/BLOCKED.md +++ b/planning/BLOCKED.md @@ -13,6 +13,14 @@ Each entry names the table that is missing and where it would have to come from. | 31 | `membrane_position` | The Eisenberg 1984 hydrophobic-moment-plot boundary lines (globular / surface / transmembrane) | Eisenberg, Schwarz et al. 1984 J Mol Biol (10.1016/0022-2836(84)90309-7) is paywalled (403). | | 35 | `wilkinson_harrison_solubility` | λ1, λ2 (and the sign of λ2) and CV′ of the revised model | Davis et al. 1999 (Wiley) and Wilkinson & Harrison 1991 are paywalled. The Harrison lab calculator (biotech.ou.edu) fails TLS and the Internet Archive was offline. | | Phase 4 scales | `hessa`, `moon_fleming` and `aasi` single-value scales (used by `hydrophobicity_profile`, E6 `gravy(scale=)` and E7 `hydrophobic_moment(scale=)`) | Hessa 2005 ΔG_app (Nature, paywalled); Moon & Fleming 2011 (values are in SI Table S1, not available); Juretić 2009 AASI (paywalled). Available scales: kyte_doolittle, hopp_woods, eisenberg, wimley_white, engelman, domcut, plus any AAindex1 code. | +| 47 (partial) | `signal_peptide_features`: `SP_vonHeijne_max` and `SP_small_m3_m1` | The von Heijne 1986 eukaryotic cleavage-site weight matrix | The NAR 1986 paper (PMC311474) is only a scanned PDF behind a browser challenge. `SP_small_m3_m1` depends on the matrix-selected site. Shipped: `SP_nregion_charge`, `SP_hregion_maxKD`. | +| 48 (partial) | `functional_site_counts`: monopartite/bipartite NLS and NES patterns | Verified regex consensus patterns | Kosugi 2009 (PMC2695404) defines NLS classes as activity profiles, not regexes; la Cour 2004 is behind a Cloudflare check. Shipped: N-glycosylation, ER retention and PTS1 as the PROSITE patterns PS00001, PS00014 and PS00342 (which are broader than the plan's narrower variants), plus tryptic sites. | +| 49 | `chou_fasman` | The full Chou–Fasman 1978 nucleation/extension/overlap rules and turn threshold | Chou & Fasman 1974/1978 are paywalled. Secondary sources are incomplete and disagree on the turn cutoff (7.5e-3 vs 7.5e-5). The Pα/Pβ/Pt and bend-frequency tables are in AAindex (CHOP780201–203, 212–215), but the rules can't be verified. | +| 50 | `gor` | GOR I 3×17×20 directional information tables | Garnier, Osguthorpe & Robson 1978 J Mol Biol is paywalled. | +| 51 | `coiled_coil` | Lupas 1991 MTK profile matrix and Gaussian parameters | Science 1991 is paywalled; the only machine-readable copy (ncoils) is GPL-2.0+. | +| 52 | `fold_amyloid` | Garbuzynskiy 2010 expected-contacts scale and threshold | Bioinformatics 2010 is behind a Cloudflare check and the FoldAmyloid server didn't respond. Campen 2008 lists a "FoldUnfold" contact scale, but it can't be confirmed to be the FoldAmyloid scale. | +| 54 (partial) | `antigenicity_surface`: Emini, Karplus–Schulz, Kolaskar–Tongaonkar | Emini surface-probability values/formula; Karplus–Schulz rigid-neighbour definition; Kolaskar–Tongaonkar scale | Emini 1985 (PMC255070) is a scanned PDF behind a browser challenge; Karplus & Schulz 1985 and Kolaskar & Tongaonkar 1990 are paywalled, and none of the three is on ExPASy ProtScale. Shipped: Parker (window 7) and Janin profiles. | +| 46 (partial) | `transmembrane_features`: `TM_min_dGapp` | Hessa 2005 position-independent ΔG_app scale | Nature 2005 is paywalled. The other four columns are shipped. | ## Shipped with caveats @@ -27,8 +35,11 @@ Each entry names the table that is missing and where it would have to come from. - **`kappa`.** δ_max is found by enumerating the segregated arrangements (positive block, negative block, neutral residues split over the three gaps), as the plan specifies, because the Das & Pappu SI is behind a browser challenge. It reproduces the published κ of the patterned (Glu-Lys)25 variants sv1, sv2, sv7, sv8, sv19 and sv30 to within 0.002. The irregular variants could not be read reliably from the Fig. 1 image and are not tested. - **`isoelectric_point(pka_scale="emboss")`.** Uses EMBOSS 6.6.0 `Epk.dat`, whose N-terminal pKa differs from the 8.6 that Kozlowski 2016 Table 1 attributes to EMBOSS. - **`flexibility`.** Uses Vihinen's average parameter set (AAindex VINM940101, the same table Biopython uses) with the window-9, 0.25-edge weighting. The neighbour-dependent sets (VINM940102–104) need Vihinen's definition of a rigid neighbour, which is paywalled, so they are not used. -- **`hydrophobic_moment(scale=None)`.** Keeps protpy's existing table (A = 0.62, the "Eisenberg consensus" values used by most moment tools), so the default output is unchanged. `scale="eisenberg"` is AAindex EISD840101 (Eisenberg 1984 Ann Rev, A = 0.25); the two published "consensus" scales differ. +- **`hydrophobic_moment(scale=None)`.** Keeps protpy's existing table, so the default output is unchanged. That table is the Eisenberg, Schwarz, Komarony & Wall 1984 J Mol Biol normalised consensus scale (A = 0.62; confirmed against ExPASy ProtScale). `scale="eisenberg"` is AAindex EISD840101 (Eisenberg 1984 Annu Rev Biochem, A = 0.25), a different published consensus scale. - **`half_life`.** The E. coli value for an N-terminal Pro is "?" in ProtParam and is returned as NaN. - **`linguistic_complexity`.** The Troyanskaya 2002 full text is behind a Cloudflare check. The ratio-of-sums form in the plan is used: Σ distinct k-mers / Σ min(20^k, L−k+1). This is consistent with the abstract, which says the method counts "the number of subwords present" with suffix trees. Some sources use a product-of-ratios form instead. - **`wootton_federhen_complexity`.** Implements the SEG trigger stage only (K2 ≤ trigger windows), not SEG's extension threshold or interval optimisation, as the plan specifies. +- **`aggrescan`.** The a3v values and the hot-spot rules (HST −0.02, ≥5 residues, no Pro, trapezoidal areas) come from the paper's Additional files 1–2. The rule that picks the window size from sequence length, and the charge-corrected values for the two terminal residues, aren't in the published material. So `window` is a parameter (default 5) and the terminal residues take the edge-window value. Aβ42 gives the paper's two hot spots. +- **`fold_index`.** The default window of 51 comes from the plan; Prilusky 2005 and the FoldIndex server couldn't be accessed to confirm it. The 2.785 and 1.151 constants are confirmed via Das & Pappu 2013 (PMC). +- **`plaac`.** Ported from the MIT-licensed PLAAC Java source. It reproduces PLAAC 1.1.0's MOT3 reference output exactly (LLR 40.751, COREscore 40.751, PRDlen 296). - **`atomic_composition`.** By default it matches Pfeature (no water removed); `subtract_water=True` gives the true peptide formula. Pfeature's W and Y formulas each have one H too many, and protpy uses the correct formulas. diff --git a/protpy/__init__.py b/protpy/__init__.py index 20f05f1..327cc02 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -7,6 +7,7 @@ from .physicochemical import * from .complexity import * from .signal import * +from .structural import * from .utils import get_descriptor_info, list_descriptors, print_descriptor_info from .encode import all_descriptors, encode_dataset, read_sequences_from_file @@ -52,4 +53,6 @@ "hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility", "wootton_federhen_complexity", "linguistic_complexity", "lempel_ziv_complexity", "fft_descriptors", "dct_descriptors", + "aggrescan", "uversky_charge_hydropathy", "fold_index", "disorder_propensity", "transmembrane_features", + "signal_peptide_features", "functional_site_counts", "plaac", "antigenicity_surface", "cysteine_features", ] \ No newline at end of file diff --git a/protpy/composition.py b/protpy/composition.py index 57088e6..68be4e6 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -1255,10 +1255,11 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100 Default angle of 100° corresponds to an alpha-helix; 160° is used for beta-strands (Eisenberg et al. 1984). - By default H is protpy's normalised hydrophobicity table (the values used - for PseAAC, A = 0.62, also widely quoted as the "Eisenberg consensus" scale). - ``scale`` selects another single-value scale, e.g. "eisenberg" (AAindex - EISD840101, Eisenberg 1984, A = 0.25), "kyte_doolittle" or any AAindex1 code. + By default H is protpy's normalised hydrophobicity table (A = 0.62), which is + the normalised consensus scale of Eisenberg, Schwarz, Komarony & Wall (1984, + J Mol Biol 179:125-142) as listed by ExPASy ProtScale. ``scale`` selects + another single-value scale, e.g. "eisenberg" (AAindex EISD840101, Eisenberg + 1984 Annu Rev Biochem, A = 0.25), "kyte_doolittle" or any AAindex1 code. Parameters ========== diff --git a/protpy/data/aa_scales.json b/protpy/data/aa_scales.json index 95f9e70..1419328 100644 --- a/protpy/data/aa_scales.json +++ b/protpy/data/aa_scales.json @@ -1814,5 +1814,61 @@ "reference": "Vihinen, M., Torkkila, E. and Riikonen, P. 'Accuracy of protein flexibility predictions' Proteins 19, 141-149 (1994)", "doi": "10.1002/prot.340190207", "source_note": "AAindex1 record VINM940101 (average normalised B-values of Vihinen et al. 1994; identical to Biopython ProtParamData.Flex); copied 2026-10-12." + }, + "parker": { + "name": "HPLC parameter (Parker et al., 1986)", + "values": { + "A": 2.1, + "C": 1.4, + "D": 10.0, + "E": 7.8, + "F": -9.2, + "G": 5.7, + "H": 2.1, + "I": -8.0, + "K": 5.7, + "L": -9.2, + "M": -4.2, + "N": 7.0, + "P": 2.1, + "Q": 6.0, + "R": 4.2, + "S": 6.5, + "T": 5.2, + "V": -3.7, + "W": -10.0, + "Y": -1.9 + }, + "reference": "Parker, J.M.R., Guo, D. and Hodges, R.S. 'New hydrophilicity scale derived from high-performance liquid chromatography peptide retention data: Correlation of predicted surface residues with antigencity and x-ray-derived accessible sites' Biochemistry 25, 5425-5432 (1986)", + "doi": "10.1021/bi00367a013", + "source_note": "AAindex1 record PARJ860101, identical to ExPASy ProtScale 'Hphob. / Parker'; copied 2026-10-12." + }, + "janin": { + "name": "Transfer free energy (Janin, 1979)", + "values": { + "A": 0.3, + "C": 0.9, + "D": -0.6, + "E": -0.7, + "F": 0.5, + "G": 0.3, + "H": -0.1, + "I": 0.7, + "K": -1.8, + "L": 0.5, + "M": 0.4, + "N": -0.5, + "P": -0.3, + "Q": -0.7, + "R": -1.4, + "S": -0.1, + "T": -0.2, + "V": 0.6, + "W": 0.3, + "Y": -0.4 + }, + "reference": "Janin, J. 'Surface and inside volumes in globular proteins' Nature 277, 491-492 (1979)", + "doi": "10.1038/277491a0", + "source_note": "AAindex1 record JANJ790102, identical to ExPASy ProtScale 'Hphob. / Janin'; copied 2026-10-12." } } \ No newline at end of file diff --git a/protpy/data/structural_params.json b/protpy/data/structural_params.json new file mode 100644 index 0000000..4c82ff6 --- /dev/null +++ b/protpy/data/structural_params.json @@ -0,0 +1,135 @@ +{ + "aggrescan_a3v": { + "name": "AGGRESCAN amino-acid aggregation propensity (a3v)", + "values": { + "A": -0.036, + "C": 0.604, + "D": -1.836, + "E": -1.412, + "F": 1.754, + "G": -0.535, + "H": -1.033, + "I": 1.822, + "K": -0.931, + "L": 1.38, + "M": 0.91, + "N": -1.302, + "P": -0.334, + "Q": -1.231, + "R": -1.24, + "S": -0.294, + "T": -0.159, + "V": 1.594, + "W": 1.037, + "Y": 1.159 + }, + "reference": "Conchillo-Sol\u00e9 O. et al. (2007). AGGRESCAN: a server for the prediction and evaluation of 'hot spots' of aggregation in polypeptides. BMC Bioinformatics, 8, 65.", + "doi": "10.1186/1471-2105-8-65", + "source_note": "Additional file 1 of the paper (12859_2006_1437_MOESM1_ESM.pdf), parsed 2026-10-12. Hot-spot threshold -0.02 and hot-spot rules from Additional file 2." + }, + "top_idp": { + "name": "TOP-IDP disorder propensity scale", + "values": { + "A": 0.06, + "C": 0.02, + "D": 0.192, + "E": 0.736, + "F": -0.697, + "G": 0.166, + "H": 0.303, + "I": -0.486, + "K": 0.586, + "L": -0.326, + "M": -0.397, + "N": 0.007, + "P": 0.987, + "Q": 0.318, + "R": 0.18, + "S": 0.341, + "T": 0.059, + "V": -0.121, + "W": -0.884, + "Y": -0.51 + }, + "reference": "Campen A. et al. (2008). TOP-IDP-scale: a new amino acid scale measuring propensity for intrinsic disorder. Protein Pept Lett, 15(9), 956-963.", + "doi": "10.2174/092986608785849164", + "source_note": "Table 2 of the paper (PMC2676888), parsed 2026-10-12." + }, + "disorder_residue_classes": { + "name": "Order- and disorder-promoting residues", + "values": { + "disorder_promoting": "ARGQSPEK", + "order_promoting": "WCFIYVLN" + }, + "reference": "Dunker A.K. et al. (2001). Intrinsically disordered protein. J Mol Graph Model, 19(1), 26-59.", + "doi": "10.1016/S1093-3263(00)00138-8", + "source_note": "Residue lists as restated in Campen et al. 2008 (PMC2676888, Introduction), which cites Dunker 2001; 2026-10-12." + }, + "plaac": { + "name": "PLAAC prion-like domain model (S. cerevisiae, 28 prion domains)", + "values": { + "fg": { + "A": 0.04865, + "C": 0.00219, + "D": 0.01638, + "E": 0.00783, + "F": 0.02537, + "G": 0.07603, + "H": 0.0181, + "I": 0.02018, + "K": 0.01641, + "L": 0.02639, + "M": 0.02975, + "N": 0.25885, + "P": 0.05126, + "Q": 0.15178, + "R": 0.025, + "S": 0.10988, + "T": 0.03841, + "V": 0.01972, + "W": 0.00157, + "Y": 0.05624 + }, + "bg": { + "A": 0.055, + "C": 0.0126, + "D": 0.0586, + "E": 0.0655, + "F": 0.0441, + "G": 0.0498, + "H": 0.0217, + "I": 0.0655, + "K": 0.0735, + "L": 0.095, + "M": 0.0207, + "N": 0.0615, + "P": 0.0438, + "Q": 0.0396, + "R": 0.0444, + "S": 0.0899, + "T": 0.0592, + "V": 0.0556, + "W": 0.0104, + "Y": 0.0337 + }, + "transition": [ + [ + 0.999, + 0.001 + ], + [ + 0.02, + 0.98 + ] + ], + "initial": [ + 0.9524, + 0.0476 + ], + "epsilon": 1e-05 + }, + "reference": "Lancaster A.K. et al. (2014). PLAAC: a web and command-line application to identify proteins with prion-like amino acid composition. Bioinformatics, 30(17), 2501-2502.", + "doi": "10.1093/bioinformatics/btu310", + "source_note": "Ported 2026-10-12 from the PLAAC source (github.com/whitehead/plaac, cli/src/plaac.java: prd_freq_scer_28, bg_freq_scer, prionhmm1). MIT License, Copyright (c) 2009-2014 Whitehead Institute for Biomedical Research; (c) 2011 Boston Biomedical Research Institute; (c) 2013 University of Massachusetts Medical School." + } +} \ No newline at end of file diff --git a/protpy/structural.py b/protpy/structural.py new file mode 100644 index 0000000..ab97afc --- /dev/null +++ b/protpy/structural.py @@ -0,0 +1,521 @@ +################################################################################ +############# Structural & Functional Heuristics ############## +################################################################################ + +from __future__ import annotations + +""" +Sequence-based structural and functional heuristics: aggregation hot spots +(AGGRESCAN), charge-hydropathy disorder (Uversky, FoldIndex), disorder +propensity, transmembrane and signal-peptide features, motif counts, +prion-like domains (PLAAC), antigenicity profiles and cysteine features. + +References +========== +[1] Conchillo-Solé, O. et al. (2007). AGGRESCAN: a server for the prediction and + evaluation of "hot spots" of aggregation in polypeptides. BMC Bioinformatics, + 8, 65. https://doi.org/10.1186/1471-2105-8-65 +[2] Uversky, V. N., Gillespie, J. R., & Fink, A. L. (2000). Why are "natively + unfolded" proteins unstructured under physiologic conditions? Proteins, + 41(3), 415-427. https://doi.org/10.1002/1097-0134(20001115)41:3<415::AID-PROT130>3.0.CO;2-7 +[3] Prilusky, J. et al. (2005). FoldIndex: a simple tool to predict whether a + given protein sequence is intrinsically unfolded. Bioinformatics, 21(16), + 3435-3438. https://doi.org/10.1093/bioinformatics/bti537 +[4] Dunker, A. K. et al. (2001). Intrinsically disordered protein. J Mol Graph + Model, 19(1), 26-59. https://doi.org/10.1016/S1093-3263(00)00138-8 +[5] Campen, A. et al. (2008). TOP-IDP-scale: a new amino acid scale measuring + propensity for intrinsic disorder. Protein Pept Lett, 15(9), 956-963. + https://doi.org/10.2174/092986608785849164 +[6] Kyte, J., & Doolittle, R. F. (1982). A simple method for displaying the + hydropathic character of a protein. J Mol Biol, 157(1), 105-132. + https://doi.org/10.1016/0022-2836(82)90515-0 +[7] von Heijne, G. (1986). The distribution of positively charged residues in + bacterial inner membrane proteins correlates with the trans-membrane + topology. EMBO J, 5(11), 3021-3027. + https://doi.org/10.1002/j.1460-2075.1986.tb04601.x +[8] von Heijne, G. (1983). Patterns of amino acids near signal-sequence cleavage + sites. Eur J Biochem, 133(1), 17-21. + https://doi.org/10.1111/j.1432-1033.1983.tb07424.x +[9] Sigrist, C. J. A. et al. (2013). New and continuing developments at PROSITE. + Nucleic Acids Res, 41(D1), D344-D347. https://doi.org/10.1093/nar/gks1067 +[10] Olsen, J. V., Ong, S.-E., & Mann, M. (2004). Trypsin cleaves exclusively + C-terminal to arginine and lysine residues. Mol Cell Proteomics, 3(6), + 608-614. https://doi.org/10.1074/mcp.T400003-MCP200 +[11] Lancaster, A. K., Nutter-Upham, A., Lindquist, S., & King, O. D. (2014). + PLAAC: a web and command-line application to identify proteins with + prion-like amino acid composition. Bioinformatics, 30(17), 2501-2502. + https://doi.org/10.1093/bioinformatics/btu310 +[12] Parker, J. M. R., Guo, D., & Hodges, R. S. (1986). New hydrophilicity scale + derived from high-performance liquid chromatography peptide retention data. + Biochemistry, 25(19), 5425-5432. https://doi.org/10.1021/bi00367a013 +[13] Janin, J. (1979). Surface and inside volumes in globular proteins. Nature, + 277, 491-492. https://doi.org/10.1038/277491a0 +[14] Fomenko, D. E., & Gladyshev, V. N. (2003). Identity and functions of CxxC- + derived motifs. Biochemistry, 42(38), 11214-11225. https://doi.org/10.1021/bi034459s +[15] Das, R. K., & Pappu, R. V. (2013). Conformations of intrinsically disordered + proteins are influenced by linear sequence distributions of oppositely + charged residues. PNAS, 110(33), 13392-13397. + https://doi.org/10.1073/pnas.1304749110 +""" + +import math +import re + +import numpy as np +import pandas as pd + +from ._constants import _validate_sequence, _validate_int +from ._helpers import (_charge_vector, _load_json_table, _property_matrix, _profile_stats, _runs, + _sliding_windows, _to_df) +from .composition import kyte_doolittle_ + +__all__ = [ + "aggrescan", "uversky_charge_hydropathy", "fold_index", "disorder_propensity", + "transmembrane_features", "signal_peptide_features", "functional_site_counts", "plaac", + "antigenicity_surface", "cysteine_features", +] + +################################### AGGRESCAN ############################################# + +#np.trapz was renamed np.trapezoid in numpy 2.0 +_trapz = getattr(np, "trapezoid", None) or np.trapz + +#hot-spot threshold: SwissProt-frequency-weighted mean a3v [1] +_AGGRESCAN_HST = -0.02 + +def aggrescan(sequence: str, window: int = 5) -> pd.DataFrame: + """ + Calculate AGGRESCAN aggregation "hot spot" descriptors [1]. Each residue is + given its experimental aggregation propensity a3v; the a4v profile (AP) is the + a3v mean over a sliding window, assigned to the window's central residue. + Off-centre N- and C-terminal residues take the first and last window values. + A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot + threshold HST = -0.02 and containing no proline. Areas use trapezoidal + integration of the AP with HST as the zero axis. + + Outputs: + a3vSA mean a3v over the sequence + nHS number of hot spots; NnHS = nHS / L x 100 + AAT area of the AP above HST over the whole sequence; AATr = AAT / L + THSA total hot-spot area (sum of HSA); THSAr = THSA / L + TA total area of the AP (HST as zero axis, signed) + a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 + a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) + HSA_max largest single hot-spot area (0 if none) + + AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and + gives the two extreme residues specific charge-corrected values; neither + rule is given in the published supplementary material, so ``window`` is a + parameter (default 5) and the extreme residues are treated like the other + off-centre residues. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=5) + odd sliding-window length (AGGRESCAN uses 5, 7, 9 or 11); reduced to the + largest odd length that fits a short sequence. + + Returns + ======= + :aggrescan_df: pd.DataFrame + pandas DataFrame of shape 1 x 12, columns prefixed ``AGG_``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + if window % 2 == 0: + raise ValueError(f"window must be odd, got {window}.") + L = len(sequence) + window = min(window, L if L % 2 else L - 1) + + a3v = _property_matrix(sequence, _load_json_table("structural_params.json", "aggrescan_a3v")["values"])[:, 0] + centres = _sliding_windows(a3v, window) + half = window // 2 + a4v = np.concatenate([np.full(half, centres[0]), centres, np.full(half, centres[-1])]) + above = a4v - _AGGRESCAN_HST + + #hot spots: >= 5 consecutive residues above HST, broken by proline + is_hs = (above > 0) & np.array([aa != "P" for aa in sequence]) + hot_spots = _runs(is_hs, min_len=5) + hsa = [_trapz(above[s:e]) for s, e in hot_spots] + aat = float(_trapz(np.clip(above, 0, None))) + thsa = float(sum(hsa)) + values = [a3v.mean(), len(hot_spots), len(hot_spots) / L * 100, aat, aat / L, thsa, thsa / L, + float(_trapz(above)), a4v.sum(), a4v.sum() / L * 100, + float(np.mean([a4v[s:e].mean() for s, e in hot_spots])) if hot_spots else 0.0, + max(hsa) if hsa else 0.0] + columns = ["AGG_a3vSA", "AGG_nHS", "AGG_NnHS", "AGG_AAT", "AGG_AATr", "AGG_THSA", "AGG_THSAr", + "AGG_TA", "AGG_a4vSS", "AGG_Na4vSS", "AGG_a4vAHS", "AGG_HSA_max"] + return _to_df(values, columns) + +######################### Uversky Charge-Hydropathy / FoldIndex ########################### + +def _mean_hydropathy(sequence: str) -> float: + """Mean Kyte-Doolittle hydropathy rescaled to 0-1, (KD + 4.5) / 9 [2].""" + return sum((kyte_doolittle_[aa] + 4.5) / 9 for aa in sequence) / len(sequence) + + +def uversky_charge_hydropathy(sequence: str) -> pd.DataFrame: + """ + Calculate the charge-hydropathy descriptors of Uversky et al. [2]: + + = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) + = |n+ - n-| / L (K/R positive, D/E negative) + HQ = 2.785 - - 1.151 + + The boundary = 2.785 - 1.151 separates natively unfolded from folded + proteins (as restated in [15]); ``UV_Unfolded`` is 1 when HQ < 0. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :uversky_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``UV_MeanH``, ``UV_MeanR``, + ``UV_HQ`` and ``UV_Unfolded``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + h = _mean_hydropathy(sequence) + r = abs(_charge_vector(sequence).mean()) + hq = 2.785 * h - r - 1.151 + return _to_df([h, r, hq, int(hq < 0)], ["UV_MeanH", "UV_MeanR", "UV_HQ", "UV_Unfolded"]) + + +def fold_index(sequence: str, window: int = 51) -> pd.DataFrame: + """ + Calculate FoldIndex profile descriptors [3]. For each window of length + ``window``: + + FI = 2.785 - || - 1.151 + + with and as in ``uversky_charge_hydropathy`` but computed over the + window. Residues covered by at least one window with FI < 0 are counted as + disordered. A window longer than the sequence uses the whole sequence, so + FI then equals ``UV_HQ``. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=51) + window length. + + Returns + ======= + :fold_index_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``FI_mean``, ``FI_min``, + ``FI_frac_disordered`` and ``FI_longest_disordered`` (residues). + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = min(_validate_int("window", window, 1), len(sequence)) + + h = _sliding_windows([(kyte_doolittle_[aa] + 4.5) / 9 for aa in sequence], window) + r = np.abs(_sliding_windows(_charge_vector(sequence), window)) + fi = 2.785 * h - r - 1.151 + disordered = np.zeros(len(sequence), dtype=bool) + for start in np.flatnonzero(fi < 0): + disordered[start:start + window] = True + runs = _runs(disordered) + longest = max((e - s for s, e in runs), default=0) + return _to_df([fi.mean(), fi.min(), disordered.mean(), longest], + ["FI_mean", "FI_min", "FI_frac_disordered", "FI_longest_disordered"]) + +################################ Disorder Propensity ###################################### + +def disorder_propensity(sequence: str) -> pd.DataFrame: + """ + Calculate disorder-propensity descriptors: the fractions of + disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting + (W, C, F, I, Y, V, L, N) residues [4], and the mean TOP-IDP value [5] + (higher = more disorder-prone). + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :disorder_df: pd.DataFrame + pandas DataFrame of shape 1 x 3, columns ``DP_frac_disorder_promoting``, + ``DP_frac_order_promoting`` and ``DP_TOP_IDP_mean``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + classes = _load_json_table("structural_params.json", "disorder_residue_classes")["values"] + top_idp = _load_json_table("structural_params.json", "top_idp")["values"] + L = len(sequence) + return _to_df([sum(aa in classes["disorder_promoting"] for aa in sequence) / L, + sum(aa in classes["order_promoting"] for aa in sequence) / L, + sum(top_idp[aa] for aa in sequence) / L], + ["DP_frac_disorder_promoting", "DP_frac_order_promoting", "DP_TOP_IDP_mean"]) + +############################## Transmembrane Features ##################################### + +def transmembrane_features(sequence: str, window: int = 19, kd_threshold: float = 1.6) -> pd.DataFrame: + """ + Calculate heuristic transmembrane-segment features from the Kyte-Doolittle + hydropathy profile [6, 7]. Windows of length ``window`` with mean hydropathy + above ``kd_threshold`` (1.6 for 19-residue windows in [6]) mark putative + transmembrane segments; overlapping windows are merged into segments. + + TM_n_segments number of merged segments + TM_max_KD maximum window hydropathy + TM_frac_in_TM fraction of residues inside a segment + TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment + + This is a hydropathy heuristic, not a topology predictor. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=19) + hydropathy window length. + :kd_threshold: float (default=1.6) + mean hydropathy threshold. + + Returns + ======= + :tm_df: pd.DataFrame + pandas DataFrame of shape 1 x 4, columns ``TM_n_segments``, + ``TM_max_KD``, ``TM_frac_in_TM`` and ``TM_positive_inside_bias``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = min(_validate_int("window", window, 1), len(sequence)) + + profile = _sliding_windows([kyte_doolittle_[aa] for aa in sequence], window) + in_tm = np.zeros(len(sequence), dtype=bool) + for start in np.flatnonzero(profile > kd_threshold): + in_tm[start:start + window] = True + segments = _runs(in_tm) + bias = 0 + if segments: + s, e = segments[0] + bias = sum(aa in "KR" for aa in sequence[max(0, s - 15):s]) - sum(aa in "KR" for aa in sequence[e:e + 15]) + return _to_df([len(segments), profile.max(), in_tm.mean(), bias], + ["TM_n_segments", "TM_max_KD", "TM_frac_in_TM", "TM_positive_inside_bias"]) + +############################## Signal Peptide Features #################################### + +def signal_peptide_features(sequence: str, n_term: int = 30) -> pd.DataFrame: + """ + Calculate simple signal-peptide features of the N-terminus [8]: + + SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) + SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues + + The von Heijne cleavage-site weight matrix is not included. + + Parameters + ========== + :sequence: str + protein sequence. + :n_term: int (default=30) + length of the N-terminal region examined. + + Returns + ======= + :sp_df: pd.DataFrame + pandas DataFrame of shape 1 x 2, columns ``SP_nregion_charge`` and + ``SP_hregion_maxKD``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + n_term = _validate_int("n_term", n_term, 1) + + region = sequence[:n_term] + charge = _charge_vector(sequence[:5]).sum() + h = _sliding_windows([kyte_doolittle_[aa] for aa in region], 8).max() + return _to_df([int(charge), h], ["SP_nregion_charge", "SP_hregion_maxKD"]) + +############################## Functional Site Counts ##################################### + +#PROSITE patterns [9] and the trypsin rule [10]; lookaheads count overlapping matches +functional_motifs_ = { + "N_glycosylation": r"(?=N[^P][ST][^P])", #PS00001 N-{P}-[ST]-{P} + "ER_retention": r"[KRHQSA][DENQ]EL$", #PS00014 [KRHQSA]-[DENQ]-E-L> + "PTS1": r"[STAGCN][RKH][LIVMAFY]$", #PS00342 [STAGCN]-[RKH]-[LIVMAFY]> + "tryptic_site": r"[KR](?!P)", #trypsin: after K/R, not before P +} + +def functional_site_counts(sequence: str) -> pd.DataFrame: + """ + Count functional sequence motifs and their density per 100 residues: + N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal + (PS00014) and peroxisomal targeting signal 1 (PS00342) [9], and tryptic + cleavage sites (after K/R, not before P) [10]. Overlapping matches are + counted. These complement the default motifs of ``motif_composition``. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :sites_df: pd.DataFrame + pandas DataFrame of shape 1 x 8, columns ``FS_{motif}`` (counts) and + ``FS_{motif}_per100``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + counts = {name: len(re.findall(pattern, sequence)) for name, pattern in functional_motifs_.items()} + values = list(counts.values()) + [c / len(sequence) * 100 for c in counts.values()] + columns = [f"FS_{n}" for n in counts] + [f"FS_{n}_per100" for n in counts] + return _to_df(values, columns) + +###################################### PLAAC ############################################## + +def _plaac_model() -> tuple[dict, dict, np.ndarray, np.ndarray]: + """Emission log-probabilities (bg, fg), log transition matrix and log initial probabilities.""" + p = _load_json_table("structural_params.json", "plaac")["values"] + eps = p["epsilon"] + + def _normalise(freqs: dict) -> dict: + #as PLAAC: normalise, add epsilon for X and *, normalise again + total = sum(freqs.values()) + freqs = {aa: v / total for aa, v in freqs.items()} + total = 1 + 2 * eps + return {aa: v / total for aa, v in freqs.items()} + + return (_normalise(p["bg"]), _normalise(p["fg"]), np.log(np.array(p["transition"])), + np.log(np.array(p["initial"]))) + + +def _viterbi(sequence: str, bg: dict, fg: dict, log_t: np.ndarray, log_i: np.ndarray) -> np.ndarray: + """Most likely background (0) / prion-like (1) state path of the two-state HMM.""" + emit = np.log(np.array([[bg[aa], fg[aa]] for aa in sequence])) + score = log_i + emit[0] + back = np.zeros((len(sequence), 2), dtype=int) + for i in range(1, len(sequence)): + cand = score[:, None] + log_t + back[i] = np.argmax(cand, axis=0) + score = cand[back[i], [0, 1]] + emit[i] + path = np.zeros(len(sequence), dtype=int) + path[-1] = int(np.argmax(score)) + for i in range(len(sequence) - 1, 0, -1): + path[i - 1] = back[i, path[i]] + return path + + +def _max_window_sum(values: np.ndarray, width: int) -> float: + csum = np.concatenate([[0.0], np.cumsum(values)]) + return float((csum[width:] - csum[:-width]).max()) + + +def plaac(sequence: str, core_length: int = 60) -> pd.DataFrame: + """ + Calculate prion-like domain descriptors with the PLAAC model [11], ported from + the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each + residue has the log-likelihood ratio LLR = ln(f_prion / f_background). + + PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues + PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse + PLAAC_PrD_length length of the prion-like run containing the best core + PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + + As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than + ``core_length`` / no prion-like run of that length); PrD_length is then 0. + + Parameters + ========== + :sequence: str + protein sequence. + :core_length: int (default=60) + minimal prion-like domain (core) length. + + Returns + ======= + :plaac_df: pd.DataFrame + pandas DataFrame of shape 1 x 4. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + core_length = _validate_int("core_length", core_length, 1) + + bg, fg, log_t, log_i = _plaac_model() + llr = np.array([math.log(fg[aa] / bg[aa]) for aa in sequence]) + window_llr = _max_window_sum(llr, core_length) if len(sequence) >= core_length else np.nan + + core, prd_length = np.nan, 0 + for s, e in _runs(_viterbi(sequence, bg, fg, log_t, log_i) == 1, min_len=core_length): + score = _max_window_sum(llr[s:e], core_length) + if np.isnan(core) or score > core: + core, prd_length = score, e - s + return _to_df([window_llr, core, prd_length, int(prd_length > 0)], + ["PLAAC_LLR", "PLAAC_COREscore", "PLAAC_PrD_length", "PLAAC_has_PrD"]) + +############################### Antigenicity / Surface #################################### + +def antigenicity_surface(sequence: str, window: int = 7) -> pd.DataFrame: + """ + Summarise hydrophilicity/surface profiles associated with antigenic sites: + the Parker HPLC hydrophilicity scale averaged over 7-residue windows [12] and + the Janin free energy of transfer from inside to outside of a globular + protein [13] over the same window. Each profile is reduced to its mean, max, + min and standard deviation. + + Parameters + ========== + :sequence: str + protein sequence. + :window: int (default=7) + window length (7 as in Parker et al.). + + Returns + ======= + :antigenicity_df: pd.DataFrame + pandas DataFrame of shape 1 x 8, columns ``PARKER_{stat}`` and ``JANIN_{stat}``. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + window = _validate_int("window", window, 1) + stats = {} + for name in ("parker", "janin"): + profile = _sliding_windows(_property_matrix(sequence, name)[:, 0], window) + stats.update(_profile_stats(profile, name.upper())) + return _to_df(list(stats.values()), list(stats)) + +################################ Cysteine Features ######################################## + +def cysteine_features(sequence: str) -> pd.DataFrame: + """ + Calculate cysteine features [14]: + + Cys_count number of C + Cys_frac C / L + Cys_even 1 if the count is even + Cys_max_disulfides floor(count / 2) + Cys_CxxC overlapping C-x-x-C motifs + Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :cys_df: pd.DataFrame + pandas DataFrame of shape 1 x 6. + """ + #validate input protein sequence + sequence = _validate_sequence(sequence) + positions = [i for i, aa in enumerate(sequence) if aa == "C"] + n = len(positions) + spacing = float(np.mean(np.diff(positions))) if n > 1 else 0.0 + return _to_df([n, n / len(sequence), int(n % 2 == 0), n // 2, + len(re.findall(r"(?=C..C)", sequence)), spacing], + ["Cys_count", "Cys_frac", "Cys_even", "Cys_max_disulfides", "Cys_CxxC", "Cys_mean_spacing"]) diff --git a/protpy/utils.py b/protpy/utils.py index 83ced37..489618f 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -1236,6 +1236,163 @@ "descriptor_sets": ["extended", "all"], "in_default_set": False, }, + "aggrescan": { + "description": "AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot spots: >=5 residues above -0.02, no Pro).", + "module": "structural", + "category": "Structural", + "abbreviation": "AGG", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=5) — odd window length (AGGRESCAN uses 5, 7, 9, 11)", + }, + "output_shape": "1 x 12", + "speed": "Fast", + "reference": "Conchillo-Solé et al. (2007). BMC Bioinformatics, 8, 65.", + "doi": "10.1186/1471-2105-8-65", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "uversky_charge_hydropathy": { + "description": "Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.785 - - 1.151.", + "module": "structural", + "category": "Structural", + "abbreviation": "UV", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Uversky, Gillespie & Fink (2000). Proteins, 41(3), 415-427.", + "doi": "10.1002/1097-0134(20001115)41:3<415::AID-PROT130>3.0.CO;2-7", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "fold_index": { + "description": "FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics.", + "module": "structural", + "category": "Structural", + "abbreviation": "FI", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=51) — window length", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Prilusky et al. (2005). Bioinformatics, 21(16), 3435-3438.", + "doi": "10.1093/bioinformatics/bti537", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "disorder_propensity": { + "description": "Fractions of disorder- and order-promoting residues and mean TOP-IDP value.", + "module": "structural", + "category": "Structural", + "abbreviation": "DP", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 3", + "speed": "Fast", + "reference": "Campen et al. (2008). Protein Pept. Lett., 15(9), 956-963.", + "doi": "10.2174/092986608785849164", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "transmembrane_features": { + "description": "Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, TM fraction, positive-inside bias).", + "module": "structural", + "category": "Structural", + "abbreviation": "TM", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=19) — hydropathy window", + "kd_threshold": "float (default=1.6) — segment threshold", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132.", + "doi": "10.1016/0022-2836(82)90515-0", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "signal_peptide_features": { + "description": "N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue hydropathy.", + "module": "structural", + "category": "Structural", + "abbreviation": "SP", + "parameters": { + "sequence": "str — protein sequence", + "n_term": "int (default=30) — N-terminal region length", + }, + "output_shape": "1 x 2", + "speed": "Fast", + "reference": "von Heijne (1983). Eur. J. Biochem., 133(1), 17-21.", + "doi": "10.1111/j.1432-1033.1983.tb07424.x", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "functional_site_counts": { + "description": "Counts and per-100-residue densities of PROSITE N-glycosylation, ER-retention and PTS1 motifs and tryptic sites.", + "module": "structural", + "category": "Structural", + "abbreviation": "FS", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 8", + "speed": "Fast", + "reference": "Sigrist et al. (2013). Nucleic Acids Res., 41(D1), D344-D347 (PROSITE).", + "doi": "10.1093/nar/gks1067", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "plaac": { + "description": "PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and presence.", + "module": "structural", + "category": "Structural", + "abbreviation": "PLAAC", + "parameters": { + "sequence": "str — protein sequence", + "core_length": "int (default=60) — minimal core length", + }, + "output_shape": "1 x 4", + "speed": "Fast", + "reference": "Lancaster et al. (2014). Bioinformatics, 30(17), 2501-2502.", + "doi": "10.1093/bioinformatics/btu310", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "antigenicity_surface": { + "description": "Profile statistics of the Parker hydrophilicity and Janin surface scales over 7-residue windows.", + "module": "structural", + "category": "Structural", + "abbreviation": "Antigenicity", + "parameters": { + "sequence": "str — protein sequence", + "window": "int (default=7) — window length", + }, + "output_shape": "1 x 8", + "speed": "Fast", + "reference": "Parker, Guo & Hodges (1986). Biochemistry, 25(19), 5425-5432.", + "doi": "10.1021/bi00367a013", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, + "cysteine_features": { + "description": "Cysteine count, fraction, parity, maximum disulfides, CxxC motifs and mean spacing.", + "module": "structural", + "category": "Structural", + "abbreviation": "Cys", + "parameters": { + "sequence": "str — protein sequence", + }, + "output_shape": "1 x 6", + "speed": "Fast", + "reference": "Fomenko & Gladyshev (2003). Biochemistry, 42(38), 11214-11225.", + "doi": "10.1021/bi034459s", + "descriptor_sets": ["extended", "all"], + "in_default_set": False, + }, } #speed rating display symbols for pretty-printing diff --git a/tests/test_backwards_compat.py b/tests/test_backwards_compat.py index 8406c11..1ce365f 100644 --- a/tests/test_backwards_compat.py +++ b/tests/test_backwards_compat.py @@ -35,8 +35,10 @@ def _sequences() -> dict: seqs = {} for name in sorted(os.listdir(TEST_DATA)): - if name.endswith(".fasta"): - seqs[name] = str(next(SeqIO.parse(os.path.join(TEST_DATA, name), "fasta")).seq) + #only the original v1.4.2 test sequences are pinned + if name.startswith("test_fasta") and name.endswith(".fasta"): + with open(os.path.join(TEST_DATA, name)) as f: + seqs[name] = str(next(SeqIO.parse(f, "fasta")).seq) return seqs diff --git a/tests/test_data/structural_reference.fasta b/tests/test_data/structural_reference.fasta new file mode 100644 index 0000000..03a4b70 --- /dev/null +++ b/tests/test_data/structural_reference.fasta @@ -0,0 +1,6 @@ +>MOT3 S. cerevisiae Mot3 (from the PLAAC repository example cli/example/MOT3.fasta, MIT License) +MNADHHLQQQQQQRQQHQQQQHQQQQHQHQHQQQQHTILQNVSNTNNIGSDSLASQPFNTTTVSSNKDDVMVNSGARELPMPLHQQQYIYPYYQYTSNNSNNNNVTAGNNMSASPIVHNNSNNSNNSNISASDYTVANNSTSNNNNNNNNNNNNNNNIHPNQFTAAANMNSNAAAAAYYSFPTANMPIPQQDQQYMFNPASYISHYYSAVNSNNNGNNAANNGSNNSSHSAPAPAPGPPHHHHHHSNTHNNLNNGGAVNTNNAPQHHPTIITDQFQFQLQQNPSPNLNLNINPAQPLHLPPGWKINTMPQPRPTTAPNHPPAPVPSSNPVASNLVPAPSSDHKYIHQCQFCEKSFKRKSWLKRHLLSHSQQRHFLCPWCLSRQKRKDNLLQHMKLKHTNYLLDELKKNNIIFNYNNSSSSNNNNDNNNNNNSNSASGSGGAGAAAAAATAPENEDGNGYDTNIKTLINDGVLNKDDVKRVLNNLIVSHNK +>GYPA_HUMAN UniProt P02724 glycophorin-A (single-pass transmembrane) +MYGKIIFVLLLSEIVSISALSTTEVAMHTSTSSSVTKSYISSQTNDTHKRDTYAATPRAHEVSEISVRTVYPPEEETGERVQLAHHFSEPEITLIIFGVMAGVIGTILLISYGIRRLIKKSPSDVKPLPSPDTDVPLSSVEIENPETSDQ +>ABETA42 UniProt P05067 residues 672-713 (amyloid-beta 1-42) +DAEFRHDSGYEVHHQKLVFFAEDVGSNKGAIIGLMVGGVVIA diff --git a/tests/test_structural.py b/tests/test_structural.py new file mode 100644 index 0000000..9a65838 --- /dev/null +++ b/tests/test_structural.py @@ -0,0 +1,118 @@ +########################################################################### +############### protPy - Structural Module Tests (Phase 7) ########### +########################################################################### + +import math +import os +import unittest + +import numpy as np +from Bio import SeqIO + +import protpy + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD" +with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_data", "structural_reference.fasta")) as _f: + REF = {r.id: str(r.seq) for r in SeqIO.parse(_f, "fasta")} + + +class ProtpyStructuralTests(unittest.TestCase): + + def test_invalid(self): + for name in ("aggrescan", "uversky_charge_hydropathy", "fold_index", "disorder_propensity", + "transmembrane_features", "signal_peptide_features", "functional_site_counts", "plaac", + "antigenicity_surface", "cysteine_features"): + func = getattr(protpy, name) + with self.subTest(func=name): + with self.assertRaises(TypeError): + func(["A"]) + with self.assertRaises(ValueError): + func("") + with self.assertRaises(ValueError): + func("ABX") + + def test_aggrescan(self): + df = protpy.aggrescan(REF["ABETA42"]) + self.assertEqual(df.shape, (1, 12)) + self.assertTrue(all(c.startswith("AGG_") for c in df.columns)) + #Abeta42 has two hot spots: the central hydrophobic cluster and the C-terminus [Table 1 of the paper] + self.assertEqual(df["AGG_nHS"].iloc[0], 2) + self.assertAlmostEqual(df["AGG_NnHS"].iloc[0], round(2 / 42 * 100, 3)) + #poly-I is all hot spot; poly-D none; proline breaks hot spots + self.assertEqual(protpy.aggrescan("I" * 12)["AGG_nHS"].iloc[0], 1) + self.assertEqual(protpy.aggrescan("D" * 12)["AGG_nHS"].iloc[0], 0) + self.assertEqual(protpy.aggrescan("IIIIIPIIIII")["AGG_nHS"].iloc[0], 2) + self.assertEqual(protpy.aggrescan("IIIIPIIII")["AGG_nHS"].iloc[0], 0) + #a3vSA is the mean propensity: I = 1.822 + self.assertEqual(protpy.aggrescan("I" * 6)["AGG_a3vSA"].iloc[0], 1.822) + self.assertFalse(protpy.aggrescan("ACD").isnull().any().any()) + with self.assertRaises(ValueError): + protpy.aggrescan(SEQ, window=6) + + def test_uversky_fold_index(self): + df = protpy.uversky_charge_hydropathy("KKKKKEEEEE") + h = sum((-3.9 + 4.5) / 9 for _ in range(5)) / 10 + sum((-3.5 + 4.5) / 9 for _ in range(5)) / 10 + self.assertEqual(df["UV_MeanH"].iloc[0], round(h, 3)) + self.assertEqual(df["UV_MeanR"].iloc[0], 0.0) + self.assertEqual(df["UV_Unfolded"].iloc[0], 1) + self.assertEqual(protpy.uversky_charge_hydropathy("LIVFLIVF")["UV_Unfolded"].iloc[0], 0) + #window >= L: FoldIndex equals the Uversky score + fi = protpy.fold_index(SEQ, window=100) + self.assertEqual(fi["FI_mean"].iloc[0], protpy.uversky_charge_hydropathy(SEQ)["UV_HQ"].iloc[0]) + df = protpy.fold_index("LIVF" * 20 + "EKSPEKSQEDKPS" * 6, window=21) + self.assertTrue(0 < df["FI_frac_disordered"].iloc[0] < 1) + self.assertGreater(df["FI_longest_disordered"].iloc[0], 21) + + def test_disorder_propensity(self): + df = protpy.disorder_propensity("WP") + self.assertEqual(df.values[0].tolist(), [0.5, 0.5, round((-0.884 + 0.987) / 2, 3)]) + + def test_transmembrane(self): + df = protpy.transmembrane_features(REF["GYPA_HUMAN"]) + self.assertGreaterEqual(df["TM_n_segments"].iloc[0], 1) + self.assertGreater(df["TM_max_KD"].iloc[0], 1.6) + self.assertEqual(protpy.transmembrane_features(SEQ)["TM_n_segments"].iloc[0], 0) + #K/R before the segment, none after -> positive bias (and the reverse) + tm = "S" * 10 + "L" * 22 + "S" * 10 + self.assertGreater(protpy.transmembrane_features("KRKRK" + tm + "SSSSS")["TM_positive_inside_bias"].iloc[0], 0) + self.assertLess(protpy.transmembrane_features("SSSSS" + tm + "KRKRK")["TM_positive_inside_bias"].iloc[0], 0) + + def test_signal_peptide(self): + df = protpy.signal_peptide_features("MKKRLLLLLLLLAAAASA" + SEQ) + self.assertEqual(df["SP_nregion_charge"].iloc[0], 3) + self.assertGreater(df["SP_hregion_maxKD"].iloc[0], 2.5) + + def test_functional_sites(self): + df = protpy.functional_site_counts("NKSANPTANGTAKDEL") + #N-glyc: NKSA yes, NPTA no (P at X), NGTA yes + self.assertEqual(df["FS_N_glycosylation"].iloc[0], 2) + self.assertEqual(df["FS_ER_retention"].iloc[0], 1) + self.assertEqual(protpy.functional_site_counts("AAASKL")["FS_PTS1"].iloc[0], 1) + self.assertEqual(protpy.functional_site_counts("AKPAKRA")["FS_tryptic_site"].iloc[0], 2) + self.assertEqual(df["FS_ER_retention_per100"].iloc[0], round(100 / 16, 3)) + + def test_plaac(self): + #reference output of PLAAC 1.1.0 for MOT3 (cli/example/MOT3-candidates-plaac-1.1.0.tsv) + df = protpy.plaac(REF["MOT3"]) + self.assertEqual(df["PLAAC_LLR"].iloc[0], 40.751) + self.assertEqual(df["PLAAC_COREscore"].iloc[0], 40.751) + self.assertEqual(df["PLAAC_PrD_length"].iloc[0], 296) + self.assertEqual(df["PLAAC_has_PrD"].iloc[0], 1) + df = protpy.plaac(SEQ) + self.assertTrue(math.isnan(df["PLAAC_LLR"].iloc[0])) + self.assertEqual(df["PLAAC_has_PrD"].iloc[0], 0) + self.assertEqual(protpy.plaac(REF["GYPA_HUMAN"])["PLAAC_has_PrD"].iloc[0], 0) + + def test_antigenicity(self): + df = protpy.antigenicity_surface(SEQ) + self.assertEqual(list(df.columns), [f"{s}_{t}" for s in ("PARKER", "JANIN") for t in ("mean", "max", "min", "std")]) + self.assertEqual(protpy.antigenicity_surface("D" * 10)["PARKER_mean"].iloc[0], 10.0) + + def test_cysteine(self): + df = protpy.cysteine_features("CAACAAACAC") + self.assertEqual(df.values[0].tolist(), [4, 0.4, 1, 2, 1, 3.0]) + self.assertEqual(protpy.cysteine_features("AAA")["Cys_mean_spacing"].iloc[0], 0.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index ae43436..9b601f1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -80,7 +80,7 @@ class ProtpyUtilsTests(unittest.TestCase): } VALID_CATEGORIES = {"Composition", "Autocorrelation", "Conjoint Triad", "CTD", "Sequence Order", - "Scales", "Physicochemical", "Complexity", "Signal"} + "Scales", "Physicochemical", "Complexity", "Signal", "Structural"} VALID_SPEEDS = {"Fast", "Moderate", "Slow"} # ── Registry integrity ──────────────────────────────────────────────────────── From 4bb971760684ffa12ddaf5e5a96c44f8baca891e Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:48:45 +0100 Subject: [PATCH 11/17] v1.5.0 phase 8: descriptor_set/include/exclude driven by the registry, faster kappa, performance test --- protpy/encode.py | 166 +++++++++++++++++++++++++++------- protpy/physicochemical.py | 43 ++++++--- tests/test_descriptor_sets.py | 82 +++++++++++++++++ tests/test_performance.py | 25 +++++ tests/test_physicochemical.py | 16 ++-- 5 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 tests/test_descriptor_sets.py create mode 100644 tests/test_performance.py diff --git a/protpy/encode.py b/protpy/encode.py index cdc961a..135071d 100644 --- a/protpy/encode.py +++ b/protpy/encode.py @@ -45,6 +45,11 @@ quasi_sequence_order_all, ) from ._constants import _validate_sequence +from . import (composition as _composition, autocorrelation as _autocorrelation, ctd as _ctd, + conjoint_triad as _conjoint_triad_module, sequence_order as _sequence_order, scales as _scales, + physicochemical as _physicochemical, complexity as _complexity, signal as _signal, + structural as _structural) +from .utils import _DESCRIPTOR_REGISTRY #descriptors that accept extra keyword arguments beyond sequence _PARAMETERISED_DESCRIPTORS: frozenset[str] = frozenset({ @@ -62,7 +67,30 @@ "ctd_", "sequence_order_coupling_number_all", "quasi_sequence_order_all", -}) +}) | frozenset( + #v1.5.0 descriptors outside the classic set that take parameters + name for name, meta in _DESCRIPTOR_REGISTRY.items() + if not meta.get("in_default_set", True) and set(meta["parameters"]) - {"sequence"} +) + +#descriptor sets accepted by all_descriptors / encode_dataset +_DESCRIPTOR_SETS = ("classic", "extended", "all") + +#modules holding each registry "module" value +_MODULES = { + "composition": _composition, "autocorrelation": _autocorrelation, "ctd": _ctd, + "conjoint_triad": _conjoint_triad_module, "sequence_order": _sequence_order, "scales": _scales, + "physicochemical": _physicochemical, "complexity": _complexity, "signal": _signal, + "structural": _structural, +} + + +def _set_members(descriptor_set: str) -> list[str]: + """Registry descriptors (outside the classic set) that belong to ``descriptor_set``, in registry order.""" + if descriptor_set == "classic": + return [] + return [name for name, meta in _DESCRIPTOR_REGISTRY.items() + if not meta.get("in_default_set", True) and descriptor_set in meta.get("descriptor_sets", [])] def all_descriptors( @@ -71,12 +99,32 @@ def all_descriptors( show_progress: bool = False, output_file: str | None = None, save_each_sequence: bool = True, + descriptor_set: str = "classic", + include: list[str] | None = None, + exclude: list[str] | None = None, _show_descriptor_list: bool = True, ) -> pd.DataFrame: """ Calculate all available protpy descriptors for a protein sequence and return them concatenated into a single DataFrame (1 x N_total). + ``descriptor_set`` chooses which descriptors are computed: + + - ``"classic"`` (default): the v1.4 descriptor set described below, with an + unchanged output. + - ``"extended"``: classic plus every v1.5 descriptor whose default output + has at most 150 columns (factor scales, charge patterning, + hydrophobicity/stability, complexity, signal and structural descriptors). + - ``"all"``: extended plus the high-dimensional v1.5 descriptors (CKSAAP, + DDE, ASDC, distance pair and auto/cross covariance). + + Set membership comes from the descriptor registry (``descriptor_sets``). + ``include`` adds further registered descriptors by name and ``exclude`` + removes descriptors by name (classic ones included, e.g. "ctd_" or + "tripeptide_composition"). Some extended descriptors are NaN where their + method leaves them undefined (``plaac`` LLR/COREscore for short or + non-prion sequences, ``half_life`` E. coli for an N-terminal Pro). + The combined variants of multi-property descriptors are used to avoid redundancy: ``ctd_`` is called with ``all_ctd=True`` (covers ctd_composition, ctd_transition and ctd_distribution across all 7 @@ -117,6 +165,10 @@ def all_descriptors( sequence_order_coupling_number_all → lag quasi_sequence_order_all → lag, weight + plus any v1.5 descriptor with parameters (see + ``protpy.get_descriptor_info(name)["parameters"]``), e.g. + ``{"kappa": {"blob_sizes": (5,)}, "aggrescan": {"window": 7}}``. + Example:: df = protpy.all_descriptors(seq, params={ @@ -148,6 +200,15 @@ def all_descriptors( nothing is written to the CSV within this call. Has no effect when ``output_file`` is ``None``. + :descriptor_set: str (default="classic") + "classic", "extended" or "all" (see above). + + :include: list[str] | None (default=None) + extra registered descriptor names to compute. + + :exclude: list[str] | None (default=None) + descriptor names to skip. + Returns ======= :descriptors_df: pd.DataFrame @@ -181,6 +242,14 @@ def all_descriptors( if not isinstance(params, dict): raise TypeError(f"params must be a dict, got {type(params)}.") + if descriptor_set not in _DESCRIPTOR_SETS: + raise ValueError(f"Invalid descriptor_set '{descriptor_set}'. Valid sets are: {list(_DESCRIPTOR_SETS)}.") + include, exclude = list(include or []), set(exclude or []) + unregistered = [n for n in include + sorted(exclude) if n not in _DESCRIPTOR_REGISTRY] + if unregistered: + raise ValueError(f"Unknown descriptor name(s) in include/exclude: {unregistered}. " + "See protpy.list_descriptors().") + unknown = set(params) - _PARAMETERISED_DESCRIPTORS if unknown: raise ValueError( @@ -195,45 +264,57 @@ def _p(name: str) -> dict: # Only include kmer_composition when the user requests a k value that is unique. _kmer_k = _p("kmer_composition").get("k", 2) - steps: list[tuple[str, object]] = [ + steps: list[tuple[str, str, object]] = [ # ── Composition ──────────────────────────────────────────────────────── - ("[Composition] Amino acid composition", lambda: amino_acid_composition(sequence)), - ("[Composition] Dipeptide composition", lambda: dipeptide_composition(sequence)), - ("[Composition] Tripeptide composition", lambda: tripeptide_composition(sequence)), - ("[Composition] GRAVY index", lambda: gravy(sequence)), - ("[Composition] Aromaticity", lambda: aromaticity(sequence)), - ("[Composition] Instability index", lambda: instability_index(sequence)), - ("[Composition] Isoelectric point", lambda: isoelectric_point(sequence)), - ("[Composition] Molecular weight", lambda: molecular_weight(sequence)), - ("[Composition] Charge distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))), - ("[Composition] Hydrophobic / polar / charged comp.", lambda: hydrophobic_polar_charged_composition(sequence)), - ("[Composition] Secondary structure propensity", lambda: secondary_structure_propensity(sequence)), - *([("[Composition] K-mer composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))] + ("[Composition] Amino acid composition", "amino_acid_composition", lambda: amino_acid_composition(sequence)), + ("[Composition] Dipeptide composition", "dipeptide_composition", lambda: dipeptide_composition(sequence)), + ("[Composition] Tripeptide composition", "tripeptide_composition", lambda: tripeptide_composition(sequence)), + ("[Composition] GRAVY index", "gravy", lambda: gravy(sequence)), + ("[Composition] Aromaticity", "aromaticity", lambda: aromaticity(sequence)), + ("[Composition] Instability index", "instability_index", lambda: instability_index(sequence)), + ("[Composition] Isoelectric point", "isoelectric_point", lambda: isoelectric_point(sequence)), + ("[Composition] Molecular weight", "molecular_weight", lambda: molecular_weight(sequence)), + ("[Composition] Charge distribution", "charge_distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))), + ("[Composition] Hydrophobic / polar / charged comp.", "hydrophobic_polar_charged_composition", lambda: hydrophobic_polar_charged_composition(sequence)), + ("[Composition] Secondary structure propensity", "secondary_structure_propensity", lambda: secondary_structure_propensity(sequence)), + *([("[Composition] K-mer composition", "kmer_composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))] if _kmer_k not in (2, 3) else []), - ("[Composition] Reduced alphabet composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))), - ("[Composition] Motif composition", lambda: motif_composition(sequence, **_p("motif_composition"))), - ("[Composition] Aliphatic index", lambda: aliphatic_index(sequence)), - ("[Composition] Extinction coefficient", lambda: extinction_coefficient(sequence)), - ("[Composition] Boman index", lambda: boman_index(sequence)), - ("[Composition] Aggregation propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))), - ("[Composition] Hydrophobic moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))), - ("[Composition] Shannon entropy", lambda: shannon_entropy(sequence)), - ("[Composition] Pseudo amino acid composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))), - ("[Composition] Amphiphilic pseudo amino acid comp.", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))), + ("[Composition] Reduced alphabet composition", "reduced_alphabet_composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))), + ("[Composition] Motif composition", "motif_composition", lambda: motif_composition(sequence, **_p("motif_composition"))), + ("[Composition] Aliphatic index", "aliphatic_index", lambda: aliphatic_index(sequence)), + ("[Composition] Extinction coefficient", "extinction_coefficient", lambda: extinction_coefficient(sequence)), + ("[Composition] Boman index", "boman_index", lambda: boman_index(sequence)), + ("[Composition] Aggregation propensity", "aggregation_propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))), + ("[Composition] Hydrophobic moment", "hydrophobic_moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))), + ("[Composition] Shannon entropy", "shannon_entropy", lambda: shannon_entropy(sequence)), + ("[Composition] Pseudo amino acid composition", "pseudo_amino_acid_composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))), + ("[Composition] Amphiphilic pseudo amino acid comp.", "amphiphilic_pseudo_amino_acid_composition", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))), # ── Autocorrelation ──────────────────────────────────────────────────── - ("[Autocorrelation] Moreaubroto autocorrelation", lambda: moreaubroto_autocorrelation(sequence, **_p("moreaubroto_autocorrelation"))), - ("[Autocorrelation] Moran autocorrelation", lambda: moran_autocorrelation(sequence, **_p("moran_autocorrelation"))), - ("[Autocorrelation] Geary autocorrelation", lambda: geary_autocorrelation(sequence, **_p("geary_autocorrelation"))), + ("[Autocorrelation] Moreaubroto autocorrelation", "moreaubroto_autocorrelation", lambda: moreaubroto_autocorrelation(sequence, **_p("moreaubroto_autocorrelation"))), + ("[Autocorrelation] Moran autocorrelation", "moran_autocorrelation", lambda: moran_autocorrelation(sequence, **_p("moran_autocorrelation"))), + ("[Autocorrelation] Geary autocorrelation", "geary_autocorrelation", lambda: geary_autocorrelation(sequence, **_p("geary_autocorrelation"))), # ── Conjoint Triad ───────────────────────────────────────────────────── - ("[Conjoint Triad] Conjoint triad", lambda: _conjoint_triad(sequence)), + ("[Conjoint Triad] Conjoint triad", "conjoint_triad", lambda: _conjoint_triad(sequence)), # ── CTD ──────────────────────────────────────────────────────────────── - ("[CTD] Composition, transition & distribution",lambda: ctd_(sequence, **{"all_ctd": True, **_p("ctd_")})), + ("[CTD] Composition, transition & distribution", "ctd_", lambda: ctd_(sequence, **{"all_ctd": True, **_p("ctd_")})), # ── Sequence Order ───────────────────────────────────────────────────── - ("[Sequence Order] Sequence order coupling number", lambda: sequence_order_coupling_number_all(sequence, **_p("sequence_order_coupling_number_all"))), - ("[Sequence Order] Quasi sequence order", lambda: quasi_sequence_order_all(sequence, **_p("quasi_sequence_order_all"))), + ("[Sequence Order] Sequence order coupling number", "sequence_order_coupling_number_all", lambda: sequence_order_coupling_number_all(sequence, **_p("sequence_order_coupling_number_all"))), + ("[Sequence Order] Quasi sequence order", "quasi_sequence_order_all", lambda: quasi_sequence_order_all(sequence, **_p("quasi_sequence_order_all"))), ] - descriptor_names = [label.split("]", 1)[1].strip() for label, _ in steps] + #v1.5 descriptors from the chosen set and include, then apply exclude + extra = _set_members(descriptor_set) + [n for n in include if n not in _set_members(descriptor_set)] + classic_names = {name for _, name, _ in steps} + for name in extra: + if name in classic_names: + continue + meta = _DESCRIPTOR_REGISTRY[name] + func = getattr(_MODULES[meta["module"]], name) + steps.append((f"[{meta['category']}] {name}", name, + lambda func=func, name=name: func(sequence, **_p(name)))) + steps = [step for step in steps if step[1] not in exclude] + + descriptor_names = [label.split("]", 1)[1].strip() for label, _, _ in steps] if _show_descriptor_list: print("Calculating descriptors: " + ", ".join(descriptor_names)) @@ -246,14 +327,14 @@ def _p(name: str) -> dict: dynamic_ncols=True, bar_format="{desc} {percentage:3.0f}%|{bar:28}| {n:02d}/{total} [{elapsed}<{remaining}]", ) - for label, fn in steps: + for label, _, fn in steps: bar.set_description(label) frames.append(fn()) bar.update(1) bar.set_description(f"All {len(steps)} descriptors computed") bar.close() else: - for label, fn in steps: + for _, _, fn in steps: frames.append(fn()) result = pd.concat(frames, axis=1) @@ -274,6 +355,9 @@ def encode_dataset( save_each_sequence: bool = True, strip_gaps: bool = False, verbose: bool = True, + descriptor_set: str = "classic", + include: list[str] | None = None, + exclude: list[str] | None = None, ) -> pd.DataFrame: """ Compute all available descriptors for every sequence in a dataset and @@ -331,6 +415,15 @@ def encode_dataset( (``"Encoding sequence N/total: id"``). Set to ``False`` to suppress all stdout output from this function. + :descriptor_set: str (default="classic") + "classic", "extended" or "all"; see ``all_descriptors``. + + :include: list[str] | None (default=None) + extra descriptor names; see ``all_descriptors``. + + :exclude: list[str] | None (default=None) + descriptor names to skip; see ``all_descriptors``. + Returns ======= :result: pd.DataFrame @@ -405,7 +498,10 @@ def encode_dataset( show_progress=show_progress, output_file=None, save_each_sequence=False, - _show_descriptor_list=(idx == 1), + descriptor_set=descriptor_set, + include=include, + exclude=exclude, + _show_descriptor_list=(idx == 1 and verbose), ) row_df.insert(0, id_col_name, seq_id) completed.append(row_df) diff --git a/protpy/physicochemical.py b/protpy/physicochemical.py index 3b609c0..f815669 100644 --- a/protpy/physicochemical.py +++ b/protpy/physicochemical.py @@ -84,7 +84,8 @@ def charge_fractions(sequence: str) -> pd.DataFrame: Returns ======= :charge_fractions_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``FCR``, ``FPlus``, ``FMinus``, ``Sigma``. + pandas DataFrame of shape 1 x 4, columns ``Charge_FCR``, ``Charge_FPlus``, + ``Charge_FMinus`` and ``Charge_Sigma``. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -93,7 +94,8 @@ def charge_fractions(sequence: str) -> pd.DataFrame: f_plus, f_minus = np.mean(q > 0), np.mean(q < 0) fcr, ncpr_ = f_plus + f_minus, f_plus - f_minus sigma = ncpr_ ** 2 / fcr if fcr else 0.0 - return _to_df([fcr, f_plus, f_minus, sigma], ["FCR", "FPlus", "FMinus", "Sigma"]) + #prefixed so "FCR" cannot clash with the tripeptide-composition column of the same name + return _to_df([fcr, f_plus, f_minus, sigma], ["Charge_FCR", "Charge_FPlus", "Charge_FMinus", "Charge_Sigma"]) #################################### NCPR ################################################# @@ -144,18 +146,29 @@ def _delta_max(n_pos: int, n_neg: int, length: int, g: int, sigma: float) -> flo """ Maximal delta over the most segregated arrangements of the composition: a block of positives and a block of negatives, with the neutral residues split across the three - gaps around them. Every split is tried (O(n0^2) candidates), in chunks. + gaps around them (left a, between b, right c). + + Blobs lying entirely in a neutral flank contribute (0 - sigma)^2 whatever the flank's + length, so for a given b only flank lengths up to g - 1 are distinct. Trying every b + with those a/c splits covers every distinct delta in O(n0 x g) candidates instead of + the O(n0^2) of a full enumeration. """ n0 = length - n_pos - n_neg - best = 0.0 + candidates = set() + for b in range(n0 + 1): + rest = n0 - b + for k in range(min(g, rest + 1)): + candidates.add((k, b)) + candidates.add((rest - k, b)) + candidates = np.array(sorted(candidates)) idx = np.arange(length) - for a in range(n0 + 1): - #a neutrals, positive block, b neutrals, negative block, remaining neutrals - b = np.arange(n0 - a + 1)[:, None] - pos = (idx >= a) & (idx < a + n_pos) - neg = (idx >= a + n_pos + b) & (idx < a + n_pos + b + n_neg) - pos = np.broadcast_to(pos, neg.shape).astype(float) - best = max(best, float(_delta(pos, neg.astype(float), g, sigma).max())) + best = 0.0 + #chunk the (candidate x residue) arrays to bound memory on long sequences + for chunk in np.array_split(candidates, max(1, len(candidates) // 1000)): + a, b = chunk[:, :1], chunk[:, 1:] + pos = ((idx >= a) & (idx < a + n_pos)).astype(float) + neg = ((idx >= a + n_pos + b) & (idx < a + n_pos + b + n_neg)).astype(float) + best = max(best, float(_delta(pos, neg, g, sigma).max())) return best @@ -171,7 +184,8 @@ def kappa(sequence: str, blob_sizes: tuple[int, ...] = (5, 6)) -> pd.DataFrame: where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the largest delta over the maximally segregated arrangements of the same composition (a positive block and a negative block with the neutral residues - distributed over the three surrounding gaps). kappa is the mean of kappa_g + distributed over the three surrounding gaps; every distinct arrangement is + evaluated). kappa is the mean of kappa_g over ``blob_sizes`` (5 and 6 in [2]); 0 means well mixed and 1 fully segregated. Charges: K, R = +1; D, E = -1; H = 0. @@ -231,7 +245,7 @@ def sequence_charge_decoration(sequence: str) -> pd.DataFrame: Returns ======= :scd_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``SCD``. + pandas DataFrame of shape 1 x 1, column ``Charge_SCD``. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -241,7 +255,8 @@ def sequence_charge_decoration(sequence: str) -> pd.DataFrame: #only charged residues contribute; pairwise over them is O(n_charged^2) m, n = np.triu_indices(len(idx), k=1) scd = np.sum(q[idx[m]] * q[idx[n]] * np.sqrt(idx[n] - idx[m])) / len(sequence) - return _to_df([scd], ["SCD"]) + #prefixed so "SCD" cannot clash with the tripeptide-composition column of the same name + return _to_df([scd], ["Charge_SCD"]) #################################### Charge Density ####################################### diff --git a/tests/test_descriptor_sets.py b/tests/test_descriptor_sets.py new file mode 100644 index 0000000..186453b --- /dev/null +++ b/tests/test_descriptor_sets.py @@ -0,0 +1,82 @@ +########################################################################### +######### protPy - descriptor_set / include / exclude (Phase 8) ####### +########################################################################### + +import os +import tempfile +import unittest + +import protpy +from protpy.encode import _set_members +from protpy.utils import _DESCRIPTOR_REGISTRY + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDAKLVVSTEQW" + + +class ProtpyDescriptorSetTests(unittest.TestCase): + + def _all(self, seq=SEQ, **kw): + return protpy.all_descriptors(seq, _show_descriptor_list=False, **kw) + + def test_classic_is_default(self): + self.assertTrue(self._all().equals(self._all(descriptor_set="classic"))) + + def test_sets_are_nested_and_unique(self): + classic, extended, full = self._all(), self._all(descriptor_set="extended"), self._all(descriptor_set="all") + self.assertLess(classic.shape[1], extended.shape[1]) + self.assertLess(extended.shape[1], full.shape[1]) + self.assertEqual(list(extended.columns[:classic.shape[1]]), list(classic.columns)) + self.assertTrue(set(extended.columns) <= set(full.columns)) + for df in (classic, extended, full): + self.assertFalse(df.columns.duplicated().any(), df.columns[df.columns.duplicated()].tolist()) + + def test_set_membership_from_registry(self): + extended = _set_members("extended") + self.assertIn("kappa", extended) + self.assertIn("plaac", extended) + for name in ("cksaap", "dde", "asdc", "distance_pair", "auto_cross_covariance"): + self.assertNotIn(name, extended) + self.assertIn(name, _set_members("all")) + self.assertEqual(_set_members("classic"), []) + #extended descriptors have at most 150 columns by default + for name in extended: + meta = _DESCRIPTOR_REGISTRY[name] + func = getattr(protpy, name) + self.assertLessEqual(func(SEQ).shape[1], 150, name) + + def test_include_exclude_params(self): + df = self._all(include=["kappa"], exclude=["tripeptide_composition", "ctd_"], + params={"kappa": {"blob_sizes": (5,)}}) + self.assertIn("Kappa_5", df.columns) + self.assertNotIn("Kappa_6", df.columns) + self.assertNotIn("AAA", df.columns) + self.assertFalse(any(c.startswith("CTD_") for c in df.columns)) + #including a member of the set again does not duplicate it + self.assertEqual(self._all(descriptor_set="extended", include=["kappa"]).shape, + self._all(descriptor_set="extended").shape) + with self.assertRaises(ValueError): + self._all(descriptor_set="everything") + with self.assertRaises(ValueError): + self._all(include=["not_a_descriptor"]) + with self.assertRaises(ValueError): + self._all(params={"cysteine_features": {"x": 1}}) + + def test_short_sequence_extended(self): + short = SEQ[:31] + df = self._all(short, descriptor_set="extended") + self.assertEqual(df.shape[0], 1) + #only documented NaNs: PLAAC LLR/COREscore for sequences shorter than the core length + self.assertEqual(sorted(df.columns[df.isnull().any()]), ["PLAAC_COREscore", "PLAAC_LLR"]) + + def test_encode_dataset_passthrough(self): + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "enc.csv") + df = protpy.encode_dataset([SEQ, SEQ[::-1]], output_file=out, verbose=False, + descriptor_set="extended", exclude=["tripeptide_composition"]) + self.assertEqual(df.shape[0], 2) + self.assertIn("Kappa", df.columns) + self.assertNotIn("AAA", df.columns) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_performance.py b/tests/test_performance.py new file mode 100644 index 0000000..422ba65 --- /dev/null +++ b/tests/test_performance.py @@ -0,0 +1,25 @@ +########################################################################### +############### protPy - Performance Test (Phase 8) ########### +########################################################################### + +import os +import random +import time +import unittest + +import protpy + + +@unittest.skipUnless(os.environ.get("PROTPY_PERF"), "set PROTPY_PERF=1 to run the performance test") +class ProtpyPerformanceTests(unittest.TestCase): + + def test_all_descriptors_500_residues(self): + random.seed(0) + seq = "".join(random.choice(protpy.amino_acids) for _ in range(500)) + start = time.perf_counter() + protpy.all_descriptors(seq, descriptor_set="all", _show_descriptor_list=False) + self.assertLess(time.perf_counter() - start, 2.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_physicochemical.py b/tests/test_physicochemical.py index 6177b6c..d6946d2 100644 --- a/tests/test_physicochemical.py +++ b/tests/test_physicochemical.py @@ -36,13 +36,13 @@ def _check_invalid(self, func): def test_charge_fractions(self): df = protpy.charge_fractions("KKDD") - self.assertEqual(list(df.columns), ["FCR", "FPlus", "FMinus", "Sigma"]) + self.assertEqual(list(df.columns), ["Charge_FCR", "Charge_FPlus", "Charge_FMinus", "Charge_Sigma"]) self.assertEqual(df.values[0].tolist(), [1.0, 0.5, 0.5, 0.0]) #H is neutral; sigma = NCPR^2 / FCR df = protpy.charge_fractions("KKKDAAHH") - self.assertEqual(df["FCR"].iloc[0], 0.5) - self.assertEqual(df["Sigma"].iloc[0], round(0.25 ** 2 / 0.5, 3)) - self.assertEqual(protpy.charge_fractions("AAAA")["Sigma"].iloc[0], 0.0) + self.assertEqual(df["Charge_FCR"].iloc[0], 0.5) + self.assertEqual(df["Charge_Sigma"].iloc[0], round(0.25 ** 2 / 0.5, 3)) + self.assertEqual(protpy.charge_fractions("AAAA")["Charge_Sigma"].iloc[0], 0.0) self._check_invalid(protpy.charge_fractions) def test_ncpr(self): @@ -71,14 +71,14 @@ def test_kappa(self): self._check_invalid(protpy.kappa) def test_scd(self): - self.assertEqual(protpy.sequence_charge_decoration("AAAA")["SCD"].iloc[0], 0.0) + self.assertEqual(protpy.sequence_charge_decoration("AAAA")["Charge_SCD"].iloc[0], 0.0) #brute force q = [1 if aa in "KR" else -1 if aa in "DE" else 0 for aa in SEQ] ref = sum(q[m] * q[n] * math.sqrt(m - n) for m in range(len(q)) for n in range(m)) / len(q) - self.assertAlmostEqual(protpy.sequence_charge_decoration(SEQ)["SCD"].iloc[0], round(ref, 3)) + self.assertAlmostEqual(protpy.sequence_charge_decoration(SEQ)["Charge_SCD"].iloc[0], round(ref, 3)) #segregated charges are more negative than mixed - self.assertLess(protpy.sequence_charge_decoration("E" * 25 + "K" * 25)["SCD"].iloc[0], - protpy.sequence_charge_decoration("EK" * 25)["SCD"].iloc[0]) + self.assertLess(protpy.sequence_charge_decoration("E" * 25 + "K" * 25)["Charge_SCD"].iloc[0], + protpy.sequence_charge_decoration("EK" * 25)["Charge_SCD"].iloc[0]) self._check_invalid(protpy.sequence_charge_decoration) def test_charge_density(self): From d3279ebbde4a6509de9e022a3975b43e376f647c Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:31:11 +0100 Subject: [PATCH 12/17] v1.5.0 phase 9: docs, changelog, README, tutorial cell, version 1.5.0 --- CHANGELOG.md | 28 +- DESCRIPTORS.md | 1198 ++++++++++++++++++++++++++++++++ README.md | 18 + ToDo.md | 2 +- docs/api.rst | 51 +- docs/conf.py | 2 +- docs/descriptors.rst | 1160 +++++++++++++++++++++++++++++++ docs/usage.rst | 25 + examples/protpy_tutorial.ipynb | 26 + protpy/__init__.py | 2 +- pyproject.toml | 2 +- tests/test_protpy.py | 4 +- 12 files changed, 2506 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3632f5f..4426ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [Unreleased] +## [1.5.0] - 2026-10-12 -### Fixed +### Added +- **44 new registered descriptor functions** (plus the generic `aa_scale_descriptors`) with literature references (DOIs in the registry and `docs/references.bib`): + - Composition: `cksaap`, `cksaagp`, `dde`, `asdc`, `distance_pair` (cp13/cp14/cp19/cp20), `natural_vector`, `atomic_composition`. + - Scales (`protpy/scales.py`): `aa_scale_descriptors` with wrappers `z_scales_5`, `vhse`, `atchley_factors`, `t_scales`, `st_scales`, `protfp`, `fasgai`, `blosum_indices`, `ms_whim` (all with `domains=`); `auto_cross_covariance`; `physicochemical_distance_transformation`. + - Physicochemical (`protpy/physicochemical.py`): `charge_fractions`, `ncpr`, `kappa` (reproduces the published Das & Pappu values), `sequence_charge_decoration`, `charge_density`, `hydrophobicity_profile`, `hydrophobic_ratio`, `half_life`, `ivywrel_index`, `cvp_bias`, `flexibility`. + - Complexity (`protpy/complexity.py`): `wootton_federhen_complexity`, `linguistic_complexity`, `lempel_ziv_complexity` (`shannon_entropy` re-exported). + - Signal (`protpy/signal.py`, numpy only): `fft_descriptors`, `dct_descriptors`. + - Structural (`protpy/structural.py`): `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac` (port of the MIT-licensed PLAAC; reproduces its reference output), `antigenicity_surface`, `cysteine_features`. +- **Extensions to existing functions** (defaults unchanged): `reduced_alphabet_composition(alphabet=, k=)` (GAAC/GDPC/GTPC), `conjoint_triad(gap=)` (KSCTriad), `ctd_(property_set="ifeature13")`, `amino_acid_composition`/`dipeptide_composition(region=, n=)` (split composition), `isoelectric_point`/`charge_distribution(pka_scale=)` ("emboss", "bjellqvist", "ipc_protein"), `gravy(scale=)`, `hydrophobic_moment(scale=, angle="helix"/"sheet")`, `extinction_coefficient(absorbance=True)`. +- **`descriptor_set`, `include`, `exclude`** for `all_descriptors` and `encode_dataset`: `"classic"` (default, unchanged), `"extended"` (adds the new descriptors with ≤150 columns) and `"all"`. Set membership comes from the registry's new `descriptor_sets` field. Every parameterised v1.5.0 descriptor is accepted in `params`. +- **Data files** with provenance (`name`, `values`, `reference`, `doi`, `source_note`): `protpy/data/aa_scales.json`, `reduced_alphabets.json`, `residue_tables.json`, `structural_params.json`. +- Internal helpers in `protpy/_helpers.py`. The registry gains `doi`, `in_default_set` and `descriptor_sets`, and `print_descriptor_info` prints the DOI. New registry categories: Scales, Physicochemical, Complexity, Signal, Structural. +- `docs/references.bib` with BibTeX for every registry DOI. +- Tests: golden backwards-compatibility snapshot of every v1.4.2 default output (`tests/test_backwards_compat.py`), helper/registry tests, one test module per new module, descriptor-set tests, and an opt-in performance test (`PROTPY_PERF=1`). +- Plan items that could not be implemented without unverifiable source data (e.g. PseKRAAC, Kidera factors, GOR, COILS, Chou–Fasman) are listed with reasons in `planning/BLOCKED.md`. + +### Changed +- `encode_dataset(verbose=False)` now also suppresses the descriptor-list line printed for the first sequence. +- `hydrophobic_moment` docstring now names the source of its default scale (Eisenberg, Schwarz, Komarony & Wall 1984). + +### Fixed (post-1.4.2 audit) - **Autocorrelation crash on fresh installs**: `aaindex1[code]` can return a new record on each access, so `.values.pop('-')` was lost and all three autocorrelation descriptors (and `all_descriptors`/`encode_dataset`) raised `IndexError`. Property values are now copied and filtered to the 20 canonical amino acids; aaindex records are no longer mutated. - **Moran autocorrelation formula**: multiplied by the property variance instead of dividing by it. - **CTD groups**: `polarity` groups 2/3 were copied from polarizability (A, D, G, S, T unassigned; 8 residues duplicated) — now `LIFWCMVY / PAGST / HQRKNED`. `normalized_vdwv` group 1 was missing C. @@ -28,12 +48,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **APAAComp properties parameter ignored (Bug 3)**: `amphiphilic_pseudo_amino_acid_composition` accepted a `properties` parameter but the underlying helpers `amphiphilic_correllation_function` and `amphiphilic_sequence_order_correlation_factor` hardcoded `hydrophobicity_` and `hydrophilicity_`, silently ignoring any user-supplied properties. Fixed by threading `properties` through all three functions and pre-normalizing once per call. - **`JSONDecodeError` constructor misuse (Bug 5)**: `sequence_order_coupling_number_` raised `json.JSONDecodeError(msg)` which fails at runtime with `TypeError` because `JSONDecodeError` requires `(msg, doc, pos)` positional arguments. Changed to `raise ValueError(...)`. -### Performance +### Performance (post-1.4.2 audit) - `kmer_composition`, dipeptide/tripeptide/pair composition count k-mers in one pass; QSO computes each SOCN once; PseAAC normalizes properties once per gap instead of per residue pair. - **`sequence_order_coupling_number_` disk I/O (Issue 6)**: Added `@functools.lru_cache(maxsize=2)` loader `_load_distance_matrix` in `sequence_order.py`. The distance matrix JSON was read from disk on every call; for a sequence with `lag=30`, that was 30 file reads per descriptor invocation. Now both matrices are cached in memory after the first load. - **`normalize_property` O(n²) (Issue 7)**: Refactored to compute `mean` and `std` once before the loop, reducing from O(n²) to O(n) for the 20-amino-acid property dictionaries used in PseAAC and APAAComp descriptors. -### Changed +### Changed (post-1.4.2 audit) - Invalid `lag`, `lamda`, `k`, `window` (non-integer or negative) and `weight` (non-numeric or negative) now raise `ValueError` instead of being silently replaced by defaults. `lag`/`lamda` larger than the sequence still fall back to 30. - `all_descriptors` no longer writes `descriptors.csv` by default (`output_file=None`). - `all_descriptors` no longer includes `amino_acid_pair_composition` (it duplicated dipeptide composition's 400 values). diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index b2ea515..6712d18 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -702,6 +702,1193 @@ result = protpy.quasi_sequence_order_all(protein_seq, lag=15, weight=0.05) --- +## v1.5.0 Descriptors + +Descriptors added in v1.5.0. Each lists its formula, output shape, parameters and primary reference. +`descriptor_set="extended"` in `all_descriptors` / `encode_dataset` adds every descriptor below marked **extended**; `"all"` adds the rest as well. +Items from the implementation plan that could not be implemented without unverifiable data are listed in `planning/BLOCKED.md`. + +### Composition + +#### `cksaap` (CKSAAP) + +Composition of k-spaced amino acid pairs: CKSAAP(XY,g) = N_g(XY) / (L-g-1) x 100 for g = 0..gap. + +```text +Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein +sequence. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) +is counted and divided by the number of such pairs: + +CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 + +g = 0 is the dipeptide composition. Values are percentages; a spacing with no +pairs (L - g - 1 <= 0) gives zeros. +``` + +```python +result = protpy.cksaap(protein_seq) +# Shape: 1 x 400·(gap+1) (default 1 x 2400) +``` + +- **Columns:** `CKSAAP_AA_gap0`, `CKSAAP_AC_gap0`, `CKSAAP_AD_gap0`, `CKSAAP_AE_gap0`, `CKSAAP_AF_gap0`, `CKSAAP_AG_gap0`, … +- **Parameters:** `gap`: int (default=5) — maximum spacing +- **Descriptor set:** all +- **Reference:** Chen, Kurgan & Ruan (2007). BMC Struct. Biol., 7, 25. https://doi.org/10.1186/1472-6807-7-25 + +--- + +#### `cksaagp` (CKSAAGP) + +Composition of k-spaced amino acid group pairs over the iFeature 5-group alphabet. + +```text +Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP). +As CKSAAP, but residues are first mapped to the five iFeature groups: +aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and +uncharged (STCPNQ). + +CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 + +Values are percentages and the 25 values for each spacing sum to 100 +(0 when the spacing has no pairs). +``` + +```python +result = protpy.cksaagp(protein_seq) +# Shape: 1 x 25·(gap+1) (default 1 x 150) +``` + +- **Columns:** `CKSAAGP_aliphatic.aliphatic_gap0`, `CKSAAGP_aliphatic.aromatic_gap0`, `CKSAAGP_aliphatic.positive_gap0`, `CKSAAGP_aliphatic.negative_gap0`, `CKSAAGP_aliphatic.uncharged_gap0`, `CKSAAGP_aromatic.aliphatic_gap0`, … +- **Parameters:** `gap`: int (default=5) — maximum spacing +- **Descriptor set:** extended, all +- **Reference:** Chen et al. (2018). Bioinformatics, 34(14), 2499-2502 (iFeature). https://doi.org/10.1093/bioinformatics/bty140 + +--- + +#### `dde` (DDE) + +Dipeptide deviation from expected mean: (Dc - Tm) / sqrt(Tv), with Tm from codon counts. + +```text +Calculate the Dipeptide Deviation from Expected Mean (DDE). The observed +dipeptide composition is standardised against the composition expected from +the number of codons coding each residue: + +Dc(XY) = N(XY) / (L - 1) +Tm(XY) = (C_X / 61) x (C_Y / 61) +Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) +DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) + +where C_X is the number of sense codons for residue X in the standard genetic +code (61 in total). DDE is a standardised statistic, not a percentage. +``` + +```python +result = protpy.dde(protein_seq) +# Shape: 1 x 400 +``` + +- **Columns:** `DDE_AA`, `DDE_AC`, `DDE_AD`, `DDE_AE`, `DDE_AF`, `DDE_AG`, … +- **Parameters:** sequence only +- **Descriptor set:** all +- **Reference:** Saravanan & Gautham (2015). OMICS, 19(10), 648-658. https://doi.org/10.1089/omi.2015.0095 + +--- + +#### `asdc` (ASDC) + +Adaptive skip dipeptide composition: composition of all ordered pairs (s_i, s_j), i < j. + +```text +Calculate the Adaptive Skip Dipeptide Composition (ASDC): the composition +of every ordered residue pair (s_i, s_j) with i < j, over all gaps: + +ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 + +The denominator is the total number of ordered pairs, L(L-1)/2. Values are +percentages and sum to 100 (zeros for a single residue). +``` + +```python +result = protpy.asdc(protein_seq) +# Shape: 1 x 400 +``` + +- **Columns:** `ASDC_AA`, `ASDC_AC`, `ASDC_AD`, `ASDC_AE`, `ASDC_AF`, `ASDC_AG`, … +- **Parameters:** sequence only +- **Descriptor set:** all +- **Reference:** Wei et al. (2018). Bioinformatics, 34(23), 4007-4016 (ACPred-FL). https://doi.org/10.1093/bioinformatics/bty451 + +--- + +#### `distance_pair` (DP) + +PseAAC of distance-pairs over a reduced cluster profile (cp13/cp14/cp19/cp20). + +```text +Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet +(iDNA-Prot|dis). Residues are mapped to the clusters of a cluster +profile cp(n) and the vector is + +[f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] + +where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the +composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the +nearest-neighbour pair. This follows Eqs. 5-12 of, giving G + G^2·rng +values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. + +Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of. The +printed cp(14) lists E twice; the stray E in the first cluster is dropped, +the only reading with 14 clusters. +``` + +```python +result = protpy.distance_pair(protein_seq) +# Shape: 1 x (G + G²·rng) (default 1 x 602) +``` + +- **Columns:** `DP_IMV`, `DP_L`, `DP_F`, `DP_WY`, `DP_G`, `DP_P`, … +- **Parameters:** `rng`: int (default=3) — maximum pair distance; `alphabet`: str (default='cp14') — cluster profile +- **Descriptor set:** all +- **Reference:** Liu et al. (2014). PLoS ONE, 9(9), e106691 (iDNA-Prot|dis). https://doi.org/10.1371/journal.pone.0106691 + +--- + +#### `natural_vector` (NV) + +Natural vector: count, mean position and normalised second central moment of each amino acid. + +```text +Calculate the Natural Vector of a protein sequence. For each amino acid k: + +n_k = number of occurrences of k +mu_k = mean (1-based) position of k +D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) + +mu_k and D2_k are 0 when k does not occur. Values are not percentages. +``` + +```python +result = protpy.natural_vector(protein_seq) +# Shape: 1 x 60 +``` + +- **Columns:** `NV_n_A`, `NV_n_C`, `NV_n_D`, `NV_n_E`, `NV_n_F`, `NV_n_G`, … +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Yu et al. (2013). J. Theor. Biol., 318, 197-204. https://doi.org/10.1016/j.jtbi.2012.11.005 + +--- + +#### `atomic_composition` (ATC) + +Percentage of C, H, N, O and S atoms over the residues' free amino-acid formulas. + +```text +Calculate the Atomic Composition (ATC) of a protein sequence: the +percentage of C, H, N, O and S atoms, summed over the molecular formulas of +the residues' free amino acids. + +ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 + +By default free amino-acid formulas are summed without removing water, +matching Pfeature. With ``subtract_water=True``, (L - 1) H2O are removed +for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" +-> C4H8N2O3). +``` + +```python +result = protpy.atomic_composition(protein_seq) +# Shape: 1 x 5 +``` + +- **Columns:** `ATC_C`, `ATC_H`, `ATC_N`, `ATC_O`, `ATC_S` +- **Parameters:** `subtract_water`: bool (default=False) — remove (L-1) H2O +- **Descriptor set:** extended, all +- **Reference:** Pande et al. (2023). J. Comput. Biol., 30(2), 204-222 (Pfeature). https://doi.org/10.1089/cmb.2022.0241 + +--- + +### Scales + +#### `z_scales_5` (Z5) + +Mean of the Sandberg 5 z-scales over the sequence (optionally per domain). + +```text +Calculate the mean z_scales_5 factor-scale descriptor of a protein sequence (Sandberg 5 z-scales). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.z_scales_5(protein_seq) +# Shape: 1 x 5·domains (default 1 x 5) +``` + +- **Columns:** `Z5_1`, `Z5_2`, `Z5_3`, `Z5_4`, `Z5_5` +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Sandberg et al. (1998). J. Med. Chem., 41(14), 2481-2491. https://doi.org/10.1021/jm9700575 + +--- + +#### `vhse` (VHSE) + +Mean of the VHSE (hydrophobic, steric, electronic) scales over the sequence (optionally per domain). + +```text +Calculate the mean vhse factor-scale descriptor of a protein sequence (principal components score Vectors of Hydrophobic, Steric and Electronic properties). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.vhse(protein_seq) +# Shape: 1 x 8·domains (default 1 x 8) +``` + +- **Columns:** `VHSE_1`, `VHSE_2`, `VHSE_3`, `VHSE_4`, `VHSE_5`, `VHSE_6`, … +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Mei et al. (2005). Biopolymers, 80(6), 775-786. https://doi.org/10.1002/bip.20296 + +--- + +#### `atchley_factors` (AF) + +Mean of the Atchley factors I-V over the sequence (optionally per domain). + +```text +Calculate the mean atchley_factors factor-scale descriptor of a protein sequence (Atchley factors I-V). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.atchley_factors(protein_seq) +# Shape: 1 x 5·domains (default 1 x 5) +``` + +- **Columns:** `AF_1`, `AF_2`, `AF_3`, `AF_4`, `AF_5` +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Atchley et al. (2005). PNAS, 102(18), 6395-6400. https://doi.org/10.1073/pnas.0408677102 + +--- + +#### `t_scales` (T) + +Mean of the topological T-scales over the sequence (optionally per domain). + +```text +Calculate the mean t_scales factor-scale descriptor of a protein sequence (topological T-scales). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.t_scales(protein_seq) +# Shape: 1 x 5·domains (default 1 x 5) +``` + +- **Columns:** `T_1`, `T_2`, `T_3`, `T_4`, `T_5` +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Tian, Zhou & Li (2007). J. Mol. Struct., 830, 106-115. https://doi.org/10.1016/j.molstruc.2006.07.004 + +--- + +#### `st_scales` (ST) + +Mean of the structural topology ST-scales over the sequence (optionally per domain). + +```text +Calculate the mean st_scales factor-scale descriptor of a protein sequence (structural topology ST-scales). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.st_scales(protein_seq) +# Shape: 1 x 8·domains (default 1 x 8) +``` + +- **Columns:** `ST_1`, `ST_2`, `ST_3`, `ST_4`, `ST_5`, `ST_6`, … +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Yang et al. (2010). Amino Acids, 38(3), 805-816. https://doi.org/10.1007/s00726-009-0287-y + +--- + +#### `protfp` (ProtFP) + +Mean of the ProtFP (PCA, 8 components) over the sequence (optionally per domain). + +```text +Calculate the mean protfp factor-scale descriptor of a protein sequence (ProtFP PCA, 8 components). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.protfp(protein_seq) +# Shape: 1 x 8·domains (default 1 x 8) +``` + +- **Columns:** `ProtFP_1`, `ProtFP_2`, `ProtFP_3`, `ProtFP_4`, `ProtFP_5`, `ProtFP_6`, … +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** van Westen et al. (2013). J. Cheminform., 5, 41. https://doi.org/10.1186/1758-2946-5-41 + +--- + +#### `fasgai` (FASGAI) + +Mean of the FASGAI factor scales over the sequence (optionally per domain). + +```text +Calculate the mean fasgai factor-scale descriptor of a protein sequence (factor analysis scales of generalized amino acid information). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.fasgai(protein_seq) +# Shape: 1 x 6·domains (default 1 x 6) +``` + +- **Columns:** `FASGAI_1`, `FASGAI_2`, `FASGAI_3`, `FASGAI_4`, `FASGAI_5`, `FASGAI_6` +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Liang & Li (2007). QSAR Comb. Sci., 26(6), 754-763. https://doi.org/10.1002/qsar.200630145 + +--- + +#### `blosum_indices` (BLOSUM) + +Mean of the BLOSUM62-derived indices over the sequence (optionally per domain). + +```text +Calculate the mean blosum_indices factor-scale descriptor of a protein sequence (BLOSUM62-derived indices). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.blosum_indices(protein_seq) +# Shape: 1 x 10·domains (default 1 x 10) +``` + +- **Columns:** `BLOSUM_1`, `BLOSUM_2`, `BLOSUM_3`, `BLOSUM_4`, `BLOSUM_5`, `BLOSUM_6`, … +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Georgiev (2009). J. Comput. Biol., 16(5), 703-723. https://doi.org/10.1089/cmb.2008.0173 + +--- + +#### `ms_whim` (MSWHIM) + +Mean of the MS-WHIM scores over the sequence (optionally per domain). + +```text +Calculate the mean ms_whim factor-scale descriptor of a protein sequence (MS-WHIM scores). +See ``aa_scale_descriptors`` for the formula. +``` + +```python +result = protpy.ms_whim(protein_seq) +# Shape: 1 x 3·domains (default 1 x 3) +``` + +- **Columns:** `MSWHIM_1`, `MSWHIM_2`, `MSWHIM_3` +- **Parameters:** `domains`: int (default=1) — number of near-equal segments +- **Descriptor set:** extended, all +- **Reference:** Zaliani & Gancia (1999). J. Chem. Inf. Comput. Sci., 39(3), 525-533. https://doi.org/10.1021/ci980211b + +--- + +#### `auto_cross_covariance` (ACC) + +Auto/cross-covariance of an amino-acid scale: sum z_j(i) z_k(i+l) / (L-l). + +```text +Calculate the auto- and cross-covariance (ACC) transform of an amino-acid +scale. For lag l and scale dimensions j, k: + +ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) + +``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto +covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a +protpy factor-scale family (used as published) or an AAindex1 code / list +of codes, which are standardised over the 20 amino acids. +``` + +```python +result = protpy.auto_cross_covariance(protein_seq) +# Shape: 1 x d²·lag (default 1 x 125) +``` + +- **Columns:** `ACC_1_1_lag1`, `ACC_1_2_lag1`, `ACC_1_3_lag1`, `ACC_1_4_lag1`, `ACC_1_5_lag1`, `ACC_2_1_lag1`, … +- **Parameters:** `scale`: str | list[str] (default='z_scales_5') — factor scale or AAindex code(s); `lag`: int (default=5) — maximum lag; `mode`: str (default='acc') — 'acc', 'ac' or 'cc' +- **Descriptor set:** all +- **Reference:** Wold et al. (1993). Anal. Chim. Acta, 277(2), 239-253. https://doi.org/10.1016/0003-2670(93)80437-P + +--- + +#### `physicochemical_distance_transformation` (PDT) + +Physicochemical distance transformation: mean squared difference of standardised AAindex properties at lag l. + +```text +Calculate the Physicochemical Distance Transformation (PDT). For each +AAindex property P_j (standardised over the 20 amino acids) and lag l: + +PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) +``` + +```python +result = protpy.physicochemical_distance_transformation(protein_seq) +# Shape: 1 x len(properties)·lag (default 1 x 24) +``` + +- **Columns:** `PDT_CIDH920105_1`, `PDT_CIDH920105_2`, `PDT_CIDH920105_3`, `PDT_BHAR880101_1`, `PDT_BHAR880101_2`, `PDT_BHAR880101_3`, … +- **Parameters:** `lag`: int (default=3) — maximum lag; `properties`: list[str] | None — AAindex codes (default: 8 autocorrelation properties) +- **Descriptor set:** extended, all +- **Reference:** Liu et al. (2012). PLoS ONE, 7(9), e46633. https://doi.org/10.1371/journal.pone.0046633 + +--- + +### Physicochemical + +#### `charge_fractions` (FCR) + +Fraction of charged residues (FCR), positive/negative fractions and charge asymmetry sigma = NCPR²/FCR. + +```text +Calculate the charge fractions of a protein sequence: + +FCR = (n+ + n-) / L fraction of charged residues +FPlus = n+ / L, FMinus = n- / L +Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) + +where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. +``` + +```python +result = protpy.charge_fractions(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `Charge_FCR`, `Charge_FPlus`, `Charge_FMinus`, `Charge_Sigma` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Das & Pappu (2013). PNAS, 110(33), 13392-13397. https://doi.org/10.1073/pnas.1304749110 + +--- + +#### `ncpr` (NCPR) + +Net charge per residue: (n+ - n-) / L with K/R positive and D/E negative. + +```text +Calculate the Net Charge Per Residue (NCPR) of a protein sequence: + +NCPR = (n+ - n-) / L + +with n+ counting K/R and n- counting D/E. +``` + +```python +result = protpy.ncpr(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `NCPR` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Mao et al. (2010). PNAS, 107(18), 8183-8188. https://doi.org/10.1073/pnas.0911107107 + +--- + +#### `kappa` (Kappa) + +Das-Pappu kappa: normalised blob charge-asymmetry deviation measuring segregation of opposite charges (0 mixed, 1 segregated). + +```text +Calculate the kappa charge-patterning parameter of Das & Pappu. For blob +size g, the sequence is split into the L - g + 1 overlapping blobs and + +sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) +delta_g = mean over blobs of (sigma_b - sigma)^2 +kappa_g = delta_g / delta_max,g + +where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the +largest delta over the maximally segregated arrangements of the same +composition (a positive block and a negative block with the neutral residues +distributed over the three surrounding gaps; every distinct arrangement is +evaluated). kappa is the mean of kappa_g +over ``blob_sizes`` (5 and 6 in); 0 means well mixed and 1 fully +segregated. Charges: K, R = +1; D, E = -1; H = 0. + +kappa is reported as 0 (not NaN) when the sequence has no charges or only one +charge type, where oppositely charged residues cannot be segregated. A blob +size longer than the sequence is reduced to the sequence length. +``` + +```python +result = protpy.kappa(protein_seq) +# Shape: 1 x (1 + len(blob_sizes)) (default 1 x 3) +``` + +- **Columns:** `Kappa`, `Kappa_5`, `Kappa_6` +- **Parameters:** `blob_sizes`: tuple[int] (default=(5, 6)) — blob sizes averaged +- **Descriptor set:** extended, all +- **Reference:** Das & Pappu (2013). PNAS, 110(33), 13392-13397. https://doi.org/10.1073/pnas.1304749110 + +--- + +#### `sequence_charge_decoration` (SCD) + +Sequence charge decoration: (1/L) sum_{m>n} q_m q_n sqrt(m-n). + +```text +Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh: + +SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} + +with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values +indicate more segregated opposite charges; uncharged sequences give 0. +``` + +```python +result = protpy.sequence_charge_decoration(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `Charge_SCD` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Sawle & Ghosh (2015). J. Chem. Phys., 143(8), 085101. https://doi.org/10.1063/1.4929391 + +--- + +#### `charge_density` (ChargeDensity) + +Net charge at the given pH divided by molecular weight (charges per Da). + +```text +Calculate the charge density of a protein sequence: the net charge at +``ph`` (from ``charge_distribution``) divided by the molecular weight (from +``molecular_weight``), in charges per Da. + +ChargeDensity = NetCharge(pH) / MW +``` + +```python +result = protpy.charge_density(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `ChargeDensity` +- **Parameters:** `ph`: float (default=7.4) — pH +- **Descriptor set:** extended, all +- **Reference:** Müller et al. (2017). Bioinformatics, 33(17), 2753-2755 (modlAMP). https://doi.org/10.1093/bioinformatics/btx285 + +--- + +#### `hydrophobicity_profile` (HP) + +Summary statistics of a sliding-window hydrophobicity profile (ProtScale-style edge weighting). + +```text +Summarise a sliding-window hydrophobicity profile. Each residue is +replaced by its scale value and a window of length ``window`` is slid along +the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` +(ExPASy ProtScale semantics). The profile is reduced to its mean, max, min +and standard deviation, plus the fraction of windows above ``threshold`` +when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). +Windows longer than the sequence use the whole sequence. +``` + +```python +result = protpy.hydrophobicity_profile(protein_seq) +# Shape: 1 x 4 (1 x 5 with threshold) +``` + +- **Columns:** `HP_kyte_doolittle_w9_mean`, `HP_kyte_doolittle_w9_max`, `HP_kyte_doolittle_w9_min`, `HP_kyte_doolittle_w9_std` +- **Parameters:** `scale`: str (default='kyte_doolittle') — single-value scale or AAindex code; `window`: int (default=9) — window length; `edge`: float (default=1.0) — relative end weight; `threshold`: float | None (default=None) — adds fraction of windows above threshold +- **Descriptor set:** extended, all +- **Reference:** Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132. https://doi.org/10.1016/0022-2836(82)90515-0 + +--- + +#### `hydrophobic_ratio` (HydrophobicRatio) + +APD hydrophobic ratio: percentage of L, I, V, F, M, A, C, W residues. + +```text +Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) +: the percentage of residues that are L, I, V, F, M, A, C or W. + +HydrophobicRatio = N(LIVFMACW) / L x 100 +``` + +```python +result = protpy.hydrophobic_ratio(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `HydrophobicRatio` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Wang (2020). Protein Sci., 29(1), 8-18 (APD); Wang, Li & Wang (2016). Nucleic Acids Res., 44, D1087 (APD3). https://doi.org/10.1002/pro.3702 + +--- + +#### `half_life` (HalfLife) + +N-end rule half-life (hours) in mammalian, yeast and E. coli systems from the N-terminal residue. + +```text +Estimate the in vivo half-life of a protein from its N-terminal residue by +the N-end rule, as tabulated by ExPASy ProtParam: mammalian +reticulocytes (in vitro), yeast and E. coli (in vivo). + +Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" +in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli +value for an N-terminal Pro is unknown in the source and returned as NaN. +Not applicable to N-terminally modified proteins. +``` + +```python +result = protpy.half_life(protein_seq) +# Shape: 1 x 3 +``` + +- **Columns:** `HalfLife_Mammal_h`, `HalfLife_Yeast_h`, `HalfLife_Ecoli_h` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Bachmair, Finley & Varshavsky (1986). Science, 234(4773), 179-186. https://doi.org/10.1126/science.3018930 + +--- + +#### `ivywrel_index` (IVYWREL) + +Fraction of I, V, Y, W, R, E, L residues (thermophilic adaptation index). + +```text +Calculate the IVYWREL index of Zeldovich et al.: the fraction of residues +that are I, V, Y, W, R, E or L, which correlates with optimal growth +temperature. + +IVYWREL = N(IVYWREL) / L +``` + +```python +result = protpy.ivywrel_index(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `IVYWREL` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Zeldovich, Berezovsky & Shakhnovich (2007). PLoS Comput. Biol., 3(1), e5. https://doi.org/10.1371/journal.pcbi.0030005 + +--- + +#### `cvp_bias` (CvP) + +Charged versus polar bias: %(D+E+K+R) - %(N+Q+S+T). + +```text +Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie, an +indicator of hyperthermostability: + +CvP = %(D + E + K + R) - %(N + Q + S + T) +``` + +```python +result = protpy.cvp_bias(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `CvP_Bias` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Suhre & Claverie (2003). J. Biol. Chem., 278(19), 17198-17202. https://doi.org/10.1074/jbc.M301327200 + +--- + +#### `flexibility` (Flex) + +Summary statistics of the Vihinen average-B-value flexibility profile (window 9, end weight 0.25). + +```text +Summarise the flexibility profile of Vihinen et al.. Average normalised +B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged +over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, +0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced +to its mean, max, min and standard deviation. + +The neighbour-dependent parameter sets of (residues with 0, 1 or 2 rigid +neighbours) are not used. Compared with Biopython's +``ProteinAnalysis.flexibility``, which uses the same values and weights, +protpy centres each window on its middle residue and includes the last +window; Biopython takes the residue after the centre and omits the last window. +``` + +```python +result = protpy.flexibility(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `Flex_mean`, `Flex_max`, `Flex_min`, `Flex_std` +- **Parameters:** `window`: int (default=9) — window length +- **Descriptor set:** extended, all +- **Reference:** Vihinen, Torkkila & Riikonen (1994). Proteins, 19(2), 141-149. https://doi.org/10.1002/prot.340190207 + +--- + +### Complexity + +#### `wootton_federhen_complexity` (WF) + +Wootton-Federhen local complexity (K1, K2) over sliding windows and the SEG-trigger low-complexity fraction. + +```text +Calculate Wootton-Federhen local compositional complexity. For every +window of length W with residue counts n_i: + +K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) +K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) + +The low-complexity fraction is the fraction of residues covered by at least +one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: +SEG's extension to the K2 extension threshold and its interval merging and +optimisation are not performed. A window longer than the sequence uses the +whole sequence. +``` + +```python +result = protpy.wootton_federhen_complexity(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `WF_K1_mean`, `WF_K2_mean`, `WF_K2_min`, `WF_LowComplexity_frac` +- **Parameters:** `window`: int (default=12) — window length; `k2_trigger`: float (default=2.2) — K2 trigger (bits) +- **Descriptor set:** extended, all +- **Reference:** Wootton & Federhen (1993). Comput. Chem., 17(2), 149-163. https://doi.org/10.1016/0097-8485(93)85006-X + +--- + +#### `linguistic_complexity` (LC) + +Linguistic complexity: distinct subwords observed / maximum possible (ratio of sums over word lengths). + +```text +Calculate the linguistic complexity (LC) of a protein sequence: the +number of distinct subwords (k-mers) observed, over the maximum number +possible for a sequence of that length over the 20-letter alphabet: + +LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) + +where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. +all subwords). This is the ratio-of-sums form, counting all subwords as in +; repetitive sequences have low LC. +``` + +```python +result = protpy.linguistic_complexity(protein_seq) +# Shape: 1 x 1 +``` + +- **Columns:** `LinguisticComplexity` +- **Parameters:** `max_k`: int | None (default=None) — longest word length (default L) +- **Descriptor set:** extended, all +- **Reference:** Troyanskaya et al. (2002). Bioinformatics, 18(5), 679-688. https://doi.org/10.1093/bioinformatics/18.5.679 + +--- + +#### `lempel_ziv_complexity` (LZ) + +Lempel-Ziv (LZ76) complexity c(n) and its normalised value c(n)·log20(n)/n. + +```text +Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence: the +number c(n) of new components found when the sequence is parsed from left to +right, each component being the shortest substring not producible by +copying from the preceding history. The normalised value for a 20-letter +alphabet is + +LZ_Normalized = c(n) x log_20(n) / n + +(0 for a single residue). +``` + +```python +result = protpy.lempel_ziv_complexity(protein_seq) +# Shape: 1 x 2 +``` + +- **Columns:** `LZ_Complexity`, `LZ_Normalized` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Lempel & Ziv (1976). IEEE Trans. Inf. Theory, 22(1), 75-81. https://doi.org/10.1109/TIT.1976.1055501 + +--- + +### Signal + +#### `fft_descriptors` (FFT) + +Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) in equal-width frequency bins. + +```text +Calculate a length-invariant binned power spectrum of a property signal, as +in the resonant recognition model of Cosic. Each residue is replaced by +its scale value (default: electron-ion interaction potential, EIIP), the +signal is mean-centred, and its power spectrum is + +P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue + +The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width +bins (the last bin includes 0.5) and the mean power in each bin is returned; +bins containing no frequency are 0. +``` + +```python +result = protpy.fft_descriptors(protein_seq) +# Shape: 1 x n_bins (default 1 x 10) +``` + +- **Columns:** `FFT_eiip_bin1`, `FFT_eiip_bin2`, `FFT_eiip_bin3`, `FFT_eiip_bin4`, `FFT_eiip_bin5`, `FFT_eiip_bin6`, … +- **Parameters:** `scale`: str | dict (default='eiip') — single-value scale, AAindex code or dict; `n_bins`: int (default=10) — number of frequency bins +- **Descriptor set:** extended, all +- **Reference:** Cosic (1994). IEEE Trans. Biomed. Eng., 41(12), 1101-1114. https://doi.org/10.1109/10.335859 + +--- + +#### `dct_descriptors` (DCT) + +First n orthonormal DCT-II coefficients of a mean-centred property signal (default Kyte-Doolittle). + +```text +Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II +discrete cosine transform (DCT-II) of a mean-centred property signal: + +X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), +s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 + +X_0 is 0 because the signal is mean-centred. +``` + +```python +result = protpy.dct_descriptors(protein_seq) +# Shape: 1 x n_coeffs (default 1 x 10) +``` + +- **Columns:** `DCT_kyte_doolittle_0`, `DCT_kyte_doolittle_1`, `DCT_kyte_doolittle_2`, `DCT_kyte_doolittle_3`, `DCT_kyte_doolittle_4`, `DCT_kyte_doolittle_5`, … +- **Parameters:** `scale`: str | dict (default='kyte_doolittle') — single-value scale, AAindex code or dict; `n_coeffs`: int (default=10) — number of coefficients (<= L) +- **Descriptor set:** extended, all +- **Reference:** Wang et al. (2017). PLoS ONE, 12(9), e0185587. https://doi.org/10.1371/journal.pone.0185587 + +--- + +### Structural + +#### `aggrescan` (AGG) + +AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot spots: >=5 residues above -0.02, no Pro). + +```text +Calculate AGGRESCAN aggregation "hot spot" descriptors. Each residue is +given its experimental aggregation propensity a3v; the a4v profile (AP) is the +a3v mean over a sliding window, assigned to the window's central residue. +Off-centre N- and C-terminal residues take the first and last window values. +A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot +threshold HST = -0.02 and containing no proline. Areas use trapezoidal +integration of the AP with HST as the zero axis. + +Outputs: + a3vSA mean a3v over the sequence + nHS number of hot spots; NnHS = nHS / L x 100 + AAT area of the AP above HST over the whole sequence; AATr = AAT / L + THSA total hot-spot area (sum of HSA); THSAr = THSA / L + TA total area of the AP (HST as zero axis, signed) + a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 + a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) + HSA_max largest single hot-spot area (0 if none) + +AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and +gives the two extreme residues specific charge-corrected values; neither +rule is given in the published supplementary material, so ``window`` is a +parameter (default 5) and the extreme residues are treated like the other +off-centre residues. +``` + +```python +result = protpy.aggrescan(protein_seq) +# Shape: 1 x 12 +``` + +- **Columns:** `AGG_a3vSA`, `AGG_nHS`, `AGG_NnHS`, `AGG_AAT`, `AGG_AATr`, `AGG_THSA`, … +- **Parameters:** `window`: int (default=5) — odd window length (AGGRESCAN uses 5, 7, 9, 11) +- **Descriptor set:** extended, all +- **Reference:** Conchillo-Solé et al. (2007). BMC Bioinformatics, 8, 65. https://doi.org/10.1186/1471-2105-8-65 + +--- + +#### `uversky_charge_hydropathy` (UV) + +Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.785 - - 1.151. + +```text +Calculate the charge-hydropathy descriptors of Uversky et al.: + + = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) + = |n+ - n-| / L (K/R positive, D/E negative) +HQ = 2.785 - - 1.151 + +The boundary = 2.785 - 1.151 separates natively unfolded from folded +proteins (as restated in); ``UV_Unfolded`` is 1 when HQ < 0. +``` + +```python +result = protpy.uversky_charge_hydropathy(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `UV_MeanH`, `UV_MeanR`, `UV_HQ`, `UV_Unfolded` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Uversky, Gillespie & Fink (2000). Proteins, 41(3), 415-427. https://doi.org/10.1002/1097-0134(20001115)41:3<415::AID-PROT130>3.0.CO;2-7 + +--- + +#### `fold_index` (FI) + +FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics. + +```text +Calculate FoldIndex profile descriptors. For each window of length +``window``: + +FI = 2.785 - || - 1.151 + +with and as in ``uversky_charge_hydropathy`` but computed over the +window. Residues covered by at least one window with FI < 0 are counted as +disordered. A window longer than the sequence uses the whole sequence, so +FI then equals ``UV_HQ``. +``` + +```python +result = protpy.fold_index(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `FI_mean`, `FI_min`, `FI_frac_disordered`, `FI_longest_disordered` +- **Parameters:** `window`: int (default=51) — window length +- **Descriptor set:** extended, all +- **Reference:** Prilusky et al. (2005). Bioinformatics, 21(16), 3435-3438. https://doi.org/10.1093/bioinformatics/bti537 + +--- + +#### `disorder_propensity` (DP) + +Fractions of disorder- and order-promoting residues and mean TOP-IDP value. + +```text +Calculate disorder-propensity descriptors: the fractions of +disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting +(W, C, F, I, Y, V, L, N) residues, and the mean TOP-IDP value +(higher = more disorder-prone). +``` + +```python +result = protpy.disorder_propensity(protein_seq) +# Shape: 1 x 3 +``` + +- **Columns:** `DP_frac_disorder_promoting`, `DP_frac_order_promoting`, `DP_TOP_IDP_mean` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Campen et al. (2008). Protein Pept. Lett., 15(9), 956-963. https://doi.org/10.2174/092986608785849164 + +--- + +#### `transmembrane_features` (TM) + +Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, TM fraction, positive-inside bias). + +```text +Calculate heuristic transmembrane-segment features from the Kyte-Doolittle +hydropathy profile. Windows of length ``window`` with mean hydropathy +above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative +transmembrane segments; overlapping windows are merged into segments. + +TM_n_segments number of merged segments +TM_max_KD maximum window hydropathy +TM_frac_in_TM fraction of residues inside a segment +TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment + +This is a hydropathy heuristic, not a topology predictor. +``` + +```python +result = protpy.transmembrane_features(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `TM_n_segments`, `TM_max_KD`, `TM_frac_in_TM`, `TM_positive_inside_bias` +- **Parameters:** `window`: int (default=19) — hydropathy window; `kd_threshold`: float (default=1.6) — segment threshold +- **Descriptor set:** extended, all +- **Reference:** Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132. https://doi.org/10.1016/0022-2836(82)90515-0 + +--- + +#### `signal_peptide_features` (SP) + +N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue hydropathy. + +```text +Calculate simple signal-peptide features of the N-terminus: + +SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) +SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues + +The von Heijne cleavage-site weight matrix is not included. +``` + +```python +result = protpy.signal_peptide_features(protein_seq) +# Shape: 1 x 2 +``` + +- **Columns:** `SP_nregion_charge`, `SP_hregion_maxKD` +- **Parameters:** `n_term`: int (default=30) — N-terminal region length +- **Descriptor set:** extended, all +- **Reference:** von Heijne (1983). Eur. J. Biochem., 133(1), 17-21. https://doi.org/10.1111/j.1432-1033.1983.tb07424.x + +--- + +#### `functional_site_counts` (FS) + +Counts and per-100-residue densities of PROSITE N-glycosylation, ER-retention and PTS1 motifs and tryptic sites. + +```text +Count functional sequence motifs and their density per 100 residues: +N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal +(PS00014) and peroxisomal targeting signal 1 (PS00342), and tryptic +cleavage sites (after K/R, not before P). Overlapping matches are +counted. These complement the default motifs of ``motif_composition``. +``` + +```python +result = protpy.functional_site_counts(protein_seq) +# Shape: 1 x 8 +``` + +- **Columns:** `FS_N_glycosylation`, `FS_ER_retention`, `FS_PTS1`, `FS_tryptic_site`, `FS_N_glycosylation_per100`, `FS_ER_retention_per100`, … +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Sigrist et al. (2013). Nucleic Acids Res., 41(D1), D344-D347 (PROSITE). https://doi.org/10.1093/nar/gks1067 + +--- + +#### `plaac` (PLAAC) + +PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and presence. + +```text +Calculate prion-like domain descriptors with the PLAAC model, ported from +the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each +residue has the log-likelihood ratio LLR = ln(f_prion / f_background). + +PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues +PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse +PLAAC_PrD_length length of the prion-like run containing the best core +PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + +As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than +``core_length`` / no prion-like run of that length); PrD_length is then 0. +``` + +```python +result = protpy.plaac(protein_seq) +# Shape: 1 x 4 +``` + +- **Columns:** `PLAAC_LLR`, `PLAAC_COREscore`, `PLAAC_PrD_length`, `PLAAC_has_PrD` +- **Parameters:** `core_length`: int (default=60) — minimal core length +- **Descriptor set:** extended, all +- **Reference:** Lancaster et al. (2014). Bioinformatics, 30(17), 2501-2502. https://doi.org/10.1093/bioinformatics/btu310 + +--- + +#### `antigenicity_surface` (Antigenicity) + +Profile statistics of the Parker hydrophilicity and Janin surface scales over 7-residue windows. + +```text +Summarise hydrophilicity/surface profiles associated with antigenic sites: +the Parker HPLC hydrophilicity scale averaged over 7-residue windows and +the Janin free energy of transfer from inside to outside of a globular +protein over the same window. Each profile is reduced to its mean, max, +min and standard deviation. +``` + +```python +result = protpy.antigenicity_surface(protein_seq) +# Shape: 1 x 8 +``` + +- **Columns:** `PARKER_mean`, `PARKER_max`, `PARKER_min`, `PARKER_std`, `JANIN_mean`, `JANIN_max`, … +- **Parameters:** `window`: int (default=7) — window length +- **Descriptor set:** extended, all +- **Reference:** Parker, Guo & Hodges (1986). Biochemistry, 25(19), 5425-5432. https://doi.org/10.1021/bi00367a013 + +--- + +#### `cysteine_features` (Cys) + +Cysteine count, fraction, parity, maximum disulfides, CxxC motifs and mean spacing. + +```text +Calculate cysteine features: + +Cys_count number of C +Cys_frac C / L +Cys_even 1 if the count is even +Cys_max_disulfides floor(count / 2) +Cys_CxxC overlapping C-x-x-C motifs +Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) +``` + +```python +result = protpy.cysteine_features(protein_seq) +# Shape: 1 x 6 +``` + +- **Columns:** `Cys_count`, `Cys_frac`, `Cys_even`, `Cys_max_disulfides`, `Cys_CxxC`, `Cys_mean_spacing` +- **Parameters:** sequence only +- **Descriptor set:** extended, all +- **Reference:** Fomenko & Gladyshev (2003). Biochemistry, 42(38), 11214-11225. https://doi.org/10.1021/bi034459s + +--- + +### Extensions to existing descriptors (v1.5.0) + +Defaults are unchanged; the new behaviour is opt-in. + +| Function | New parameter(s) | Effect | Reference | +|---|---|---|---| +| `reduced_alphabet_composition` | `alphabet`, `k` | `alphabet="ifeature5"` with `k=1/2/3` gives GAAC (1 x 5), GDPC (1 x 25), GTPC (1 x 125); cp13/14/19/20 also accepted | Chen et al. 2018, 10.1093/bioinformatics/bty140 | +| `conjoint_triad` | `gap` | `gap>0` gives KSCTriad, triads (s_i, s_{i+g+1}, s_{i+2g+2}) for g = 0..gap, columns suffixed `_gap{g}` | Chen et al. 2018; Shen et al. 2007, 10.1073/pnas.0607879104 | +| `ctd_` | `property_set` | `"ifeature13"` adds 6 AAindex hydrophobicity partitions (13 properties, 1 x 273) | Dubchak et al. 1995, 10.1073/pnas.92.19.8700 | +| `amino_acid_composition`, `dipeptide_composition` | `region`, `n` | `"nterm"`, `"cterm"` or `"split"` (N-terminal / middle / C-terminal blocks, prefixes `NT_`/`MID_`/`CT_`) | Kumar et al. 2006, 10.1074/jbc.M511061200 | +| `isoelectric_point`, `charge_distribution` | `pka_scale` | `"emboss"`, `"bjellqvist"` (with residue-specific terminal pKa) or `"ipc_protein"` | Bjellqvist et al. 1993, 10.1002/elps.11501401163; Kozlowski 2016, 10.1186/s13062-016-0159-9 | +| `gravy` | `scale` | average any single-value scale or AAindex code (column `GRAVY_{scale}`) | per scale | +| `hydrophobic_moment` | `scale`, `angle="helix"/"sheet"` | alternative hydrophobicity scale; named angles 100° / 160° | Eisenberg et al. 1984, 10.1073/pnas.81.1.140 | +| `extinction_coefficient` | `absorbance` | adds `Abs01_Reduced`, `Abs01_Oxidized` (A280 of a 1 g/L solution, ε / MW) | Pace et al. 1995, 10.1002/pro.5560041120 | + +--- + ## Utility Functions ### `all_descriptors` @@ -715,12 +1902,20 @@ Compute all available descriptors for a single protein sequence and return them | `show_progress` | bool | `False` | Display a `tqdm` progress bar naming each descriptor as it is computed | | `output_file` | str or None | `None` | Path to save results; `None` disables file output | | `save_each_sequence` | bool | `True` | Append this row to the output CSV immediately after computation; useful when calling in a loop | +| `descriptor_set` | str | `"classic"` | `"classic"` (v1.4 set, unchanged), `"extended"` (adds the v1.5 descriptors with ≤150 columns) or `"all"` (adds every v1.5 descriptor) | +| `include` | list[str] or None | `None` | Extra registered descriptors to compute | +| `exclude` | list[str] or None | `None` | Descriptors to skip (classic ones included, e.g. `"tripeptide_composition"`) | ```python import protpy seq = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDISIQEFKD" +# v1.5 descriptor sets +df = protpy.all_descriptors(seq, descriptor_set="extended") +df = protpy.all_descriptors(seq, include=["kappa", "plaac"], exclude=["tripeptide_composition"], + params={"kappa": {"blob_sizes": (5,)}}) + # Basic usage df = protpy.all_descriptors(seq) # Shape: 1 x 10757 (all descriptor features concatenated) @@ -923,6 +2118,9 @@ The descriptors implemented in protpy are based on the following published metho - Dubchak, I., et al. (1995). Prediction of protein folding class using global description of amino acid sequence. *PNAS*, 92(19), 8700–8704. - Dubchak, I., et al. (1999). Recognition of a protein fold in the context of the SCOP classification. *Proteins*, 35(4), 401–407. +**v1.5.0 descriptors** +- Each v1.5.0 descriptor above lists its primary reference and DOI. BibTeX for every reference is in [`docs/references.bib`](docs/references.bib). + **Sequence Order** - Grantham, R. (1974). Amino acid difference formula to help explain protein evolution. *Science*, 185(4154), 862–864. - Schneider, G., & Wrede, P. (1994). The rational design of amino acid sequences by artificial neural networks and simulated molecular evolution. *Biophysical Journal*, 66(2), 335–344. diff --git a/README.md b/README.md index c478308..e792a5f 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,24 @@ The descriptors available in `protpy` include: +### New in v1.5.0 + +| Category | New descriptors | +|---|---| +| Composition | `cksaap`, `cksaagp`, `dde`, `asdc`, `distance_pair`, `natural_vector`, `atomic_composition` | +| Scales | `z_scales_5`, `vhse`, `atchley_factors`, `t_scales`, `st_scales`, `protfp`, `fasgai`, `blosum_indices`, `ms_whim`, `auto_cross_covariance`, `physicochemical_distance_transformation` | +| Physicochemical | `charge_fractions`, `ncpr`, `kappa`, `sequence_charge_decoration`, `charge_density`, `hydrophobicity_profile`, `hydrophobic_ratio`, `half_life`, `ivywrel_index`, `cvp_bias`, `flexibility` | +| Complexity | `wootton_federhen_complexity`, `linguistic_complexity`, `lempel_ziv_complexity` | +| Signal | `fft_descriptors`, `dct_descriptors` | +| Structural | `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac`, `antigenicity_surface`, `cysteine_features` | +| Extensions | GAAC/GDPC/GTPC (`reduced_alphabet_composition(alphabet="ifeature5", k=...)`), KSCTriad (`conjoint_triad(gap=...)`), 13-property CTD (`ctd_(property_set="ifeature13")`), split AAC/DPC (`region=`), `pka_scale=` for pI/charge, `scale=` for GRAVY/hydrophobic moment, A280 absorbance | + +`all_descriptors` and `encode_dataset` take `descriptor_set="classic"` (default, unchanged output), `"extended"` or `"all"`, plus `include=` / `exclude=` lists: + +```python +df = protpy.all_descriptors(protein_seq, descriptor_set="extended") +``` + More detail of each descriptor is listed in the markdown file: [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md) diff --git a/ToDo.md b/ToDo.md index 25b766e..673cdaa 100644 --- a/ToDo.md +++ b/ToDo.md @@ -56,7 +56,7 @@ - [X] In "Parameters' and 'returns' , remove space between colon. - [X] Read over code. - [X] Create demo -- [ ] Add equations of descriptors to markdown file. https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-015-0554-8#Sec10 - https://bmcbioinformatics.biomedcentral.com/articles/10.1186/1471-2105-8-300 +- [x] Add equations of descriptors to markdown file (v1.5.0 descriptors in DESCRIPTORS.md). https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-015-0554-8#Sec10 - https://bmcbioinformatics.biomedcentral.com/articles/10.1186/1471-2105-8-300 - [X] Remove python 3.7 references, 3.8 minimum. - [X] In descriptor comments, dimensions of output should be 1 x N rather than N x 1 (N=# of features). - [X] Remove biopython from requirments & setup.py - required for testing. diff --git a/docs/api.rst b/docs/api.rst index 2af9546..934ff95 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -34,7 +34,14 @@ Composition boman_index, aggregation_propensity, hydrophobic_moment, - shannon_entropy + shannon_entropy, + cksaap, + cksaagp, + dde, + asdc, + distance_pair, + natural_vector, + atomic_composition :undoc-members: :show-inheritance: @@ -82,11 +89,51 @@ Sequence Order :undoc-members: :show-inheritance: +Scales +------ + +.. automodule:: protpy.scales + :members: + :undoc-members: + :show-inheritance: + +Physicochemical +--------------- + +.. automodule:: protpy.physicochemical + :members: + :undoc-members: + :show-inheritance: + +Complexity +---------- + +.. automodule:: protpy.complexity + :members: wootton_federhen_complexity, linguistic_complexity, lempel_ziv_complexity + :undoc-members: + :show-inheritance: + +Signal +------ + +.. automodule:: protpy.signal + :members: + :undoc-members: + :show-inheritance: + +Structural +---------- + +.. automodule:: protpy.structural + :members: + :undoc-members: + :show-inheritance: + Encode ------ .. automodule:: protpy.encode - :members: all_descriptors + :members: all_descriptors, encode_dataset :undoc-members: :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index fd4dd2f..606cbbc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ project = 'protpy' copyright = '2026, AJ McKenna' author = 'AJ McKenna' -release = '1.4.2' +release = '1.5.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/descriptors.rst b/docs/descriptors.rst index 4fc1463..ccfa1a8 100644 --- a/docs/descriptors.rst +++ b/docs/descriptors.rst @@ -1109,6 +1109,1166 @@ the results. ---- +v1.5.0 Descriptors +------------------ + +Descriptors added in v1.5.0, each with its formula, output shape and primary reference. +``descriptor_set="extended"`` in ``all_descriptors`` / ``encode_dataset`` adds the descriptors marked +*extended*; ``"all"`` adds the rest as well. See ``planning/BLOCKED.md`` for plan items that +could not be implemented without unverifiable data. + +cksaap (CKSAAP) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composition of k-spaced amino acid pairs: CKSAAP(XY,g) = N_g(XY) / (L-g-1) x 100 for g = 0..gap. + +.. code-block:: text + + Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein + sequence. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) + is counted and divided by the number of such pairs: + + CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 + + g = 0 is the dipeptide composition. Values are percentages; a spacing with no + pairs (L - g - 1 <= 0) gives zeros. + +.. code-block:: python + + result = protpy.cksaap(protein_seq) + # Shape: 1 x 400·(gap+1) (default 1 x 2400) + +* **Parameters:** ``gap``: int (default=5) — maximum spacing +* **Descriptor set:** all +* **Reference:** Chen, Kurgan & Ruan (2007). BMC Struct. Biol., 7, 25. https://doi.org/10.1186/1472-6807-7-25 + +---- + +cksaagp (CKSAAGP) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composition of k-spaced amino acid group pairs over the iFeature 5-group alphabet. + +.. code-block:: text + + Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP). + As CKSAAP, but residues are first mapped to the five iFeature groups: + aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and + uncharged (STCPNQ). + + CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 + + Values are percentages and the 25 values for each spacing sum to 100 + (0 when the spacing has no pairs). + +.. code-block:: python + + result = protpy.cksaagp(protein_seq) + # Shape: 1 x 25·(gap+1) (default 1 x 150) + +* **Parameters:** ``gap``: int (default=5) — maximum spacing +* **Descriptor set:** extended, all +* **Reference:** Chen et al. (2018). Bioinformatics, 34(14), 2499-2502 (iFeature). https://doi.org/10.1093/bioinformatics/bty140 + +---- + +dde (DDE) — Composition +~~~~~~~~~~~~~~~~~~~~~~~ + +Dipeptide deviation from expected mean: (Dc - Tm) / sqrt(Tv), with Tm from codon counts. + +.. code-block:: text + + Calculate the Dipeptide Deviation from Expected Mean (DDE). The observed + dipeptide composition is standardised against the composition expected from + the number of codons coding each residue: + + Dc(XY) = N(XY) / (L - 1) + Tm(XY) = (C_X / 61) x (C_Y / 61) + Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) + DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) + + where C_X is the number of sense codons for residue X in the standard genetic + code (61 in total). DDE is a standardised statistic, not a percentage. + +.. code-block:: python + + result = protpy.dde(protein_seq) + # Shape: 1 x 400 + +* **Parameters:** sequence only +* **Descriptor set:** all +* **Reference:** Saravanan & Gautham (2015). OMICS, 19(10), 648-658. https://doi.org/10.1089/omi.2015.0095 + +---- + +asdc (ASDC) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Adaptive skip dipeptide composition: composition of all ordered pairs (s_i, s_j), i < j. + +.. code-block:: text + + Calculate the Adaptive Skip Dipeptide Composition (ASDC): the composition + of every ordered residue pair (s_i, s_j) with i < j, over all gaps: + + ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 + + The denominator is the total number of ordered pairs, L(L-1)/2. Values are + percentages and sum to 100 (zeros for a single residue). + +.. code-block:: python + + result = protpy.asdc(protein_seq) + # Shape: 1 x 400 + +* **Parameters:** sequence only +* **Descriptor set:** all +* **Reference:** Wei et al. (2018). Bioinformatics, 34(23), 4007-4016 (ACPred-FL). https://doi.org/10.1093/bioinformatics/bty451 + +---- + +distance_pair (DP) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PseAAC of distance-pairs over a reduced cluster profile (cp13/cp14/cp19/cp20). + +.. code-block:: text + + Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet + (iDNA-Prot|dis). Residues are mapped to the clusters of a cluster + profile cp(n) and the vector is + + [f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] + + where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the + composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the + nearest-neighbour pair. This follows Eqs. 5-12 of, giving G + G^2·rng + values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. + + Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of. The + printed cp(14) lists E twice; the stray E in the first cluster is dropped, + the only reading with 14 clusters. + +.. code-block:: python + + result = protpy.distance_pair(protein_seq) + # Shape: 1 x (G + G²·rng) (default 1 x 602) + +* **Parameters:** ``rng``: int (default=3) — maximum pair distance; ``alphabet``: str (default='cp14') — cluster profile +* **Descriptor set:** all +* **Reference:** Liu et al. (2014). PLoS ONE, 9(9), e106691 (iDNA-Prot|dis). https://doi.org/10.1371/journal.pone.0106691 + +---- + +natural_vector (NV) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Natural vector: count, mean position and normalised second central moment of each amino acid. + +.. code-block:: text + + Calculate the Natural Vector of a protein sequence. For each amino acid k: + + n_k = number of occurrences of k + mu_k = mean (1-based) position of k + D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) + + mu_k and D2_k are 0 when k does not occur. Values are not percentages. + +.. code-block:: python + + result = protpy.natural_vector(protein_seq) + # Shape: 1 x 60 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Yu et al. (2013). J. Theor. Biol., 318, 197-204. https://doi.org/10.1016/j.jtbi.2012.11.005 + +---- + +atomic_composition (ATC) — Composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Percentage of C, H, N, O and S atoms over the residues' free amino-acid formulas. + +.. code-block:: text + + Calculate the Atomic Composition (ATC) of a protein sequence: the + percentage of C, H, N, O and S atoms, summed over the molecular formulas of + the residues' free amino acids. + + ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 + + By default free amino-acid formulas are summed without removing water, + matching Pfeature. With ``subtract_water=True``, (L - 1) H2O are removed + for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" + -> C4H8N2O3). + +.. code-block:: python + + result = protpy.atomic_composition(protein_seq) + # Shape: 1 x 5 + +* **Parameters:** ``subtract_water``: bool (default=False) — remove (L-1) H2O +* **Descriptor set:** extended, all +* **Reference:** Pande et al. (2023). J. Comput. Biol., 30(2), 204-222 (Pfeature). https://doi.org/10.1089/cmb.2022.0241 + +---- + +z_scales_5 (Z5) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the Sandberg 5 z-scales over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean z_scales_5 factor-scale descriptor of a protein sequence (Sandberg 5 z-scales). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.z_scales_5(protein_seq) + # Shape: 1 x 5·domains (default 1 x 5) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Sandberg et al. (1998). J. Med. Chem., 41(14), 2481-2491. https://doi.org/10.1021/jm9700575 + +---- + +vhse (VHSE) — Scales +~~~~~~~~~~~~~~~~~~~~ + +Mean of the VHSE (hydrophobic, steric, electronic) scales over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean vhse factor-scale descriptor of a protein sequence (principal components score Vectors of Hydrophobic, Steric and Electronic properties). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.vhse(protein_seq) + # Shape: 1 x 8·domains (default 1 x 8) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Mei et al. (2005). Biopolymers, 80(6), 775-786. https://doi.org/10.1002/bip.20296 + +---- + +atchley_factors (AF) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the Atchley factors I-V over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean atchley_factors factor-scale descriptor of a protein sequence (Atchley factors I-V). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.atchley_factors(protein_seq) + # Shape: 1 x 5·domains (default 1 x 5) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Atchley et al. (2005). PNAS, 102(18), 6395-6400. https://doi.org/10.1073/pnas.0408677102 + +---- + +t_scales (T) — Scales +~~~~~~~~~~~~~~~~~~~~~ + +Mean of the topological T-scales over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean t_scales factor-scale descriptor of a protein sequence (topological T-scales). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.t_scales(protein_seq) + # Shape: 1 x 5·domains (default 1 x 5) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Tian, Zhou & Li (2007). J. Mol. Struct., 830, 106-115. https://doi.org/10.1016/j.molstruc.2006.07.004 + +---- + +st_scales (ST) — Scales +~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the structural topology ST-scales over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean st_scales factor-scale descriptor of a protein sequence (structural topology ST-scales). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.st_scales(protein_seq) + # Shape: 1 x 8·domains (default 1 x 8) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Yang et al. (2010). Amino Acids, 38(3), 805-816. https://doi.org/10.1007/s00726-009-0287-y + +---- + +protfp (ProtFP) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the ProtFP (PCA, 8 components) over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean protfp factor-scale descriptor of a protein sequence (ProtFP PCA, 8 components). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.protfp(protein_seq) + # Shape: 1 x 8·domains (default 1 x 8) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** van Westen et al. (2013). J. Cheminform., 5, 41. https://doi.org/10.1186/1758-2946-5-41 + +---- + +fasgai (FASGAI) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the FASGAI factor scales over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean fasgai factor-scale descriptor of a protein sequence (factor analysis scales of generalized amino acid information). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.fasgai(protein_seq) + # Shape: 1 x 6·domains (default 1 x 6) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Liang & Li (2007). QSAR Comb. Sci., 26(6), 754-763. https://doi.org/10.1002/qsar.200630145 + +---- + +blosum_indices (BLOSUM) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the BLOSUM62-derived indices over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean blosum_indices factor-scale descriptor of a protein sequence (BLOSUM62-derived indices). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.blosum_indices(protein_seq) + # Shape: 1 x 10·domains (default 1 x 10) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Georgiev (2009). J. Comput. Biol., 16(5), 703-723. https://doi.org/10.1089/cmb.2008.0173 + +---- + +ms_whim (MSWHIM) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean of the MS-WHIM scores over the sequence (optionally per domain). + +.. code-block:: text + + Calculate the mean ms_whim factor-scale descriptor of a protein sequence (MS-WHIM scores). + See ``aa_scale_descriptors`` for the formula. + +.. code-block:: python + + result = protpy.ms_whim(protein_seq) + # Shape: 1 x 3·domains (default 1 x 3) + +* **Parameters:** ``domains``: int (default=1) — number of near-equal segments +* **Descriptor set:** extended, all +* **Reference:** Zaliani & Gancia (1999). J. Chem. Inf. Comput. Sci., 39(3), 525-533. https://doi.org/10.1021/ci980211b + +---- + +auto_cross_covariance (ACC) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Auto/cross-covariance of an amino-acid scale: sum z_j(i) z_k(i+l) / (L-l). + +.. code-block:: text + + Calculate the auto- and cross-covariance (ACC) transform of an amino-acid + scale. For lag l and scale dimensions j, k: + + ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) + + ``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto + covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a + protpy factor-scale family (used as published) or an AAindex1 code / list + of codes, which are standardised over the 20 amino acids. + +.. code-block:: python + + result = protpy.auto_cross_covariance(protein_seq) + # Shape: 1 x d²·lag (default 1 x 125) + +* **Parameters:** ``scale``: str | list[str] (default='z_scales_5') — factor scale or AAindex code(s); ``lag``: int (default=5) — maximum lag; ``mode``: str (default='acc') — 'acc', 'ac' or 'cc' +* **Descriptor set:** all +* **Reference:** Wold et al. (1993). Anal. Chim. Acta, 277(2), 239-253. https://doi.org/10.1016/0003-2670(93)80437-P + +---- + +physicochemical_distance_transformation (PDT) — Scales +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Physicochemical distance transformation: mean squared difference of standardised AAindex properties at lag l. + +.. code-block:: text + + Calculate the Physicochemical Distance Transformation (PDT). For each + AAindex property P_j (standardised over the 20 amino acids) and lag l: + + PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) + +.. code-block:: python + + result = protpy.physicochemical_distance_transformation(protein_seq) + # Shape: 1 x len(properties)·lag (default 1 x 24) + +* **Parameters:** ``lag``: int (default=3) — maximum lag; ``properties``: list[str] | None — AAindex codes (default: 8 autocorrelation properties) +* **Descriptor set:** extended, all +* **Reference:** Liu et al. (2012). PLoS ONE, 7(9), e46633. https://doi.org/10.1371/journal.pone.0046633 + +---- + +charge_fractions (FCR) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fraction of charged residues (FCR), positive/negative fractions and charge asymmetry sigma = NCPR²/FCR. + +.. code-block:: text + + Calculate the charge fractions of a protein sequence: + + FCR = (n+ + n-) / L fraction of charged residues + FPlus = n+ / L, FMinus = n- / L + Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) + + where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. + +.. code-block:: python + + result = protpy.charge_fractions(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Das & Pappu (2013). PNAS, 110(33), 13392-13397. https://doi.org/10.1073/pnas.1304749110 + +---- + +ncpr (NCPR) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Net charge per residue: (n+ - n-) / L with K/R positive and D/E negative. + +.. code-block:: text + + Calculate the Net Charge Per Residue (NCPR) of a protein sequence: + + NCPR = (n+ - n-) / L + + with n+ counting K/R and n- counting D/E. + +.. code-block:: python + + result = protpy.ncpr(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Mao et al. (2010). PNAS, 107(18), 8183-8188. https://doi.org/10.1073/pnas.0911107107 + +---- + +kappa (Kappa) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Das-Pappu kappa: normalised blob charge-asymmetry deviation measuring segregation of opposite charges (0 mixed, 1 segregated). + +.. code-block:: text + + Calculate the kappa charge-patterning parameter of Das & Pappu. For blob + size g, the sequence is split into the L - g + 1 overlapping blobs and + + sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) + delta_g = mean over blobs of (sigma_b - sigma)^2 + kappa_g = delta_g / delta_max,g + + where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the + largest delta over the maximally segregated arrangements of the same + composition (a positive block and a negative block with the neutral residues + distributed over the three surrounding gaps; every distinct arrangement is + evaluated). kappa is the mean of kappa_g + over ``blob_sizes`` (5 and 6 in); 0 means well mixed and 1 fully + segregated. Charges: K, R = +1; D, E = -1; H = 0. + + kappa is reported as 0 (not NaN) when the sequence has no charges or only one + charge type, where oppositely charged residues cannot be segregated. A blob + size longer than the sequence is reduced to the sequence length. + +.. code-block:: python + + result = protpy.kappa(protein_seq) + # Shape: 1 x (1 + len(blob_sizes)) (default 1 x 3) + +* **Parameters:** ``blob_sizes``: tuple[int] (default=(5, 6)) — blob sizes averaged +* **Descriptor set:** extended, all +* **Reference:** Das & Pappu (2013). PNAS, 110(33), 13392-13397. https://doi.org/10.1073/pnas.1304749110 + +---- + +sequence_charge_decoration (SCD) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sequence charge decoration: (1/L) sum_{m>n} q_m q_n sqrt(m-n). + +.. code-block:: text + + Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh: + + SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} + + with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values + indicate more segregated opposite charges; uncharged sequences give 0. + +.. code-block:: python + + result = protpy.sequence_charge_decoration(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Sawle & Ghosh (2015). J. Chem. Phys., 143(8), 085101. https://doi.org/10.1063/1.4929391 + +---- + +charge_density (ChargeDensity) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Net charge at the given pH divided by molecular weight (charges per Da). + +.. code-block:: text + + Calculate the charge density of a protein sequence: the net charge at + ``ph`` (from ``charge_distribution``) divided by the molecular weight (from + ``molecular_weight``), in charges per Da. + + ChargeDensity = NetCharge(pH) / MW + +.. code-block:: python + + result = protpy.charge_density(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** ``ph``: float (default=7.4) — pH +* **Descriptor set:** extended, all +* **Reference:** Müller et al. (2017). Bioinformatics, 33(17), 2753-2755 (modlAMP). https://doi.org/10.1093/bioinformatics/btx285 + +---- + +hydrophobicity_profile (HP) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Summary statistics of a sliding-window hydrophobicity profile (ProtScale-style edge weighting). + +.. code-block:: text + + Summarise a sliding-window hydrophobicity profile. Each residue is + replaced by its scale value and a window of length ``window`` is slid along + the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` + (ExPASy ProtScale semantics). The profile is reduced to its mean, max, min + and standard deviation, plus the fraction of windows above ``threshold`` + when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). + Windows longer than the sequence use the whole sequence. + +.. code-block:: python + + result = protpy.hydrophobicity_profile(protein_seq) + # Shape: 1 x 4 (1 x 5 with threshold) + +* **Parameters:** ``scale``: str (default='kyte_doolittle') — single-value scale or AAindex code; ``window``: int (default=9) — window length; ``edge``: float (default=1.0) — relative end weight; ``threshold``: float | None (default=None) — adds fraction of windows above threshold +* **Descriptor set:** extended, all +* **Reference:** Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132. https://doi.org/10.1016/0022-2836(82)90515-0 + +---- + +hydrophobic_ratio (HydrophobicRatio) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +APD hydrophobic ratio: percentage of L, I, V, F, M, A, C, W residues. + +.. code-block:: text + + Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) + : the percentage of residues that are L, I, V, F, M, A, C or W. + + HydrophobicRatio = N(LIVFMACW) / L x 100 + +.. code-block:: python + + result = protpy.hydrophobic_ratio(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Wang (2020). Protein Sci., 29(1), 8-18 (APD); Wang, Li & Wang (2016). Nucleic Acids Res., 44, D1087 (APD3). https://doi.org/10.1002/pro.3702 + +---- + +half_life (HalfLife) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +N-end rule half-life (hours) in mammalian, yeast and E. coli systems from the N-terminal residue. + +.. code-block:: text + + Estimate the in vivo half-life of a protein from its N-terminal residue by + the N-end rule, as tabulated by ExPASy ProtParam: mammalian + reticulocytes (in vitro), yeast and E. coli (in vivo). + + Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" + in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli + value for an N-terminal Pro is unknown in the source and returned as NaN. + Not applicable to N-terminally modified proteins. + +.. code-block:: python + + result = protpy.half_life(protein_seq) + # Shape: 1 x 3 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Bachmair, Finley & Varshavsky (1986). Science, 234(4773), 179-186. https://doi.org/10.1126/science.3018930 + +---- + +ivywrel_index (IVYWREL) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fraction of I, V, Y, W, R, E, L residues (thermophilic adaptation index). + +.. code-block:: text + + Calculate the IVYWREL index of Zeldovich et al.: the fraction of residues + that are I, V, Y, W, R, E or L, which correlates with optimal growth + temperature. + + IVYWREL = N(IVYWREL) / L + +.. code-block:: python + + result = protpy.ivywrel_index(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Zeldovich, Berezovsky & Shakhnovich (2007). PLoS Comput. Biol., 3(1), e5. https://doi.org/10.1371/journal.pcbi.0030005 + +---- + +cvp_bias (CvP) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Charged versus polar bias: %(D+E+K+R) - %(N+Q+S+T). + +.. code-block:: text + + Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie, an + indicator of hyperthermostability: + + CvP = %(D + E + K + R) - %(N + Q + S + T) + +.. code-block:: python + + result = protpy.cvp_bias(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Suhre & Claverie (2003). J. Biol. Chem., 278(19), 17198-17202. https://doi.org/10.1074/jbc.M301327200 + +---- + +flexibility (Flex) — Physicochemical +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Summary statistics of the Vihinen average-B-value flexibility profile (window 9, end weight 0.25). + +.. code-block:: text + + Summarise the flexibility profile of Vihinen et al.. Average normalised + B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged + over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, + 0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced + to its mean, max, min and standard deviation. + + The neighbour-dependent parameter sets of (residues with 0, 1 or 2 rigid + neighbours) are not used. Compared with Biopython's + ``ProteinAnalysis.flexibility``, which uses the same values and weights, + protpy centres each window on its middle residue and includes the last + window; Biopython takes the residue after the centre and omits the last window. + +.. code-block:: python + + result = protpy.flexibility(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** ``window``: int (default=9) — window length +* **Descriptor set:** extended, all +* **Reference:** Vihinen, Torkkila & Riikonen (1994). Proteins, 19(2), 141-149. https://doi.org/10.1002/prot.340190207 + +---- + +wootton_federhen_complexity (WF) — Complexity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Wootton-Federhen local complexity (K1, K2) over sliding windows and the SEG-trigger low-complexity fraction. + +.. code-block:: text + + Calculate Wootton-Federhen local compositional complexity. For every + window of length W with residue counts n_i: + + K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) + K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) + + The low-complexity fraction is the fraction of residues covered by at least + one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: + SEG's extension to the K2 extension threshold and its interval merging and + optimisation are not performed. A window longer than the sequence uses the + whole sequence. + +.. code-block:: python + + result = protpy.wootton_federhen_complexity(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** ``window``: int (default=12) — window length; ``k2_trigger``: float (default=2.2) — K2 trigger (bits) +* **Descriptor set:** extended, all +* **Reference:** Wootton & Federhen (1993). Comput. Chem., 17(2), 149-163. https://doi.org/10.1016/0097-8485(93)85006-X + +---- + +linguistic_complexity (LC) — Complexity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Linguistic complexity: distinct subwords observed / maximum possible (ratio of sums over word lengths). + +.. code-block:: text + + Calculate the linguistic complexity (LC) of a protein sequence: the + number of distinct subwords (k-mers) observed, over the maximum number + possible for a sequence of that length over the 20-letter alphabet: + + LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) + + where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. + all subwords). This is the ratio-of-sums form, counting all subwords as in + ; repetitive sequences have low LC. + +.. code-block:: python + + result = protpy.linguistic_complexity(protein_seq) + # Shape: 1 x 1 + +* **Parameters:** ``max_k``: int | None (default=None) — longest word length (default L) +* **Descriptor set:** extended, all +* **Reference:** Troyanskaya et al. (2002). Bioinformatics, 18(5), 679-688. https://doi.org/10.1093/bioinformatics/18.5.679 + +---- + +lempel_ziv_complexity (LZ) — Complexity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Lempel-Ziv (LZ76) complexity c(n) and its normalised value c(n)·log20(n)/n. + +.. code-block:: text + + Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence: the + number c(n) of new components found when the sequence is parsed from left to + right, each component being the shortest substring not producible by + copying from the preceding history. The normalised value for a 20-letter + alphabet is + + LZ_Normalized = c(n) x log_20(n) / n + + (0 for a single residue). + +.. code-block:: python + + result = protpy.lempel_ziv_complexity(protein_seq) + # Shape: 1 x 2 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Lempel & Ziv (1976). IEEE Trans. Inf. Theory, 22(1), 75-81. https://doi.org/10.1109/TIT.1976.1055501 + +---- + +fft_descriptors (FFT) — Signal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) in equal-width frequency bins. + +.. code-block:: text + + Calculate a length-invariant binned power spectrum of a property signal, as + in the resonant recognition model of Cosic. Each residue is replaced by + its scale value (default: electron-ion interaction potential, EIIP), the + signal is mean-centred, and its power spectrum is + + P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue + + The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width + bins (the last bin includes 0.5) and the mean power in each bin is returned; + bins containing no frequency are 0. + +.. code-block:: python + + result = protpy.fft_descriptors(protein_seq) + # Shape: 1 x n_bins (default 1 x 10) + +* **Parameters:** ``scale``: str | dict (default='eiip') — single-value scale, AAindex code or dict; ``n_bins``: int (default=10) — number of frequency bins +* **Descriptor set:** extended, all +* **Reference:** Cosic (1994). IEEE Trans. Biomed. Eng., 41(12), 1101-1114. https://doi.org/10.1109/10.335859 + +---- + +dct_descriptors (DCT) — Signal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First n orthonormal DCT-II coefficients of a mean-centred property signal (default Kyte-Doolittle). + +.. code-block:: text + + Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II + discrete cosine transform (DCT-II) of a mean-centred property signal: + + X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), + s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 + + X_0 is 0 because the signal is mean-centred. + +.. code-block:: python + + result = protpy.dct_descriptors(protein_seq) + # Shape: 1 x n_coeffs (default 1 x 10) + +* **Parameters:** ``scale``: str | dict (default='kyte_doolittle') — single-value scale, AAindex code or dict; ``n_coeffs``: int (default=10) — number of coefficients (<= L) +* **Descriptor set:** extended, all +* **Reference:** Wang et al. (2017). PLoS ONE, 12(9), e0185587. https://doi.org/10.1371/journal.pone.0185587 + +---- + +aggrescan (AGG) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot spots: >=5 residues above -0.02, no Pro). + +.. code-block:: text + + Calculate AGGRESCAN aggregation "hot spot" descriptors. Each residue is + given its experimental aggregation propensity a3v; the a4v profile (AP) is the + a3v mean over a sliding window, assigned to the window's central residue. + Off-centre N- and C-terminal residues take the first and last window values. + A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot + threshold HST = -0.02 and containing no proline. Areas use trapezoidal + integration of the AP with HST as the zero axis. + + Outputs: + a3vSA mean a3v over the sequence + nHS number of hot spots; NnHS = nHS / L x 100 + AAT area of the AP above HST over the whole sequence; AATr = AAT / L + THSA total hot-spot area (sum of HSA); THSAr = THSA / L + TA total area of the AP (HST as zero axis, signed) + a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 + a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) + HSA_max largest single hot-spot area (0 if none) + + AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and + gives the two extreme residues specific charge-corrected values; neither + rule is given in the published supplementary material, so ``window`` is a + parameter (default 5) and the extreme residues are treated like the other + off-centre residues. + +.. code-block:: python + + result = protpy.aggrescan(protein_seq) + # Shape: 1 x 12 + +* **Parameters:** ``window``: int (default=5) — odd window length (AGGRESCAN uses 5, 7, 9, 11) +* **Descriptor set:** extended, all +* **Reference:** Conchillo-Solé et al. (2007). BMC Bioinformatics, 8, 65. https://doi.org/10.1186/1471-2105-8-65 + +---- + +uversky_charge_hydropathy (UV) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.785 - - 1.151. + +.. code-block:: text + + Calculate the charge-hydropathy descriptors of Uversky et al.: + + = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) + = |n+ - n-| / L (K/R positive, D/E negative) + HQ = 2.785 - - 1.151 + + The boundary = 2.785 - 1.151 separates natively unfolded from folded + proteins (as restated in); ``UV_Unfolded`` is 1 when HQ < 0. + +.. code-block:: python + + result = protpy.uversky_charge_hydropathy(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Uversky, Gillespie & Fink (2000). Proteins, 41(3), 415-427. https://doi.org/10.1002/1097-0134(20001115)41:3<415::AID-PROT130>3.0.CO;2-7 + +---- + +fold_index (FI) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics. + +.. code-block:: text + + Calculate FoldIndex profile descriptors. For each window of length + ``window``: + + FI = 2.785 - || - 1.151 + + with and as in ``uversky_charge_hydropathy`` but computed over the + window. Residues covered by at least one window with FI < 0 are counted as + disordered. A window longer than the sequence uses the whole sequence, so + FI then equals ``UV_HQ``. + +.. code-block:: python + + result = protpy.fold_index(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** ``window``: int (default=51) — window length +* **Descriptor set:** extended, all +* **Reference:** Prilusky et al. (2005). Bioinformatics, 21(16), 3435-3438. https://doi.org/10.1093/bioinformatics/bti537 + +---- + +disorder_propensity (DP) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fractions of disorder- and order-promoting residues and mean TOP-IDP value. + +.. code-block:: text + + Calculate disorder-propensity descriptors: the fractions of + disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting + (W, C, F, I, Y, V, L, N) residues, and the mean TOP-IDP value + (higher = more disorder-prone). + +.. code-block:: python + + result = protpy.disorder_propensity(protein_seq) + # Shape: 1 x 3 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Campen et al. (2008). Protein Pept. Lett., 15(9), 956-963. https://doi.org/10.2174/092986608785849164 + +---- + +transmembrane_features (TM) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, TM fraction, positive-inside bias). + +.. code-block:: text + + Calculate heuristic transmembrane-segment features from the Kyte-Doolittle + hydropathy profile. Windows of length ``window`` with mean hydropathy + above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative + transmembrane segments; overlapping windows are merged into segments. + + TM_n_segments number of merged segments + TM_max_KD maximum window hydropathy + TM_frac_in_TM fraction of residues inside a segment + TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment + + This is a hydropathy heuristic, not a topology predictor. + +.. code-block:: python + + result = protpy.transmembrane_features(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** ``window``: int (default=19) — hydropathy window; ``kd_threshold``: float (default=1.6) — segment threshold +* **Descriptor set:** extended, all +* **Reference:** Kyte & Doolittle (1982). J. Mol. Biol., 157(1), 105-132. https://doi.org/10.1016/0022-2836(82)90515-0 + +---- + +signal_peptide_features (SP) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue hydropathy. + +.. code-block:: text + + Calculate simple signal-peptide features of the N-terminus: + + SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) + SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues + + The von Heijne cleavage-site weight matrix is not included. + +.. code-block:: python + + result = protpy.signal_peptide_features(protein_seq) + # Shape: 1 x 2 + +* **Parameters:** ``n_term``: int (default=30) — N-terminal region length +* **Descriptor set:** extended, all +* **Reference:** von Heijne (1983). Eur. J. Biochem., 133(1), 17-21. https://doi.org/10.1111/j.1432-1033.1983.tb07424.x + +---- + +functional_site_counts (FS) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Counts and per-100-residue densities of PROSITE N-glycosylation, ER-retention and PTS1 motifs and tryptic sites. + +.. code-block:: text + + Count functional sequence motifs and their density per 100 residues: + N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal + (PS00014) and peroxisomal targeting signal 1 (PS00342), and tryptic + cleavage sites (after K/R, not before P). Overlapping matches are + counted. These complement the default motifs of ``motif_composition``. + +.. code-block:: python + + result = protpy.functional_site_counts(protein_seq) + # Shape: 1 x 8 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Sigrist et al. (2013). Nucleic Acids Res., 41(D1), D344-D347 (PROSITE). https://doi.org/10.1093/nar/gks1067 + +---- + +plaac (PLAAC) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and presence. + +.. code-block:: text + + Calculate prion-like domain descriptors with the PLAAC model, ported from + the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each + residue has the log-likelihood ratio LLR = ln(f_prion / f_background). + + PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues + PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse + PLAAC_PrD_length length of the prion-like run containing the best core + PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + + As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than + ``core_length`` / no prion-like run of that length); PrD_length is then 0. + +.. code-block:: python + + result = protpy.plaac(protein_seq) + # Shape: 1 x 4 + +* **Parameters:** ``core_length``: int (default=60) — minimal core length +* **Descriptor set:** extended, all +* **Reference:** Lancaster et al. (2014). Bioinformatics, 30(17), 2501-2502. https://doi.org/10.1093/bioinformatics/btu310 + +---- + +antigenicity_surface (Antigenicity) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Profile statistics of the Parker hydrophilicity and Janin surface scales over 7-residue windows. + +.. code-block:: text + + Summarise hydrophilicity/surface profiles associated with antigenic sites: + the Parker HPLC hydrophilicity scale averaged over 7-residue windows and + the Janin free energy of transfer from inside to outside of a globular + protein over the same window. Each profile is reduced to its mean, max, + min and standard deviation. + +.. code-block:: python + + result = protpy.antigenicity_surface(protein_seq) + # Shape: 1 x 8 + +* **Parameters:** ``window``: int (default=7) — window length +* **Descriptor set:** extended, all +* **Reference:** Parker, Guo & Hodges (1986). Biochemistry, 25(19), 5425-5432. https://doi.org/10.1021/bi00367a013 + +---- + +cysteine_features (Cys) — Structural +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Cysteine count, fraction, parity, maximum disulfides, CxxC motifs and mean spacing. + +.. code-block:: text + + Calculate cysteine features: + + Cys_count number of C + Cys_frac C / L + Cys_even 1 if the count is even + Cys_max_disulfides floor(count / 2) + Cys_CxxC overlapping C-x-x-C motifs + Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) + +.. code-block:: python + + result = protpy.cysteine_features(protein_seq) + # Shape: 1 x 6 + +* **Parameters:** sequence only +* **Descriptor set:** extended, all +* **Reference:** Fomenko & Gladyshev (2003). Biochemistry, 42(38), 11214-11225. https://doi.org/10.1021/bi034459s + +---- + Descriptor Summary ------------------ diff --git a/docs/usage.rst b/docs/usage.rst index bd40a16..f75df39 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -492,6 +492,31 @@ The descriptors that accept custom parameters and their available keyword argume Passing an unrecognised descriptor name or a descriptor that takes no extra parameters will raise a ``ValueError``. Passing a non-dict value for ``params`` will raise a ``TypeError``. +Every v1.5.0 descriptor that takes parameters can also be configured through ``params`` +(see ``protpy.get_descriptor_info(name)["parameters"]``). + +**Descriptor sets** — ``descriptor_set`` chooses how much is computed. ``"classic"`` (the +default) is the v1.4 set above with an unchanged output; ``"extended"`` adds every v1.5.0 +descriptor whose default output has at most 150 columns (factor scales, charge patterning, +hydrophobicity/stability, complexity, signal and structural heuristics); ``"all"`` also adds +the high-dimensional ones (CKSAAP, DDE, ASDC, distance pair, auto/cross covariance). +``include`` and ``exclude`` add or remove individual descriptors by name: + +.. code-block:: python + + df = protpy.all_descriptors(protein_seq, descriptor_set="extended") + df = protpy.all_descriptors(protein_seq, descriptor_set="all") + df = protpy.all_descriptors(protein_seq, + include=["kappa", "plaac"], + exclude=["tripeptide_composition"], + params={"kappa": {"blob_sizes": (5,)}}) + + # the same arguments work for whole datasets + df = protpy.encode_dataset(sequences, descriptor_set="extended", output_file=None) + +A few extended descriptors are ``NaN`` where their method leaves them undefined: +``plaac`` LLR/COREscore for sequences shorter than the core length or without a +prion-like domain, and ``half_life`` (E. coli) for an N-terminal proline. .. note:: A demo of the software is available `here `_. \ No newline at end of file diff --git a/examples/protpy_tutorial.ipynb b/examples/protpy_tutorial.ipynb index fd0559a..cb151f6 100644 --- a/examples/protpy_tutorial.ipynb +++ b/examples/protpy_tutorial.ipynb @@ -1708,6 +1708,32 @@ "plt.show()\n" ] }, + { + "cell_type": "markdown", + "id": "261dbc43", + "metadata": {}, + "source": [ + "## New in v1.5.0 — extended descriptor set\n", + "\n", + "v1.5.0 adds 44 literature-referenced descriptors (factor scales, charge patterning, complexity, signal and structural heuristics). `descriptor_set=\"extended\"` adds every new descriptor with at most 150 columns to `all_descriptors`; `\"all\"` adds the high-dimensional ones too. The default (`\"classic\"`) output is unchanged." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5f33308", + "metadata": {}, + "outputs": [], + "source": [ + "classic = protpy.all_descriptors(SEQ_MEDIUM, _show_descriptor_list=False)\n", + "extended = protpy.all_descriptors(SEQ_MEDIUM, descriptor_set=\"extended\", _show_descriptor_list=False)\n", + "print(f\"classic: {classic.shape[1]} features, extended: {extended.shape[1]} features\")\n", + "\n", + "# a few of the new descriptors\n", + "new_cols = [\"Kappa\", \"Charge_SCD\", \"UV_HQ\", \"PLAAC_has_PrD\", \"LZ_Normalized\", \"AGG_nHS\", \"VHSE_1\"]\n", + "extended[new_cols]" + ] + }, { "cell_type": "markdown", "id": "a067958b", diff --git a/protpy/__init__.py b/protpy/__init__.py index 327cc02..74b7dd7 100644 --- a/protpy/__init__.py +++ b/protpy/__init__.py @@ -17,7 +17,7 @@ #protPy package metadata __name__ = 'protpy' -__version__ = "1.4.2" +__version__ = "1.5.0" __description__ = "Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences." __author__ = 'AJ McKenna: https://github.com/amckenna41' __authorEmail__ = 'amckenna41@qub.ac.uk' diff --git a/pyproject.toml b/pyproject.toml index ca78273..a1b6e35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "protpy" -version = "1.4.2" +version = "1.5.0" description = "Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences." readme = "README.md" license = { text = "MIT" } diff --git a/tests/test_protpy.py b/tests/test_protpy.py index 331f7ba..bd99ad5 100644 --- a/tests/test_protpy.py +++ b/tests/test_protpy.py @@ -22,8 +22,8 @@ class ProtpyTests(unittest.TestCase): """ def test_protpy_metadata(self): """ Testing correct protpy version and metadata. """ - self.assertEqual(protpy.__version__, "1.4.2", - "protpy version is not correct, expected version 1.4.2, got version {}".format(protpy.__version__)) + self.assertEqual(protpy.__version__, "1.5.0", + "protpy version is not correct, expected version 1.5.0, got version {}".format(protpy.__version__)) self.assertEqual(protpy.__name__, "protpy", "protpy software name is not correct, expected protpy, got {}.".format(protpy.__name__)) self.assertEqual(protpy.__author__, "AJ McKenna: https://github.com/amckenna41", From fb18aaacc92b3c36a182602204355af472405a4a Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:38:54 +0100 Subject: [PATCH 13/17] v1.5.0: warning-free docs build, test/data READMEs, pytest collection fix, registry-signature and short-sequence tests --- CHANGELOG.md | 8 ++++- DESCRIPTORS.md | 42 ++++++++++++---------- docs/conf.py | 19 ++++++++++ docs/descriptors.rst | 42 ++++++++++++---------- docs/index.rst | 65 +++++++++++++++++++++++++++++++++++ protpy/composition.py | 4 +-- protpy/data/README.md | 9 ++++- protpy/signal.py | 2 +- protpy/structural.py | 38 +++++++++++--------- protpy/utils.py | 2 +- pyproject.toml | 3 +- tests/README.md | 25 +++++++++++--- tests/test_descriptor_sets.py | 17 +++++++++ tests/test_helpers.py | 6 ++++ 14 files changed, 219 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4426ac6..dfefcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Data files** with provenance (`name`, `values`, `reference`, `doi`, `source_note`): `protpy/data/aa_scales.json`, `reduced_alphabets.json`, `residue_tables.json`, `structural_params.json`. - Internal helpers in `protpy/_helpers.py`. The registry gains `doi`, `in_default_set` and `descriptor_sets`, and `print_descriptor_info` prints the DOI. New registry categories: Scales, Physicochemical, Complexity, Signal, Structural. - `docs/references.bib` with BibTeX for every registry DOI. -- Tests: golden backwards-compatibility snapshot of every v1.4.2 default output (`tests/test_backwards_compat.py`), helper/registry tests, one test module per new module, descriptor-set tests, and an opt-in performance test (`PROTPY_PERF=1`). +- Tests: golden backwards-compatibility snapshot of every v1.4.2 default output (`tests/test_backwards_compat.py`), helper/registry tests (including registry parameters matching every function signature and registry DOIs present in `docs/references.bib`), one test module per new module, descriptor-set tests (including every v1.5.0 descriptor on 1-8 residue sequences), and an opt-in performance test (`PROTPY_PERF=1`). - Plan items that could not be implemented without unverifiable source data (e.g. PseKRAAC, Kidera factors, GOR, COILS, Chou–Fasman) are listed with reasons in `planning/BLOCKED.md`. +### Documentation +- `docs/conf.py` converts protpy's `:name: type` parameter style to numpy style before napoleon runs. Parameter names, types and defaults now render correctly throughout the API docs; previously napoleon emitted nameless `:param :` fields. The Sphinx build is now warning-free (previously 39 warnings). +- `docs/index.rst` lists the v1.5.0 descriptor categories; `docs/api.rst` documents the new modules and `encode_dataset`; `docs/usage.rst` covers `descriptor_set` / `include` / `exclude`; `docs/descriptors.rst` and `DESCRIPTORS.md` have a v1.5.0 section generated from the registry and docstrings. +- `tests/README.md` lists every test module and test data file; `protpy/data/README.md` describes the new data files. + ### Changed +- pytest now collects only `tests/` (`testpaths`) and skips `build/`, `dist/` and `*.egg-info`, so stale local build copies of the package are no longer collected. - `encode_dataset(verbose=False)` now also suppresses the descriptor-list line printed for the first sequence. - `hydrophobic_moment` docstring now names the source of its default scale (Eisenberg, Schwarz, Komarony & Wall 1984). diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index 6712d18..57044f6 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -1542,7 +1542,7 @@ in the resonant recognition model of Cosic. Each residue is replaced by its scale value (default: electron-ion interaction potential, EIIP), the signal is mean-centred, and its power spectrum is -P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue +P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width bins (the last bin includes 0.5) and the mean power in each bin is returned; @@ -1602,7 +1602,7 @@ A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot threshold HST = -0.02 and containing no proline. Areas use trapezoidal integration of the AP with HST as the zero axis. -Outputs: +Outputs:: a3vSA mean a3v over the sequence nHS number of hot spots; NnHS = nHS / L x 100 AAT area of the AP above HST over the whole sequence; AATr = AAT / L @@ -1639,7 +1639,7 @@ Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.78 Calculate the charge-hydropathy descriptors of Uversky et al.: = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = |n+ - n-| / L (K/R positive, D/E negative) + = abs(n+ - n-) / L (K/R positive, D/E negative) HQ = 2.785 - - 1.151 The boundary = 2.785 - 1.151 separates natively unfolded from folded @@ -1660,13 +1660,13 @@ result = protpy.uversky_charge_hydropathy(protein_seq) #### `fold_index` (FI) -FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics. +FoldIndex profile summary: windowed 2.785 - abs() - 1.151 and disordered-residue statistics. ```text Calculate FoldIndex profile descriptors. For each window of length ``window``: -FI = 2.785 - || - 1.151 +FI = 2.785 - abs() - 1.151 with and as in ``uversky_charge_hydropathy`` but computed over the window. Residues covered by at least one window with FI < 0 are counted as @@ -1719,11 +1719,13 @@ hydropathy profile. Windows of length ``window`` with mean hydropathy above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative transmembrane segments; overlapping windows are merged into segments. -TM_n_segments number of merged segments -TM_max_KD maximum window hydropathy -TM_frac_in_TM fraction of residues inside a segment -TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment +Columns:: + + TM_n_segments number of merged segments + TM_max_KD maximum window hydropathy + TM_frac_in_TM fraction of residues inside a segment + TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment This is a hydropathy heuristic, not a topology predictor. ``` @@ -1747,9 +1749,11 @@ N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue ```text Calculate simple signal-peptide features of the N-terminus: -SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) -SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues +Columns:: + + SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) + SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues The von Heijne cleavage-site weight matrix is not included. ``` @@ -1799,11 +1803,13 @@ Calculate prion-like domain descriptors with the PLAAC model, ported from the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each residue has the log-likelihood ratio LLR = ln(f_prion / f_background). -PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues -PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse -PLAAC_PrD_length length of the prion-like run containing the best core -PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists +Columns:: + + PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues + PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse + PLAAC_PrD_length length of the prion-like run containing the best core + PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than ``core_length`` / no prion-like run of that length); PrD_length is then 0. diff --git a/docs/conf.py b/docs/conf.py index 606cbbc..c8b5d9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,3 +39,22 @@ # html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] + + +#protpy docstrings write parameters as ":name: type" under a "Parameters" heading; napoleon +#expects numpy's "name : type", so rewrite those lines before napoleon (priority 500) runs +import re as _re +_FIELD = _re.compile(r"^(\s*):(\w+):\s*(.*)$") + +def _house_style_to_numpy(app, what, name, obj, options, lines): + section = None + for i, line in enumerate(lines): + stripped = line.strip() + if stripped in ("Parameters", "Returns", "Raises") and i + 1 < len(lines) and set(lines[i + 1].strip()) <= {"=", "-"}: + section = stripped + match = _FIELD.match(line) + if section in ("Parameters", "Returns") and match: + lines[i] = f"{match.group(1)}{match.group(2)} : {match.group(3)}" + +def setup(app): + app.connect("autodoc-process-docstring", _house_style_to_numpy, priority=400) diff --git a/docs/descriptors.rst b/docs/descriptors.rst index ccfa1a8..b103192 100644 --- a/docs/descriptors.rst +++ b/docs/descriptors.rst @@ -1943,7 +1943,7 @@ Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) its scale value (default: electron-ion interaction potential, EIIP), the signal is mean-centred, and its power spectrum is - P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue + P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width bins (the last bin includes 0.5) and the mean power in each bin is returned; @@ -2001,7 +2001,7 @@ AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot threshold HST = -0.02 and containing no proline. Areas use trapezoidal integration of the AP with HST as the zero axis. - Outputs: + Outputs:: a3vSA mean a3v over the sequence nHS number of hot spots; NnHS = nHS / L x 100 AAT area of the AP above HST over the whole sequence; AATr = AAT / L @@ -2038,7 +2038,7 @@ Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.78 Calculate the charge-hydropathy descriptors of Uversky et al.: = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = |n+ - n-| / L (K/R positive, D/E negative) + = abs(n+ - n-) / L (K/R positive, D/E negative) HQ = 2.785 - - 1.151 The boundary = 2.785 - 1.151 separates natively unfolded from folded @@ -2058,14 +2058,14 @@ Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.78 fold_index (FI) — Structural ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics. +FoldIndex profile summary: windowed 2.785 - abs() - 1.151 and disordered-residue statistics. .. code-block:: text Calculate FoldIndex profile descriptors. For each window of length ``window``: - FI = 2.785 - || - 1.151 + FI = 2.785 - abs() - 1.151 with and as in ``uversky_charge_hydropathy`` but computed over the window. Residues covered by at least one window with FI < 0 are counted as @@ -2118,11 +2118,13 @@ Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative transmembrane segments; overlapping windows are merged into segments. - TM_n_segments number of merged segments - TM_max_KD maximum window hydropathy - TM_frac_in_TM fraction of residues inside a segment - TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment + Columns:: + + TM_n_segments number of merged segments + TM_max_KD maximum window hydropathy + TM_frac_in_TM fraction of residues inside a segment + TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment This is a hydropathy heuristic, not a topology predictor. @@ -2146,9 +2148,11 @@ N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue Calculate simple signal-peptide features of the N-terminus: - SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) - SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues + Columns:: + + SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) + SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues The von Heijne cleavage-site weight matrix is not included. @@ -2198,11 +2202,13 @@ PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and p the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each residue has the log-likelihood ratio LLR = ln(f_prion / f_background). - PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues - PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse - PLAAC_PrD_length length of the prion-like run containing the best core - PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + Columns:: + + PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues + PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse + PLAAC_PrD_length length of the prion-like run containing the best core + PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than ``core_length`` / no prion-like run of that length); PrD_length is then 0. diff --git a/docs/index.rst b/docs/index.rst index 3901fac..14080c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,6 +68,71 @@ Sequence Order Descriptors (5) * **Quasi Sequence Order (QSO)** * **Quasi Sequence Order — all matrices** +v1.5.0 Composition Descriptors (7) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **cksaap** (CKSAAP) +* **cksaagp** (CKSAAGP) +* **dde** (DDE) +* **asdc** (ASDC) +* **distance_pair** (DP) +* **natural_vector** (NV) +* **atomic_composition** (ATC) + +v1.5.0 Scales Descriptors (11) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **z_scales_5** (Z5) +* **vhse** (VHSE) +* **atchley_factors** (AF) +* **t_scales** (T) +* **st_scales** (ST) +* **protfp** (ProtFP) +* **fasgai** (FASGAI) +* **blosum_indices** (BLOSUM) +* **ms_whim** (MSWHIM) +* **auto_cross_covariance** (ACC) +* **physicochemical_distance_transformation** (PDT) + +v1.5.0 Physicochemical Descriptors (11) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **charge_fractions** (FCR) +* **ncpr** (NCPR) +* **kappa** (Kappa) +* **sequence_charge_decoration** (SCD) +* **charge_density** (ChargeDensity) +* **hydrophobicity_profile** (HP) +* **hydrophobic_ratio** (HydrophobicRatio) +* **half_life** (HalfLife) +* **ivywrel_index** (IVYWREL) +* **cvp_bias** (CvP) +* **flexibility** (Flex) + +v1.5.0 Complexity Descriptors (3) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **wootton_federhen_complexity** (WF) +* **linguistic_complexity** (LC) +* **lempel_ziv_complexity** (LZ) + +v1.5.0 Signal Descriptors (2) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **fft_descriptors** (FFT) +* **dct_descriptors** (DCT) + +v1.5.0 Structural Descriptors (10) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **aggrescan** (AGG) +* **uversky_charge_hydropathy** (UV) +* **fold_index** (FI) +* **disorder_propensity** (DP) +* **transmembrane_features** (TM) +* **signal_peptide_features** (SP) +* **functional_site_counts** (FS) +* **plaac** (PLAAC) +* **antigenicity_surface** (Antigenicity) +* **cysteine_features** (Cys) + +The v1.5.0 descriptors are added to ``all_descriptors`` with ``descriptor_set="extended"`` or ``"all"``; +see :doc:`usage` and :doc:`descriptors`. + .. note:: A demo of the software is available `on Colab `_ diff --git a/protpy/composition.py b/protpy/composition.py index 68be4e6..73eecac 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -1557,7 +1557,7 @@ def amphiphilic_pseudo_amino_acid_composition(sequence: str, lamda: int = 30, we rank of correlation. Number of calculable descriptors depends on lambda value. :weight: float weighting factor. - :properties: list[dict] | None (default=None → [hydrophobicity_, hydrophilicity_]) + :properties: list[dict] | None (default=None → [``hydrophobicity_``, ``hydrophilicity_``]) list of property dicts mapping each amino acid to a float value. When None the default hydrophobicity and hydrophilicity scales are used. @@ -1629,7 +1629,7 @@ def amphiphilic_sequence_order_correlation_factor(sequence: str, k: int = 1, protein sequence. :k: int (default=1) gap between amino acids in the sequence. - :properties: list[dict] | None (default=None → [hydrophobicity_, hydrophilicity_]) + :properties: list[dict] | None (default=None → [``hydrophobicity_``, ``hydrophilicity_``]) list of property dicts. Pre-normalized once per call. Returns diff --git a/protpy/data/README.md b/protpy/data/README.md index 0192133..b0ae0ae 100644 --- a/protpy/data/README.md +++ b/protpy/data/README.md @@ -1,8 +1,15 @@ -# Data required for protPy sequence order descriptors +# Data files used by protpy descriptors * `grantham-physicochemical-distance-matrix.json` - grantham distance score for each amino acid combination, used in the calculation of the sequence-order related protein descriptors [[1]](#references). * `schneider-werde-physicochemical-distance-matrix.json` - schneider-werde distance score for each amino acid combination, used in the calculation of the sequence-order related protein descriptors [[2]](#references). +Added in v1.5.0. Each file maps a table name to `{"name", "values", "reference", "doi", "source_note"}`; `source_note` records where and when every table was transcribed. + +* `aa_scales.json` - single-value hydrophobicity/property scales (Kyte-Doolittle, Hopp-Woods, Eisenberg, Wimley-White, Engelman, DomCut, EIIP, Vihinen flexibility, Parker, Janin) and the factor-scale families (z-scales 5, VHSE, Atchley, T-, ST-scales, ProtFP, FASGAI, BLOSUM indices, MS-WHIM). +* `reduced_alphabets.json` - iFeature 5-group alphabet, cp13/cp14/cp19/cp20 cluster profiles and the six extra iFeature CTD partitions. +* `residue_tables.json` - sense-codon counts, free amino-acid formulas, pKa sets (protpy, EMBOSS, Bjellqvist, IPC_protein) and the N-end rule half-life table. +* `structural_params.json` - AGGRESCAN a3v, TOP-IDP, order/disorder-promoting residues and the PLAAC model parameters. + References ---------- \[1\]: Grantham R. Amino acid difference formula to help explain protein evolution. Science. 1974 Sep 6;185(4154):862-4. doi: 10.1126/science.185.4154.862. PMID: 4843792.
diff --git a/protpy/signal.py b/protpy/signal.py index a7e2aea..ca2c0bc 100644 --- a/protpy/signal.py +++ b/protpy/signal.py @@ -45,7 +45,7 @@ def fft_descriptors(sequence: str, scale: str | dict = "eiip", n_bins: int = 10) its scale value (default: electron-ion interaction potential, EIIP), the signal is mean-centred, and its power spectrum is - P(f) = |FFT(x)(f)|^2 / L, f = 0 .. 0.5 cycles/residue + P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width bins (the last bin includes 0.5) and the mean power in each bin is returned; diff --git a/protpy/structural.py b/protpy/structural.py index ab97afc..9b085be 100644 --- a/protpy/structural.py +++ b/protpy/structural.py @@ -93,7 +93,7 @@ def aggrescan(sequence: str, window: int = 5) -> pd.DataFrame: threshold HST = -0.02 and containing no proline. Areas use trapezoidal integration of the AP with HST as the zero axis. - Outputs: + Outputs:: a3vSA mean a3v over the sequence nHS number of hot spots; NnHS = nHS / L x 100 AAT area of the AP above HST over the whole sequence; AATr = AAT / L @@ -162,7 +162,7 @@ def uversky_charge_hydropathy(sequence: str) -> pd.DataFrame: Calculate the charge-hydropathy descriptors of Uversky et al. [2]: = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = |n+ - n-| / L (K/R positive, D/E negative) + = abs(n+ - n-) / L (K/R positive, D/E negative) HQ = 2.785 - - 1.151 The boundary = 2.785 - 1.151 separates natively unfolded from folded @@ -192,7 +192,7 @@ def fold_index(sequence: str, window: int = 51) -> pd.DataFrame: Calculate FoldIndex profile descriptors [3]. For each window of length ``window``: - FI = 2.785 - || - 1.151 + FI = 2.785 - abs() - 1.151 with and as in ``uversky_charge_hydropathy`` but computed over the window. Residues covered by at least one window with FI < 0 are counted as @@ -266,11 +266,13 @@ def transmembrane_features(sequence: str, window: int = 19, kd_threshold: float above ``kd_threshold`` (1.6 for 19-residue windows in [6]) mark putative transmembrane segments; overlapping windows are merged into segments. - TM_n_segments number of merged segments - TM_max_KD maximum window hydropathy - TM_frac_in_TM fraction of residues inside a segment - TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment + Columns:: + + TM_n_segments number of merged segments + TM_max_KD maximum window hydropathy + TM_frac_in_TM fraction of residues inside a segment + TM_positive_inside_bias (K+R in the 15 residues before the first segment) + - (K+R in the 15 residues after it); 0 if no segment This is a hydropathy heuristic, not a topology predictor. @@ -311,9 +313,11 @@ def signal_peptide_features(sequence: str, n_term: int = 30) -> pd.DataFrame: """ Calculate simple signal-peptide features of the N-terminus [8]: - SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) - SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues + Columns:: + + SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) + SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue + windows within the first ``n_term`` residues The von Heijne cleavage-site weight matrix is not included. @@ -420,11 +424,13 @@ def plaac(sequence: str, core_length: int = 60) -> pd.DataFrame: the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each residue has the log-likelihood ratio LLR = ln(f_prion / f_background). - PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues - PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse - PLAAC_PrD_length length of the prion-like run containing the best core - PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + Columns:: + + PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues + PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a + prion-like run of the two-state HMM Viterbi parse + PLAAC_PrD_length length of the prion-like run containing the best core + PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than ``core_length`` / no prion-like run of that length); PrD_length is then 0. diff --git a/protpy/utils.py b/protpy/utils.py index 489618f..8957d51 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -1268,7 +1268,7 @@ "in_default_set": False, }, "fold_index": { - "description": "FoldIndex profile summary: windowed 2.785 - || - 1.151 and disordered-residue statistics.", + "description": "FoldIndex profile summary: windowed 2.785 - abs() - 1.151 and disordered-residue statistics.", "module": "structural", "category": "Structural", "abbreviation": "FI", diff --git a/pyproject.toml b/pyproject.toml index a1b6e35..5bbc974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,4 +82,5 @@ where = ["."] "protpy.data" = ["*.json"] [tool.pytest.ini_options] -norecursedirs = ["docs/_build"] +testpaths = ["tests"] +norecursedirs = ["docs/_build", "build", "dist", "*.egg-info"] diff --git a/tests/README.md b/tests/README.md index dff9d87..a681ed8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,16 +7,29 @@ All tests in the project were ran using Python's unittest testing framework (htt python3 -m unittest discover tests ``` -You can add the flag *-b* to suppress some of the verbose output when running the unittests. +You can add the flag *-b* to suppress some of the verbose output when running the unittests. The CI workflow runs the same tests with `python -m pytest`. Unit tests ---------- -* `test_protpy.py` - unit tests for overall `protpy` package. -* `test_autocorrellation.py` - unit tests for autocorrelation module and functionality. +* `test_protpy.py` - unit tests for overall `protpy` package (version, exported descriptor names). +* `test_autocorrelation.py` - unit tests for autocorrelation module and functionality. * `test_composition.py` - unit tests for composition module and functionality, including pseudo composition and amphiphillic composition. +* `test_composition_extended.py` - v1.5.0 composition descriptors (CKSAAP, CKSAAGP, DDE, ASDC, distance pair, natural vector, atomic composition) and the GAAC/GDPC/GTPC, KSCTriad, 13-property CTD and split-composition extensions. * `test_conjoint_triad.py` - unit tests for conjoint triad module and functionality. * `test_ctd.py` - unit tests for CTD module and functionality. * `test_sequence_order.py` - unit tests for sequence order module and functionality. +* `test_scales.py` - factor scales, auto/cross covariance and PDT (optional cross-check against `peptides`, skipped if not installed). +* `test_physicochemical.py` - charge patterning (including the published Das & Pappu kappa values), pKa scales, hydrophobicity profile, half-life, flexibility and the GRAVY / hydrophobic moment / extinction coefficient extensions. +* `test_complexity.py` - Wootton-Federhen, linguistic and Lempel-Ziv complexity. +* `test_signal.py` - FFT and DCT descriptors. +* `test_structural.py` - AGGRESCAN, Uversky/FoldIndex, disorder propensity, TM and signal-peptide features, functional sites, PLAAC (against PLAAC's reference output), antigenicity and cysteine features. +* `test_encode.py` - `all_descriptors`, `encode_dataset` and `read_sequences_from_file`. +* `test_descriptor_sets.py` - `descriptor_set`, `include` and `exclude`, and every v1.5.0 descriptor on 1-8 residue sequences. +* `test_utils.py` - descriptor registry and metadata functions. +* `test_helpers.py` - internal helpers, data-table provenance, registry parameters matching function signatures, and registry DOIs present in `docs/references.bib`. +* `test_regressions.py` - regression tests for bugs fixed in the post-1.4.2 audit. +* `test_backwards_compat.py` - golden snapshot: every v1.4.2 default output must be unchanged. Regenerate only when deliberately re-baselining: `python tests/test_backwards_compat.py --regen`. +* `test_performance.py` - timing test for `descriptor_set="all"` on a 500-residue sequence; skipped unless `PROTPY_PERF=1` is set. Test Files ---------- @@ -24,10 +37,14 @@ Test Files * `test_data/test_fasta2.fasta` - Nucleoprotein (P59595 · NCAP_SARS) [[2]](#references). * `test_data/test_fasta3.fasta` - Envelope small membrane protein (P59637 · VEMP_SARS) [[3]](#references). * `test_data/test_fasta4.fasta` - Nucleoprotein (P0DTC9 · NCAP_SARS2) [[4]](#references). +* `test_data/structural_reference.fasta` - MOT3 (from the MIT-licensed PLAAC repository example), glycophorin A (P02724) [[5]](#references) and amyloid-β 1-42 (P05067 residues 672-713) [[6]](#references), used by `test_structural.py`. +* `test_data/golden_v1_4_2.json.gz` - golden outputs used by `test_backwards_compat.py`. References ---------- \[1\]: https://www.uniprot.org/uniprotkb/P59594/entry
\[2\]: https://www.uniprot.org/uniprotkb/P59595/entry
\[3\]: https://www.uniprot.org/uniprotkb/P59637/entry
-\[4\]: https://www.uniprot.org/uniprotkb/P0DTC9/entry \ No newline at end of file +\[4\]: https://www.uniprot.org/uniprotkb/P0DTC9/entry
+\[5\]: https://www.uniprot.org/uniprotkb/P02724/entry
+\[6\]: https://www.uniprot.org/uniprotkb/P05067/entry \ No newline at end of file diff --git a/tests/test_descriptor_sets.py b/tests/test_descriptor_sets.py index 186453b..a0a13d7 100644 --- a/tests/test_descriptor_sets.py +++ b/tests/test_descriptor_sets.py @@ -68,6 +68,23 @@ def test_short_sequence_extended(self): #only documented NaNs: PLAAC LLR/COREscore for sequences shorter than the core length self.assertEqual(sorted(df.columns[df.isnull().any()]), ["PLAAC_COREscore", "PLAAC_LLR"]) + def test_new_descriptors_on_very_short_sequences(self): + #every v1.5 descriptor either works or raises its documented ValueError on tiny inputs + documented = {"dde": "at least 2 residues", "auto_cross_covariance": "lag", + "physicochemical_distance_transformation": "lag", "dct_descriptors": "n_coeffs"} + for name, meta in _DESCRIPTOR_REGISTRY.items(): + if meta["in_default_set"]: + continue + for seq in ("A", "AC", "MKTAYIAK"): + with self.subTest(name=name, seq=seq): + try: + df = getattr(protpy, name)(seq) + except ValueError as e: + self.assertIn(name, documented) + self.assertIn(documented[name], str(e)) + continue + self.assertEqual(df.shape[0], 1) + def test_encode_dataset_passthrough(self): with tempfile.TemporaryDirectory() as tmp: out = os.path.join(tmp, "enc.csv") diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 4bca79b..8111868 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -96,6 +96,12 @@ def test_every_descriptor_registered_with_reference(self): self.assertIn("doi", meta) self.assertIn("in_default_set", meta) + def test_registry_parameters_match_signatures(self): + import inspect + for name, meta in _DESCRIPTOR_REGISTRY.items(): + with self.subTest(name=name): + self.assertEqual(list(inspect.signature(getattr(protpy, name)).parameters), list(meta["parameters"])) + def test_registry_dois_in_bib(self): with open(os.path.join(ROOT, "docs", "references.bib")) as f: bib = {d.lower() for d in re.findall(r"doi\s*=\s*\{([^}]*)\}", f.read(), flags=re.I)} From 9cabde4f1048924ea1d5d2d1a0d079af63b7e452 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:49:59 +0100 Subject: [PATCH 14/17] Add opt-in normalized=True ([0, 1] output with _norm columns) to scalar, autocorrelation, CTD and sequence-order descriptors --- CHANGELOG.md | 7 ++ DESCRIPTORS.md | 36 ++++++++ README.md | 2 + docs/usage.rst | 64 +++++++++++++++ protpy/_helpers.py | 28 +++++++ protpy/autocorrelation.py | 57 +++++++++++-- protpy/composition.py | 106 ++++++++++++++++++++---- protpy/ctd.py | 40 ++++++--- protpy/encode.py | 39 +++++++-- protpy/sequence_order.py | 47 ++++++++--- protpy/utils.py | 30 ++++++- tests/README.md | 1 + tests/test_encode.py | 2 +- tests/test_normalization.py | 159 ++++++++++++++++++++++++++++++++++++ 14 files changed, 563 insertions(+), 55 deletions(-) create mode 100644 tests/test_normalization.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dfefcf0..a46d67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Structural (`protpy/structural.py`): `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac` (port of the MIT-licensed PLAAC; reproduces its reference output), `antigenicity_surface`, `cysteine_features`. - **Extensions to existing functions** (defaults unchanged): `reduced_alphabet_composition(alphabet=, k=)` (GAAC/GDPC/GTPC), `conjoint_triad(gap=)` (KSCTriad), `ctd_(property_set="ifeature13")`, `amino_acid_composition`/`dipeptide_composition(region=, n=)` (split composition), `isoelectric_point`/`charge_distribution(pka_scale=)` ("emboss", "bjellqvist", "ipc_protein"), `gravy(scale=)`, `hydrophobic_moment(scale=, angle="helix"/"sheet")`, `extinction_coefficient(absorbance=True)`. - **`descriptor_set`, `include`, `exclude`** for `all_descriptors` and `encode_dataset`: `"classic"` (default, unchanged), `"extended"` (adds the new descriptors with ≤150 columns) and `"all"`. Set membership comes from the registry's new `descriptor_sets` field. Every parameterised v1.5.0 descriptor is accepted in `params`. +- **`normalized=False` option** (opt-in; addresses the GitHub request for [0, 1] features) on `gravy`, `instability_index`, `isoelectric_point`, `molecular_weight`, `charge_distribution`, `secondary_structure_propensity`, `aliphatic_index`, `extinction_coefficient`, `boman_index`, `shannon_entropy`, `hydrophobic_moment`, `pseudo_amino_acid_composition`, the three autocorrelation descriptors, `ctd_composition`/`ctd_transition`/`ctd_distribution`/`ctd_`, the SOCN functions and `quasi_sequence_order`/`quasi_sequence_order_all`. + - Outputs are rescaled with each descriptor's theoretical bounds (see DESCRIPTORS.md), and every column gets a `_norm` suffix so raw and normalised values can't be mixed in one CSV. + - Several bounds differ from the ones originally proposed, which would not have kept values in [0, 1]: the hydrophobic moment uses the scale's max abs(H); the charge bounds use L·f(pKa) rather than pKa·L; SOCN uses (N−d)·max(distance)²; Moran uses ±N/(N−d); Geary uses 2(N−1)/(N−d); QSO's composition part is divided by 100. + - `amphiphilic_pseudo_amino_acid_composition` has no such option because its outputs have no fixed bound. + - The existing autocorrelation `normalize` parameter (property standardisation) is unchanged. + - `all_descriptors` now forwards `params` to `gravy`, `instability_index`, `isoelectric_point`, `molecular_weight`, `secondary_structure_propensity`, `aliphatic_index`, `extinction_coefficient`, `boman_index` and `shannon_entropy`. - **Data files** with provenance (`name`, `values`, `reference`, `doi`, `source_note`): `protpy/data/aa_scales.json`, `reduced_alphabets.json`, `residue_tables.json`, `structural_params.json`. - Internal helpers in `protpy/_helpers.py`. The registry gains `doi`, `in_default_set` and `descriptor_sets`, and `print_descriptor_info` prints the DOI. New registry categories: Scales, Physicochemical, Complexity, Signal, Structural. - `docs/references.bib` with BibTeX for every registry DOI. @@ -28,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Documentation - `docs/conf.py` converts protpy's `:name: type` parameter style to numpy style before napoleon runs. Parameter names, types and defaults now render correctly throughout the API docs; previously napoleon emitted nameless `:param :` fields. The Sphinx build is now warning-free (previously 39 warnings). - `docs/index.rst` lists the v1.5.0 descriptor categories; `docs/api.rst` documents the new modules and `encode_dataset`; `docs/usage.rst` covers `descriptor_set` / `include` / `exclude`; `docs/descriptors.rst` and `DESCRIPTORS.md` have a v1.5.0 section generated from the registry and docstrings. +- The autocorrelation `normalize` parameter's docs now say what it does: it standardises the property values (it never rescaled the output to 0–1). - `tests/README.md` lists every test module and test data file; `protpy/data/README.md` describes the new data files. ### Changed diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index 57044f6..a1f178a 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -702,6 +702,42 @@ result = protpy.quasi_sequence_order_all(protein_seq, lag=15, weight=0.05) --- +## Normalised output (`normalized=True`) + +The descriptors below take an opt-in `normalized=False` parameter. With `normalized=True` every output is rescaled to [0, 1] using the descriptor's theoretical bounds, not the bounds of any particular dataset, and every column gets a `_norm` suffix (e.g. `GRAVY_norm`). Raw and normalised outputs therefore never share a column name, and `encode_dataset` refuses to append one to a file holding the other. Default outputs are unchanged. + +```python +protpy.gravy(seq, normalized=True) # GRAVY_norm +protpy.ctd_(seq, normalized=True) # CTD_..._norm +protpy.all_descriptors(seq, params={"gravy": {"normalized": True}, "moran_autocorrelation": {"normalized": True}}) +``` + +| Descriptor | Rescaling | +|---|---| +| `gravy` | between the scale's min and max residue value (GRAVY is a mean) | +| `instability_index` | [10·min(DIWV, 0), 10·max(DIWV, 0)] | +| `isoelectric_point` | pI / 14 (the searched pH range) | +| `molecular_weight` | between the all-Gly and all-Trp sequences of the same length, i.e. (mean residue mass − min) / (max − min) | +| `charge_distribution` | positive / (L·f₊), negative / (L·f₋), net mapped from [−L·f₋, L·f₊]; f₊, f₋ are the charges of the most basic (highest pKa) and most acidic (lowest pKa) residue at that pH and pKa set | +| `secondary_structure_propensity` | each column between its table's min and max propensity | +| `aliphatic_index` | / 390 (all-Ile/Leu) | +| `extinction_coefficient` | coefficients / (5500·L) (all-Trp); absorbances / (5500 / (204.228 − 18.015)) | +| `boman_index` | between −max and −min of the solubility scale | +| `shannon_entropy` | / log₂20 | +| `hydrophobic_moment` | / max abs(H) of the scale (a window moment cannot exceed it) | +| `pseudo_amino_acid_composition` | / 100 (non-negative percentages summing to 100) | +| `moreaubroto_autocorrelation` | between min and max of P(a)·P(b) over all residue pairs (including 0 for lags without pairs) | +| `moran_autocorrelation` | from [−N/(N−d), N/(N−d)] (Cauchy–Schwarz); 0.5 = no autocorrelation | +| `geary_autocorrelation` | / (2(N−1)/(N−d)) | +| `ctd_composition`, `ctd_transition` | already fractions in [0, 1]; unchanged apart from the `_norm` suffix | +| `ctd_distribution`, `ctd_` | distribution / 100; composition and transition unchanged | +| `sequence_order_coupling_number_` (float), `sequence_order_coupling_number`, `sequence_order_coupling_number_all` | τ_d / ((N−d)·max(distance)²) | +| `quasi_sequence_order`, `quasi_sequence_order_all` | first 20 components / 100 (percentages over 1 + w·Στ ≥ 1); lag components unchanged (always < 1) | + +`amphiphilic_pseudo_amino_acid_composition` has no `normalized` option: its shared denominator 1 + w·Στ uses correlation factors that can be negative, so its outputs have no fixed bound. Bounds based on theory keep features comparable across datasets; to scale to the range actually seen in a training set, fit a min–max scaler (e.g. scikit-learn's `MinMaxScaler`) on that set instead. + +--- + ## v1.5.0 Descriptors Descriptors added in v1.5.0. Each lists its formula, output shape, parameters and primary reference. diff --git a/README.md b/README.md index e792a5f..851e670 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ The descriptors available in `protpy` include: df = protpy.all_descriptors(protein_seq, descriptor_set="extended") ``` +Most scalar, autocorrelation, CTD and sequence-order descriptors also take `normalized=True`, which rescales their output to [0, 1] with the descriptor's theoretical bounds and adds a `_norm` column suffix (see [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md#normalised-output-normalizedtrue)). + More detail of each descriptor is listed in the markdown file: [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md) diff --git a/docs/usage.rst b/docs/usage.rst index f75df39..40d935a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -410,6 +410,70 @@ Calculate **Quasi Sequence Order (QSO)**: # QSO_Grant1 QSO_Grant2 QSO_Grant3 ... # 0.123287 0.079967 0.04332 ... +Normalised Output +----------------- + +The descriptors in the table below take an opt-in ``normalized=False`` parameter. With ``normalized=True`` +every output is rescaled to [0, 1] using the descriptor's theoretical bounds and every column +gets a ``_norm`` suffix, so raw and normalised values never share a column name. Default outputs +are unchanged. + +.. code-block:: python + + protpy.gravy(protein_seq, normalized=True) # GRAVY_norm + protpy.all_descriptors(protein_seq, params={"gravy": {"normalized": True}, + "ctd_": {"normalized": True}}) + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Descriptor + - Rescaling + * - ``gravy`` + - between the scale's min and max residue value (GRAVY is a mean) + * - ``instability_index`` + - [10·min(DIWV, 0), 10·max(DIWV, 0)] + * - ``isoelectric_point`` + - pI / 14 (the searched pH range) + * - ``molecular_weight`` + - between the all-Gly and all-Trp sequences of the same length, i.e. (mean residue mass − min) / (max − min) + * - ``charge_distribution`` + - positive / (L·f₊), negative / (L·f₋), net mapped from [−L·f₋, L·f₊]; f₊, f₋ are the charges of the most basic (highest pKa) and most acidic (lowest pKa) residue at that pH and pKa set + * - ``secondary_structure_propensity`` + - each column between its table's min and max propensity + * - ``aliphatic_index`` + - / 390 (all-Ile/Leu) + * - ``extinction_coefficient`` + - coefficients / (5500·L) (all-Trp); absorbances / (5500 / (204.228 − 18.015)) + * - ``boman_index`` + - between −max and −min of the solubility scale + * - ``shannon_entropy`` + - / log₂20 + * - ``hydrophobic_moment`` + - / max abs(H) of the scale (a window moment cannot exceed it) + * - ``pseudo_amino_acid_composition`` + - / 100 (non-negative percentages summing to 100) + * - ``moreaubroto_autocorrelation`` + - between min and max of P(a)·P(b) over all residue pairs (including 0 for lags without pairs) + * - ``moran_autocorrelation`` + - from [−N/(N−d), N/(N−d)] (Cauchy–Schwarz); 0.5 = no autocorrelation + * - ``geary_autocorrelation`` + - / (2(N−1)/(N−d)) + * - ``ctd_composition``, ``ctd_transition`` + - already fractions in [0, 1]; unchanged apart from the ``_norm`` suffix + * - ``ctd_distribution``, ``ctd_`` + - distribution / 100; composition and transition unchanged + * - ``sequence_order_coupling_number_`` (float), ``sequence_order_coupling_number``, ``sequence_order_coupling_number_all`` + - τ_d / ((N−d)·max(distance)²) + * - ``quasi_sequence_order``, ``quasi_sequence_order_all`` + - first 20 components / 100 (percentages over 1 + w·Στ ≥ 1); lag components unchanged (always < 1) + +``amphiphilic_pseudo_amino_acid_composition`` has no ``normalized`` option: its denominator +1 + w·Στ can approach zero or become negative, so its outputs have no fixed bound. To scale features +to the range seen in a training set instead, fit a min-max scaler (e.g. scikit-learn's +``MinMaxScaler``) on that set. + Encoding All Descriptors at Once --------------------------------- diff --git a/protpy/_helpers.py b/protpy/_helpers.py index ef9cf46..0bab9eb 100644 --- a/protpy/_helpers.py +++ b/protpy/_helpers.py @@ -151,3 +151,31 @@ def _to_df(values: list, columns: list[str]) -> pd.DataFrame: values = [round(float(v), 3) if isinstance(v, (float, np.floating)) else int(v) if isinstance(v, (bool, np.bool_, np.integer)) else v for v in values] return pd.DataFrame([values], columns=columns) + + +def _minmax(df: pd.DataFrame, lo, hi) -> pd.DataFrame: + """ + Rescale a 1 x N descriptor DataFrame to [0, 1] with the descriptor's theoretical bounds + (scalars or per-column arrays) and suffix every column with ``_norm`` so normalised and + raw outputs can never share a column. Values are clipped to [0, 1] to absorb the 3 dp + rounding of the raw values, and rounded to 6 dp. + """ + lo = np.broadcast_to(np.asarray(lo, dtype=float), (df.shape[1],)) + hi = np.broadcast_to(np.asarray(hi, dtype=float), (df.shape[1],)) + span = hi - lo + raw = df.iloc[0].to_numpy(dtype=float) + scaled = np.divide(raw - lo, span, out=np.zeros_like(raw), where=span > 0) + return pd.DataFrame([np.round(_unit_clip(scaled), 6)], columns=[f"{c}_norm" for c in df.columns]) + + +def _unit_clip(values: np.ndarray) -> np.ndarray: + """Clip to [0, 1]; only ever absorbs the rounding of raw values (tests disable it to check the bounds).""" + return np.clip(values, 0.0, 1.0) + + +def _scale_range(scale: str | dict) -> tuple[float, float]: + """Minimum and maximum residue value of a single-value scale over the 20 amino acids.""" + values = _property_matrix("".join(amino_acids), scale) + if values.shape[1] != 1: + raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") + return float(values.min()), float(values.max()) diff --git a/protpy/autocorrelation.py b/protpy/autocorrelation.py index e959aa1..53d06e2 100644 --- a/protpy/autocorrelation.py +++ b/protpy/autocorrelation.py @@ -25,12 +25,14 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int +from ._helpers import _minmax ############################### MoreauBroto Autocorrelation ############################### def moreaubroto_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, + normalized: bool = False) -> pd.DataFrame: """ Calculate MoreauBrotoAuto Autocorrelation (MBAuto) descriptor for sequence. Autocorrelation descriptors are a class of topological descriptors, @@ -68,7 +70,16 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the 8 standard AAIndex properties listed above. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output; use + ``normalized`` for [0, 1] output. + :normalized: bool (default=False) + rescale every value to [0, 1] with its theoretical bounds (see below); + columns get a ``_norm`` suffix. Independent of ``normalize``, which + standardises the property values before the calculation. + Bounds: the minimum and maximum product P(a) x P(b) over all amino-acid + pairs (extended to include 0, the value of lags with no residue pairs), + since each value is a mean of such products. Returns ======= @@ -106,13 +117,22 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, moreaubroto_autocorrelation_df = pd.DataFrame([list(moreaubroto_autocorrelation.values())], columns=list(moreaubroto_autocorrelation.keys())) + if normalized: + lo, hi = [], [] + for key in aai_properties: + products = [aai_properties[key][a] * aai_properties[key][b] for a in amino_acids for b in amino_acids] + #include 0: lags with no residue pairs are reported as 0 + lo += [min(min(products), 0.0)] * lag + hi += [max(max(products), 0.0)] * lag + return _minmax(moreaubroto_autocorrelation_df, lo, hi) return moreaubroto_autocorrelation_df ################################## Moran Autocorrelation ################################## def moran_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, + normalized: bool = False) -> pd.DataFrame: """ Refer to MBAuto docstring for autocorrelation explanation. Moran autocorrelation (MAuto) utilizes property deviations from the @@ -127,7 +147,15 @@ def moran_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output; use + ``normalized`` for [0, 1] output. + :normalized: bool (default=False) + rescale every value to [0, 1] with its theoretical bounds (see below); + columns get a ``_norm`` suffix. Independent of ``normalize``, which + standardises the property values before the calculation. + Bounds: abs(I(d)) <= N / (N - d) by the Cauchy-Schwarz inequality, so I is + mapped from [-N/(N-d), N/(N-d)]; 0.5 means no autocorrelation. Returns ======= @@ -174,13 +202,18 @@ def moran_autocorrelation(sequence: str, lag: int = 30, moran_autocorrelation_df = pd.DataFrame([list(moran_autocorrelation.values())], columns=list(moran_autocorrelation.keys())) + if normalized: + N = len(sequence) + bound = [N / (N - i) if N > i else 1.0 for i in range(1, lag + 1)] * len(aai_properties) + return _minmax(moran_autocorrelation_df, [-b for b in bound], bound) return moran_autocorrelation_df ################################## Geary Autocorrelation ################################## def geary_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, + normalized: bool = False) -> pd.DataFrame: """ Refer to MBAuto docstring for autocorrelation description. Geary Autocorrelation (GAuto) utilizes the square-difference of property @@ -195,7 +228,15 @@ def geary_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output; use + ``normalized`` for [0, 1] output. + :normalized: bool (default=False) + rescale every value to [0, 1] with its theoretical bounds (see below); + columns get a ``_norm`` suffix. Independent of ``normalize``, which + standardises the property values before the calculation. + Bounds: 0 <= C(d) <= 2(N - 1) / (N - d), since each squared difference is at + most twice the sum of the two squared deviations. Returns ======= @@ -238,6 +279,10 @@ def geary_autocorrelation(sequence: str, lag: int = 30, geary_autocorrelation_df = pd.DataFrame([list(geary_autocorrelation.values())], columns=list(geary_autocorrelation.keys())) + if normalized: + N = len(sequence) + bound = [2 * (N - 1) / (N - i) if N > i else 1.0 for i in range(1, lag + 1)] * len(aai_properties) + return _minmax(geary_autocorrelation_df, 0.0, bound) return geary_autocorrelation_df def _get_properties(properties: list[str] | str, normalize: bool) -> dict[str, dict[str, float]]: diff --git a/protpy/composition.py b/protpy/composition.py index 73eecac..2d4eb49 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -98,7 +98,8 @@ class prediction: new methods of statistical classification. Proc Int Conf """ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight -from ._helpers import _load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df, _property_matrix +from ._helpers import (_load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df, _property_matrix, + _minmax, _scale_range) #physicochemical values for all amino acids for hydrophobicity, hydrophilicty and residue mass #values taken from http://www.csbio.sjtu.edu.cn/bioinf/PseAAC/ParaValue.htm @@ -319,7 +320,7 @@ def tripeptide_composition(sequence: str) -> pd.DataFrame: "W": -0.9, "Y": -1.3 } -def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: +def gravy(sequence: str, scale: str = "kyte_doolittle", normalized: bool = False) -> pd.DataFrame: """ Calculate the Grand Average of Hydropathy (GRAVY) for a protein sequence. GRAVY is the sum of the Kyte-Doolittle hydropathy values of all residues @@ -340,6 +341,9 @@ def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: protein sequence. :scale: str (default="kyte_doolittle") hydrophobicity scale to average. + :normalized: bool (default=False) + rescale to [0, 1] with the scale's minimum and maximum residue values + (GRAVY is a mean, so it lies between them); columns get a ``_norm`` suffix. Returns ======= @@ -354,7 +358,8 @@ def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: values = _property_matrix(sequence, scale) if values.shape[1] != 1: raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") - return _to_df([values.mean()], [f"GRAVY_{scale}"]) + df = _to_df([values.mean()], [f"GRAVY_{scale}"]) + return _minmax(df, *_scale_range(scale)) if normalized else df #sum Kyte-Doolittle hydropathy values and divide by sequence length gravy_score = round(sum(kyte_doolittle_[aa] for aa in sequence) / len(sequence), 3) @@ -362,6 +367,8 @@ def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: #transform to DataFrame gravy_df = pd.DataFrame([[gravy_score]], columns=["GRAVY"]) + if normalized: + return _minmax(gravy_df, min(kyte_doolittle_.values()), max(kyte_doolittle_.values())) return gravy_df ################################## Aromaticity ############################################ @@ -465,7 +472,7 @@ def aromaticity(sequence: str) -> pd.DataFrame: "EP": 20.26,"EA": 1.0, "EI": 1.0, "EL": 1.0, "EG": 1.0, "EE": 20.26 } -def instability_index(sequence: str) -> pd.DataFrame: +def instability_index(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate the Instability Index (II) of a protein sequence. The instability index is a measure of in vivo stability, computed as a weighted sum of @@ -480,6 +487,9 @@ def instability_index(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale to [0, 1] with the bounds 10 x min(DIWV, 0) and 10 x max(DIWV, 0) + (II = 10 x sum of L - 1 dipeptide weights / L); column ``InstabilityIndex_norm``. Returns ======= @@ -496,6 +506,9 @@ def instability_index(sequence: str) -> pd.DataFrame: #transform to DataFrame instability_index_df = pd.DataFrame([[ii]], columns=["InstabilityIndex"]) + if normalized: + weights = list(diwv_.values()) + [1.0] + return _minmax(instability_index_df, 10 * min(min(weights), 0), 10 * max(max(weights), 0)) return instability_index_df ################################# Isoelectric Point ###################################### @@ -513,7 +526,7 @@ def _pka_set(pka_scale: str) -> dict: raise ValueError(f"Invalid pka_scale '{pka_scale}'. Valid scales are: {list(sets)}.") return sets[pka_scale] -def isoelectric_point(sequence: str, pka_scale: str = "protpy") -> pd.DataFrame: +def isoelectric_point(sequence: str, pka_scale: str = "protpy", normalized: bool = False) -> pd.DataFrame: """ Calculate the theoretical Isoelectric Point (pI) of a protein sequence. pI is the pH at which the net charge of the protein is zero. It is computed @@ -532,6 +545,9 @@ def isoelectric_point(sequence: str, pka_scale: str = "protpy") -> pd.DataFrame: protein sequence. :pka_scale: str (default="protpy") pKa value set: "protpy", "emboss", "bjellqvist" or "ipc_protein". + :normalized: bool (default=False) + rescale the pI from the searched pH range 0-14 to [0, 1] (pI / 14); + column ``IsoelectricPoint_norm``. Returns ======= @@ -577,6 +593,8 @@ def _net_charge(ph: float) -> float: #transform to DataFrame isoelectric_point_df = pd.DataFrame([[round(ph, 3)]], columns=["IsoelectricPoint"]) + if normalized: + return _minmax(isoelectric_point_df, 0.0, 14.0) return isoelectric_point_df ############################### Molecular Weight ########################################## @@ -589,7 +607,7 @@ def _net_charge(ph: float) -> float: "S": 105.093, "T": 119.119, "V": 117.148, "W": 204.228, "Y": 181.191 } -def molecular_weight(sequence: str) -> pd.DataFrame: +def molecular_weight(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate the Molecular Weight (MW) of a protein sequence. MW is computed as the sum of average residue masses minus one water molecule per peptide @@ -601,6 +619,11 @@ def molecular_weight(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale to [0, 1] between the lightest (all-Gly) and heaviest (all-Trp) + sequences of the same length, which reduces to + (mean residue mass - min mass) / (max mass - min mass); column + ``MolecularWeight_norm``. Returns ======= @@ -614,11 +637,17 @@ def molecular_weight(sequence: str) -> pd.DataFrame: mw = sum(residue_molecular_weight_[aa] for aa in sequence) - (len(sequence) - 1) * 18.015 molecular_weight_df = pd.DataFrame([[round(mw, 3)]], columns=["MolecularWeight"]) + if normalized: + L = len(sequence) + lightest = L * min(residue_molecular_weight_.values()) - (L - 1) * 18.015 + heaviest = L * max(residue_molecular_weight_.values()) - (L - 1) * 18.015 + return _minmax(molecular_weight_df, lightest, heaviest) return molecular_weight_df ############################## Charge Distribution ######################################## -def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy") -> pd.DataFrame: +def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy", + normalized: bool = False) -> pd.DataFrame: """ Calculate Charge Distribution of a protein sequence at a given pH. Positive charge is contributed by Lys (K), Arg (R) and His (H); negative charge by @@ -637,6 +666,11 @@ def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy :pka_scale: str (default="protpy") pKa value set for K, R, H, D and E: "protpy", "emboss", "bjellqvist" or "ipc_protein" (see ``isoelectric_point``). Termini are not included. + :normalized: bool (default=False) + rescale to [0, 1] at the given pH: PositiveCharge / (L x f+), + NegativeCharge / (L x f-) and (NetCharge + L x f-) / (L x (f+ + f-)), where + f+ is the charge of the most basic residue (highest pKa) and f- that of the + most acidic (lowest pKa); columns get a ``_norm`` suffix. Returns ======= @@ -662,6 +696,11 @@ def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy columns=["PositiveCharge", "NegativeCharge", "NetCharge"] ) + if normalized: + L = len(sequence) + f_pos = max(1 / (1 + 10 ** (ph - pka_charged_[aa])) for aa in ("K", "R", "H")) + f_neg = max(1 / (1 + 10 ** (pka_charged_[aa] - ph)) for aa in ("D", "E")) + return _minmax(charge_distribution_df, [0, 0, -L * f_neg], [L * f_pos, L * f_neg, L * f_pos]) return charge_distribution_df ####################### Hydrophobic/Polar/Charged Composition ############################ @@ -725,7 +764,7 @@ def hydrophobic_polar_charged_composition(sequence: str) -> pd.DataFrame: "R": 1.24, "S": 1.33, "T": 1.03, "V": 0.50, "W": 0.99, "Y": 1.29 } -def secondary_structure_propensity(sequence: str) -> pd.DataFrame: +def secondary_structure_propensity(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate Secondary Structure Propensity of a protein sequence. Each residue is assigned Chou-Fasman propensity values for alpha-helix (H), beta-sheet (E) @@ -736,6 +775,9 @@ def secondary_structure_propensity(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale each column to [0, 1] with the minimum and maximum residue + propensity of its table; columns get a ``_norm`` suffix. Returns ======= @@ -753,6 +795,9 @@ def secondary_structure_propensity(sequence: str) -> pd.DataFrame: ssp_df = pd.DataFrame([[h, e, c]], columns=["Helix", "Sheet", "Coil"]) + if normalized: + tables = (helix_propensity_, sheet_propensity_, coil_propensity_) + return _minmax(ssp_df, [min(t.values()) for t in tables], [max(t.values()) for t in tables]) return ssp_df ################################# k-mer Composition ###################################### @@ -999,7 +1044,7 @@ def _aa_class(aa): ################################## Aliphatic Index ######################################## -def aliphatic_index(sequence: str) -> pd.DataFrame: +def aliphatic_index(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate the Aliphatic Index (AI) of a protein sequence. The aliphatic index is a measure of thermostability, defined as the relative volume @@ -1014,6 +1059,9 @@ def aliphatic_index(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale to [0, 1] by the maximum of 390 (an all-Ile/Leu sequence); + column ``AliphaticIndex_norm``. Returns ======= @@ -1035,11 +1083,13 @@ def aliphatic_index(sequence: str) -> pd.DataFrame: aliphatic_index_df = pd.DataFrame([[ai]], columns=["AliphaticIndex"]) + if normalized: + return _minmax(aliphatic_index_df, 0.0, 390.0) return aliphatic_index_df ############################### Extinction Coefficient #################################### -def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFrame: +def extinction_coefficient(sequence: str, absorbance: bool = False, normalized: bool = False) -> pd.DataFrame: """ Calculate the molar Extinction Coefficient of a protein sequence at 280 nm. The coefficient is derived from the number of Trp (W), Tyr (Y) and @@ -1059,6 +1109,10 @@ def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFr protein sequence. :absorbance: bool (default=False) also return ``Abs01_Reduced`` and ``Abs01_Oxidized``. + :normalized: bool (default=False) + rescale to [0, 1]: coefficients by the all-Trp maximum 5500 x L, and + absorbances by the per-residue maximum 5500 / (204.228 - 18.015) (Trp's + coefficient over its residue mass); columns get a ``_norm`` suffix. Returns ======= @@ -1088,6 +1142,9 @@ def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFr extinction_coefficient_df["Abs01_Reduced"] = round(ec_reduced / mw, 3) extinction_coefficient_df["Abs01_Oxidized"] = round(ec_oxidized / mw, 3) + if normalized: + hi = [5500 * len(sequence)] * 2 + [5500 / (residue_molecular_weight_["W"] - 18.015)] * (2 if absorbance else 0) + return _minmax(extinction_coefficient_df, 0.0, hi) return extinction_coefficient_df ################################ Boman Index ############################################## @@ -1100,7 +1157,7 @@ def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFr "W": 2.33, "Y": -0.14 } -def boman_index(sequence: str) -> pd.DataFrame: +def boman_index(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate the Boman Index (potential protein interaction index) for a protein sequence. The index is the sum of the solubility values of @@ -1116,6 +1173,9 @@ def boman_index(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale to [0, 1] between -max and -min of the solubility scale + (BI is a negated mean); column ``BomanIndex_norm``. Returns ======= @@ -1130,6 +1190,8 @@ def boman_index(sequence: str) -> pd.DataFrame: boman_index_df = pd.DataFrame([[bi]], columns=["BomanIndex"]) + if normalized: + return _minmax(boman_index_df, -max(boman_scale_.values()), -min(boman_scale_.values())) return boman_index_df ############################### Aggregation Propensity #################################### @@ -1192,7 +1254,7 @@ def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_thresh ################################## Shannon Entropy ######################################## -def shannon_entropy(sequence: str) -> pd.DataFrame: +def shannon_entropy(sequence: str, normalized: bool = False) -> pd.DataFrame: """ Calculate the Shannon Entropy of a protein sequence. Shannon entropy quantifies the diversity of the amino acid composition using the @@ -1211,6 +1273,9 @@ def shannon_entropy(sequence: str) -> pd.DataFrame: ========== :sequence: str protein sequence. + :normalized: bool (default=False) + rescale to [0, 1] by the maximum log2(20) bits; column + ``ShannonEntropy_norm``. Returns ======= @@ -1235,12 +1300,14 @@ def shannon_entropy(sequence: str) -> pd.DataFrame: shannon_entropy_df = pd.DataFrame([[round(entropy, 3)]], columns=["ShannonEntropy"]) + if normalized: + return _minmax(shannon_entropy_df, 0.0, math.log2(20)) return shannon_entropy_df ############################### Hydrophobic Moment ######################################## def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100, - scale: str | None = None) -> pd.DataFrame: + scale: str | None = None, normalized: bool = False) -> pd.DataFrame: """ Calculate the mean and maximum Hydrophobic Moment of a protein sequence. The hydrophobic moment measures the amphiphilicity of a helical segment — @@ -1271,6 +1338,9 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100 residue rotation angle in degrees, or "helix" (100) / "sheet" (160). :scale: str | None (default=None) hydrophobicity scale; None uses protpy's default table. + :normalized: bool (default=False) + rescale to [0, 1] by the largest absolute value of the hydrophobicity + scale (a window moment cannot exceed it); columns get a ``_norm`` suffix. Returns ======= @@ -1316,11 +1386,14 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100 columns=["HydrophobicMoment_Mean", "HydrophobicMoment_Max"] ) + if normalized: + return _minmax(hydrophobic_moment_df, 0.0, max(abs(v) for v in h_scale.values())) return hydrophobic_moment_df ############################## Pseudo Amino Acid Composition ############################## -def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float = 0.05, properties: list[str] | None = None) -> pd.DataFrame: +def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float = 0.05, properties: list[str] | None = None, + normalized: bool = False) -> pd.DataFrame: """ Pseudo amino acid composition (PAAComp) combines the vanilla amino acid composition descriptor with additional local features, such as correlation between residues of @@ -1342,6 +1415,9 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float weighting factor. :properties: list list of dicts of physicochemical/structural property values for amino acids. + :normalized: bool (default=False) + rescale to [0, 1] by dividing by 100 (components are non-negative + percentages summing to 100); columns get a ``_norm`` suffix. Returns ======= @@ -1407,6 +1483,8 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float psuedo_amino_acid_composition_df = pd.DataFrame([list(psuedo_aac.values())], columns=list(psuedo_aac.keys())) + if normalized: + return _minmax(psuedo_amino_acid_composition_df, 0.0, 100.0) return psuedo_amino_acid_composition_df def sequence_order_correlation_factor(sequence: str, k: int = 1, properties: list[dict[str, float]] | None = None) -> float: diff --git a/protpy/ctd.py b/protpy/ctd.py index ae3d43a..0de16d9 100644 --- a/protpy/ctd.py +++ b/protpy/ctd.py @@ -11,7 +11,7 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence -from ._helpers import _load_json_table +from ._helpers import _load_json_table, _minmax """ References @@ -110,7 +110,7 @@ def str_to_num(sequence: str, property: dict[str, str]) -> str: ##################################### CTD Composition ##################################### -def ctd_composition(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: +def ctd_composition(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: """ Calculate composition physicochemical/structural descriptor. Composition is determined as the number of amino acids of a particular property @@ -123,6 +123,9 @@ def ctd_composition(sequence: str, property: str = "hydrophobicity") -> pd.DataF protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. + :normalized: bool (default=False) + composition values are already fractions in [0, 1]; they are returned + unchanged with a ``_norm`` column suffix, for consistency. Returns ======= @@ -156,11 +159,13 @@ def ctd_composition(sequence: str, property: str = "hydrophobicity") -> pd.DataF #transform values and columns to DataFrame ctd_composition_df = pd.DataFrame([list(ctd_composition_.values())], columns=list(ctd_composition_.keys())) + if normalized: + return _minmax(ctd_composition_df, 0.0, 1.0) return ctd_composition_df ##################################### CTD Transition ###################################### -def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: +def ctd_transition(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: """ Calculate transition physicochemical/structural descriptor. Transition is determined as the number of transitions from a particular property @@ -174,6 +179,9 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFr protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. + :normalized: bool (default=False) + transition values are already fractions in [0, 1]; they are returned + unchanged with a ``_norm`` column suffix, for consistency. Returns ======= @@ -215,11 +223,13 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFr #transform values and columns to DataFrame ctd_transition_df = pd.DataFrame([list(ctd_transition_.values())], columns=list(ctd_transition_.keys())) + if normalized: + return _minmax(ctd_transition_df, 0.0, 1.0) return ctd_transition_df #################################### CTD Distribution ##################################### -def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: +def ctd_distribution(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: """ Calculate distribution physicochemical/structural descriptor. Distribution is the chain length within which the first, 25%, 50%, 75% and 100% of the @@ -232,6 +242,9 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.Data protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. + :normalized: bool (default=False) + rescale the distribution percentages to [0, 1] (/ 100); columns get a + ``_norm`` suffix. Returns ======= @@ -294,10 +307,12 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.Data #transform values and columns to DataFrame ctd_distribution_df = pd.DataFrame([list(ctd_distribution_.values())], columns=list(ctd_distribution_.keys())) + if normalized: + return _minmax(ctd_distribution_df, 0.0, 100.0) return ctd_distribution_df def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, - property_set: str = "default") -> pd.DataFrame: + property_set: str = "default", normalized: bool = False) -> pd.DataFrame: """ Calculate all Composition, Transition and Distribution (CTD) features of protein sequences. Composition is the number of amino acids of a particular property (e.g., hydrophobicity) @@ -326,6 +341,9 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, property set used when ``all_ctd=True``: "default" (the 7 properties above, 1 x 147) or "ifeature13" (adds the six AAindex hydrophobicity partitions used by iFeature, 13 properties, 1 x 273) [4]. + :normalized: bool (default=False) + rescale to [0, 1] (composition and transition unchanged, distribution + / 100); columns get a ``_norm`` suffix. Returns ======= @@ -346,17 +364,17 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, #if using single property, calculate each of the CTD descriptors individually if not all_ctd: - comp_df = ctd_composition(sequence, property=property) - trans_df = ctd_transition(sequence, property=property) - distr_df = ctd_distribution(sequence, property=property) + comp_df = ctd_composition(sequence, property=property, normalized=normalized) + trans_df = ctd_transition(sequence, property=property, normalized=normalized) + distr_df = ctd_distribution(sequence, property=property, normalized=normalized) else: #if using all calculable properties, calculate CTD descriptors for each property for prop in ctd_property_sets[property_set]: - comp = ctd_composition(sequence, property=prop) + comp = ctd_composition(sequence, property=prop, normalized=normalized) comp_df = pd.concat([comp_df, comp], axis=1) - trans = ctd_transition(sequence, property=prop) + trans = ctd_transition(sequence, property=prop, normalized=normalized) trans_df = pd.concat([trans_df, trans], axis=1) - distr = ctd_distribution(sequence, property=prop) + distr = ctd_distribution(sequence, property=prop, normalized=normalized) distr_df = pd.concat([distr_df, distr], axis=1) #concatenate all descriptors diff --git a/protpy/encode.py b/protpy/encode.py index 135071d..36a01f2 100644 --- a/protpy/encode.py +++ b/protpy/encode.py @@ -53,6 +53,15 @@ #descriptors that accept extra keyword arguments beyond sequence _PARAMETERISED_DESCRIPTORS: frozenset[str] = frozenset({ + "gravy", + "instability_index", + "isoelectric_point", + "molecular_weight", + "secondary_structure_propensity", + "aliphatic_index", + "extinction_coefficient", + "boman_index", + "shannon_entropy", "charge_distribution", "kmer_composition", "reduced_alphabet_composition", @@ -165,6 +174,18 @@ def all_descriptors( sequence_order_coupling_number_all → lag quasi_sequence_order_all → lag, weight + Every descriptor above, and ``gravy``, ``instability_index``, + ``isoelectric_point``, ``molecular_weight``, + ``secondary_structure_propensity``, ``aliphatic_index``, + ``extinction_coefficient``, ``boman_index`` and ``shannon_entropy``, + also accepts ``normalized=True``, which rescales its output to [0, 1] + and suffixes its columns with ``_norm``:: + + df = protpy.all_descriptors(seq, params={ + 'gravy': {'normalized': True}, + 'ctd_': {'normalized': True}, + }) + plus any v1.5 descriptor with parameters (see ``protpy.get_descriptor_info(name)["parameters"]``), e.g. ``{"kappa": {"blob_sizes": (5,)}, "aggrescan": {"window": 7}}``. @@ -269,24 +290,24 @@ def _p(name: str) -> dict: ("[Composition] Amino acid composition", "amino_acid_composition", lambda: amino_acid_composition(sequence)), ("[Composition] Dipeptide composition", "dipeptide_composition", lambda: dipeptide_composition(sequence)), ("[Composition] Tripeptide composition", "tripeptide_composition", lambda: tripeptide_composition(sequence)), - ("[Composition] GRAVY index", "gravy", lambda: gravy(sequence)), + ("[Composition] GRAVY index", "gravy", lambda: gravy(sequence, **_p("gravy"))), ("[Composition] Aromaticity", "aromaticity", lambda: aromaticity(sequence)), - ("[Composition] Instability index", "instability_index", lambda: instability_index(sequence)), - ("[Composition] Isoelectric point", "isoelectric_point", lambda: isoelectric_point(sequence)), - ("[Composition] Molecular weight", "molecular_weight", lambda: molecular_weight(sequence)), + ("[Composition] Instability index", "instability_index", lambda: instability_index(sequence, **_p("instability_index"))), + ("[Composition] Isoelectric point", "isoelectric_point", lambda: isoelectric_point(sequence, **_p("isoelectric_point"))), + ("[Composition] Molecular weight", "molecular_weight", lambda: molecular_weight(sequence, **_p("molecular_weight"))), ("[Composition] Charge distribution", "charge_distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))), ("[Composition] Hydrophobic / polar / charged comp.", "hydrophobic_polar_charged_composition", lambda: hydrophobic_polar_charged_composition(sequence)), - ("[Composition] Secondary structure propensity", "secondary_structure_propensity", lambda: secondary_structure_propensity(sequence)), + ("[Composition] Secondary structure propensity", "secondary_structure_propensity", lambda: secondary_structure_propensity(sequence, **_p("secondary_structure_propensity"))), *([("[Composition] K-mer composition", "kmer_composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))] if _kmer_k not in (2, 3) else []), ("[Composition] Reduced alphabet composition", "reduced_alphabet_composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))), ("[Composition] Motif composition", "motif_composition", lambda: motif_composition(sequence, **_p("motif_composition"))), - ("[Composition] Aliphatic index", "aliphatic_index", lambda: aliphatic_index(sequence)), - ("[Composition] Extinction coefficient", "extinction_coefficient", lambda: extinction_coefficient(sequence)), - ("[Composition] Boman index", "boman_index", lambda: boman_index(sequence)), + ("[Composition] Aliphatic index", "aliphatic_index", lambda: aliphatic_index(sequence, **_p("aliphatic_index"))), + ("[Composition] Extinction coefficient", "extinction_coefficient", lambda: extinction_coefficient(sequence, **_p("extinction_coefficient"))), + ("[Composition] Boman index", "boman_index", lambda: boman_index(sequence, **_p("boman_index"))), ("[Composition] Aggregation propensity", "aggregation_propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))), ("[Composition] Hydrophobic moment", "hydrophobic_moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))), - ("[Composition] Shannon entropy", "shannon_entropy", lambda: shannon_entropy(sequence)), + ("[Composition] Shannon entropy", "shannon_entropy", lambda: shannon_entropy(sequence, **_p("shannon_entropy"))), ("[Composition] Pseudo amino acid composition", "pseudo_amino_acid_composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))), ("[Composition] Amphiphilic pseudo amino acid comp.", "amphiphilic_pseudo_amino_acid_composition", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))), # ── Autocorrelation ──────────────────────────────────────────────────── diff --git a/protpy/sequence_order.py b/protpy/sequence_order.py index 1629ec1..cadc361 100644 --- a/protpy/sequence_order.py +++ b/protpy/sequence_order.py @@ -32,6 +32,7 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight +from ._helpers import _minmax @functools.lru_cache(maxsize=2) def _load_distance_matrix(name: str) -> dict: @@ -48,7 +49,8 @@ def _load_distance_matrix(name: str) -> dict: ############################# Sequence Order Coupling Number ############################## -def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: str = "schneider-wrede") -> float: +def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: str = "schneider-wrede", + normalized: bool = False) -> float: """ Calculate Sequence Order Coupling Number (SOCN) features for input protein sequence. SOCN computes the dissimilarity between amino acid pairs. The distance between @@ -69,6 +71,9 @@ def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: gap between two amino acids. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". + :normalized: bool (default=False) + rescale to [0, 1] by the maximum possible value (N - d) x max(distance)^2 + (tau sums N - d squared distances); 0 when N <= d. Rounded to 6 dp. Returns ======= @@ -98,10 +103,13 @@ def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: next_aa = sequence[aa + d] tau = tau + math.pow(distance_matrix_data[current_aa + next_aa], 2) + if normalized: + bound = (len(sequence) - d) * max(distance_matrix_data.values()) ** 2 + return round(tau / bound, 6) if bound > 0 else 0.0 return round(tau, 3) def sequence_order_coupling_number(sequence: str, lag: int = 30, - distance_matrix: str = "schneider-wrede") -> pd.DataFrame: + distance_matrix: str = "schneider-wrede", normalized: bool = False) -> pd.DataFrame: """ Calculate Sequence Order Coupling Number (SOCN) features for input protein sequence. SOCN computes the dissimilarity between amino acid pairs. The distance between @@ -118,6 +126,9 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, maximum gap between 2 amino acids; the protein length should be larger than lag. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". + :normalized: bool (default=False) + rescale every SOCN to [0, 1] as in ``sequence_order_coupling_number_``; + columns get a ``_norm`` suffix. Returns ======= @@ -149,17 +160,17 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, #iterate over sequence with lag, calculating SOCN using input distance matrix values for i in range(lag): - tau = sequence_order_coupling_number_(sequence, i+1, distance_matrix) + tau = sequence_order_coupling_number_(sequence, i+1, distance_matrix, normalized=normalized) #append SOCN column and value to dict - seq_order[col_prefix + str(i + 1)] = round(tau, 3) + seq_order[col_prefix + str(i + 1) + ("_norm" if normalized else "")] = tau if normalized else round(tau, 3) #transform SOCN data into pandas dataframe sequence_order_df = pd.DataFrame([list(seq_order.values())], columns=list(seq_order.keys())) return sequence_order_df -def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataFrame: +def sequence_order_coupling_number_all(sequence: str, lag: int = 30, normalized: bool = False) -> pd.DataFrame: """ Calculate Sequence Order Coupling Number (SOCN) descriptor values of input protein sequence using both matrices (schneider-wrede & grantham) [3]. The distance between @@ -175,6 +186,9 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataF protein sequence. :lag: int (default=30) maximum gap between 2 amino acids; the protein length should be larger than lag. + :normalized: bool (default=False) + rescale every SOCN to [0, 1] (see ``sequence_order_coupling_number_``); + columns get a ``_norm`` suffix. Returns ======= @@ -196,10 +210,10 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataF lag = 30 #calculate SOCN using schneider-wrede distance matrix - socn_schneider = sequence_order_coupling_number(sequence, lag, "schneider-wrede") + socn_schneider = sequence_order_coupling_number(sequence, lag, "schneider-wrede", normalized) #calculate SOCN using grantham distance matrix - socn_grantham = sequence_order_coupling_number(sequence, lag, "grantham") + socn_grantham = sequence_order_coupling_number(sequence, lag, "grantham", normalized) #merge 2 dataframes socn_all = pd.concat([socn_schneider, socn_grantham], axis=1) @@ -209,7 +223,7 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataF ################################## Quasi Sequence Order ################################### def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, - distance_matrix: str = "schneider-wrede") -> pd.DataFrame: + distance_matrix: str = "schneider-wrede", normalized: bool = False) -> pd.DataFrame: """ Calculate Quasi Sequence Order (QSO) features for the protein sequences. The quasi-sequence-order descriptors were proposed by K.C. Chou, et.al. [1]. @@ -231,6 +245,11 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, weighting factor. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". + :normalized: bool (default=False) + rescale to [0, 1]: the first 20 components (amino-acid percentages over + 1 + w x sum(tau), at most 100) are divided by 100 and the lag components + (w x tau_d / (1 + w x sum(tau)), always below 1) are kept; columns get a + ``_norm`` suffix. Returns ======= @@ -283,9 +302,13 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, quasi_sequence_order_df = pd.DataFrame([list(quasi_sequence_order.values())], columns=list(quasi_sequence_order.keys())) + if normalized: + #first 20 components: amino-acid percentages / (1 + w sum(tau)) <= 100; + #lag components: w tau_d / (1 + w sum(tau)) < 1 + return _minmax(quasi_sequence_order_df, 0.0, [100.0] * 20 + [1.0] * (quasi_sequence_order_df.shape[1] - 20)) return quasi_sequence_order_df -def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1) -> pd.DataFrame: +def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1, normalized: bool = False) -> pd.DataFrame: """ Calculate Quasi Sequence Order features for input protein sequence using both physicochemical distance matrices. Concatenate into one output dataframe. @@ -303,6 +326,8 @@ def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1) maximum gap between 2 amino acids; the protein length should be larger than lag. :weight: float (default=0.1) weighting factor. + :normalized: bool (default=False) + see ``quasi_sequence_order``; columns get a ``_norm`` suffix. Returns ======= @@ -327,11 +352,11 @@ def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1) #calculate quasi seq order using schneider distance matrix quasi_schneider = quasi_sequence_order(sequence, lag, weight=weight, - distance_matrix="schneider-wrede") + distance_matrix="schneider-wrede", normalized=normalized) #calculate quasi seq order using schneider distance matrix quasi_grantham = quasi_sequence_order(sequence, lag, weight=weight, - distance_matrix="grantham") + distance_matrix="grantham", normalized=normalized) #concat quasi sequence order dataframes quasi_sequence_order_all_df = pd.concat([quasi_schneider, quasi_grantham], axis=1) diff --git a/protpy/utils.py b/protpy/utils.py index 8957d51..8de1303 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -77,6 +77,7 @@ "parameters": { "sequence": "str — protein sequence", "scale": "str (default='kyte_doolittle') — single-value scale or AAindex code (column GRAVY_{scale})", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -110,6 +111,7 @@ "abbreviation": "II", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -128,6 +130,7 @@ "parameters": { "sequence": "str — protein sequence", "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -145,6 +148,7 @@ "abbreviation": "MW", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -164,6 +168,7 @@ "sequence": "str — protein sequence", "ph": "float (default=7.4) — pH for charge calculation", "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3", "speed": "Fast", @@ -199,6 +204,7 @@ "abbreviation": "SSP", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3", "speed": "Fast", @@ -291,6 +297,7 @@ "abbreviation": "AI", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -309,6 +316,7 @@ "parameters": { "sequence": "str — protein sequence", "absorbance": "bool (default=False) — add Abs01_Reduced / Abs01_Oxidized (A280 of 1 g/L)", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 2 [ExtCoeff_Reduced, ExtCoeff_Oxidized]", "speed": "Fast", @@ -327,6 +335,7 @@ "abbreviation": "BI", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -369,6 +378,7 @@ "window": "int (default=11) — helical window size", "angle": "float | str (default=100) — rotation angle in degrees, or 'helix' (100) / 'sheet' (160)", "scale": "str | None (default=None) — hydrophobicity scale; None keeps protpy's default table", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 2 [HydrophobicMoment_Mean, HydrophobicMoment_Max]", "speed": "Fast", @@ -386,6 +396,7 @@ "abbreviation": "ShannonH", "parameters": { "sequence": "str — protein sequence", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -407,6 +418,7 @@ "lamda": "int (default=30) — rank of correlation (output = 20 + lamda features)", "weight": "float (default=0.05) — weighting factor for correlation terms", "properties": "list[str] (default=[]) — AAIndex1 accession codes; empty = use built-in hydrophobicity/hydrophilicity/mass", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (20 + lamda) (default 1 x 50)", "speed": "Moderate", @@ -449,7 +461,8 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values before calculation", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -470,7 +483,8 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -491,7 +505,8 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -531,6 +546,7 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — one of: hydrophobicity, normalized_vdwv, polarity, charge, secondary_struct, solvent_accessibility, polarizability", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3 (per property)", "speed": "Fast", @@ -549,6 +565,7 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — CTD physicochemical property", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3 (per property)", "speed": "Fast", @@ -568,6 +585,7 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — CTD physicochemical property", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 15 (per property)", "speed": "Fast", @@ -589,6 +607,7 @@ "property": "str (default='hydrophobicity') — CTD property (used when all_ctd=False)", "all_ctd": "bool (default=True) — if True, compute over all 7 properties", "property_set": "str (default='default') — 'default' (7 properties) or 'ifeature13' (13 properties, 1 x 273)", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 21 (single property) / 1 x 147 (all 7 properties)", "speed": "Fast (single) / Moderate (all)", @@ -610,6 +629,7 @@ "sequence": "str — protein sequence", "d": "int (default=1) — gap between amino acid pairs", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "scalar float", "speed": "Fast", @@ -629,6 +649,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap; output features = lag", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x lag (default 1 x 30)", "speed": "Moderate", @@ -647,6 +668,7 @@ "parameters": { "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap; output features = lag × 2", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag × 2) (default 1 x 60)", "speed": "Moderate", @@ -668,6 +690,7 @@ "lag": "int (default=30) — maximum gap; output features = lag + 20", "weight": "float (default=0.1) — weighting factor for SOCN terms", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag + 20) (default 1 x 50)", "speed": "Moderate", @@ -686,6 +709,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap", "weight": "float (default=0.1) — weighting factor", + "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x ((lag + 20) × 2) (default 1 x 100)", "speed": "Moderate", diff --git a/tests/README.md b/tests/README.md index a681ed8..f883486 100644 --- a/tests/README.md +++ b/tests/README.md @@ -23,6 +23,7 @@ Unit tests * `test_complexity.py` - Wootton-Federhen, linguistic and Lempel-Ziv complexity. * `test_signal.py` - FFT and DCT descriptors. * `test_structural.py` - AGGRESCAN, Uversky/FoldIndex, disorder propensity, TM and signal-peptide features, functional sites, PLAAC (against PLAAC's reference output), antigenicity and cysteine features. +* `test_normalization.py` - `normalized=True`: default outputs unchanged, `_norm` column suffixes, outputs within [0, 1] for random, homopolymer and very short sequences (with clipping disabled, to prove the bounds), bounds reached by extreme sequences, and the option through `all_descriptors(params=...)`. * `test_encode.py` - `all_descriptors`, `encode_dataset` and `read_sequences_from_file`. * `test_descriptor_sets.py` - `descriptor_set`, `include` and `exclude`, and every v1.5.0 descriptor on 1-8 residue sequences. * `test_utils.py` - descriptor registry and metadata functions. diff --git a/tests/test_encode.py b/tests/test_encode.py index a962690..b60bedc 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -268,7 +268,7 @@ def test_all_descriptors_params_validation(self): with self.assertRaises(ValueError): protpy.all_descriptors(self.protein_seq1, params={'conjoint_triad': {}}) with self.assertRaises(ValueError): - protpy.all_descriptors(self.protein_seq1, params={'gravy': {}}) + protpy.all_descriptors(self.protein_seq1, params={'aromaticity': {}}) #3.) params of wrong type raises TypeError with self.assertRaises(TypeError): protpy.all_descriptors(self.protein_seq1, params="lag=15") diff --git a/tests/test_normalization.py b/tests/test_normalization.py new file mode 100644 index 0000000..06f1a28 --- /dev/null +++ b/tests/test_normalization.py @@ -0,0 +1,159 @@ +########################################################################### +############ protPy - normalized=True output rescaling tests ######### +########################################################################### + +import inspect +import math +import random +import unittest + +import numpy as np + +import protpy + +SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDWC" + +#descriptors with a normalized parameter (dataframe outputs) +NORMALIZABLE = [ + "gravy", "instability_index", "isoelectric_point", "molecular_weight", "charge_distribution", + "secondary_structure_propensity", "aliphatic_index", "extinction_coefficient", "boman_index", + "shannon_entropy", "hydrophobic_moment", "pseudo_amino_acid_composition", + "moreaubroto_autocorrelation", "moran_autocorrelation", "geary_autocorrelation", + "ctd_composition", "ctd_transition", "ctd_distribution", "ctd_", + "sequence_order_coupling_number", "sequence_order_coupling_number_all", + "quasi_sequence_order", "quasi_sequence_order_all", +] + + +def _sequences(): + """Random sequences of several lengths plus homopolymers and very short edge cases.""" + rng = random.Random(7) + seqs = ["".join(rng.choice(protpy.amino_acids) for _ in range(n)) for n in (35, 60, 120, 400)] + seqs += [aa * 35 for aa in protpy.amino_acids] + seqs += ["A", "AC", "WWWWW", "KKKKDDDDKKKKDDDD"] + return seqs + + +class ProtpyNormalizationTests(unittest.TestCase): + + def test_default_off_and_unchanged(self): + for name in NORMALIZABLE: + with self.subTest(name=name): + func = getattr(protpy, name) + self.assertIs(inspect.signature(func).parameters["normalized"].default, False) + self.assertTrue(func(SEQ).equals(func(SEQ, normalized=False))) + + def test_columns_suffixed_and_in_unit_range(self): + for name in NORMALIZABLE: + func = getattr(protpy, name) + for seq in _sequences(): + with self.subTest(name=name, seq=seq[:12]): + raw, norm = func(seq), func(seq, normalized=True) + self.assertEqual(list(norm.columns), [f"{c}_norm" for c in raw.columns]) + values = norm.to_numpy(dtype=float) + self.assertFalse(np.isnan(values).any()) + self.assertTrue(((values >= 0) & (values <= 1)).all()) + + def test_bounds_hold_without_clipping(self): + #_minmax clips only to absorb 3 dp rounding of raw values; with clipping disabled every + #value must still lie within rounding distance of [0, 1], proving the bounds are correct + from unittest import mock + with mock.patch("protpy._helpers._unit_clip", side_effect=lambda v: v): + for name in NORMALIZABLE: + for seq in _sequences(): + with self.subTest(name=name, seq=seq[:12]): + values = getattr(protpy, name)(seq, normalized=True).to_numpy(dtype=float) + self.assertTrue(((values >= -1e-3) & (values <= 1 + 1e-3)).all(), values.min()) + + def test_options_combine_with_normalized(self): + #bounds must also hold, unclipped, for every option that changes the raw values + from unittest import mock + cases = [ + ("gravy", {"scale": "hopp_woods"}), ("gravy", {"scale": "KYTJ820101"}), + ("charge_distribution", {"ph": 3.0, "pka_scale": "ipc_protein"}), ("charge_distribution", {"ph": 11.0}), + ("isoelectric_point", {"pka_scale": "bjellqvist"}), + ("extinction_coefficient", {"absorbance": True}), ("hydrophobic_moment", {"scale": "eisenberg", "angle": "sheet"}), + ("hydrophobic_moment", {"window": 3, "angle": 160}), + ("pseudo_amino_acid_composition", {"lamda": 5, "weight": 0.9}), + ("moreaubroto_autocorrelation", {"normalize": False, "lag": 5}), ("moran_autocorrelation", {"normalize": False}), + ("geary_autocorrelation", {"lag": 40}), ("ctd_", {"property_set": "ifeature13"}), + ("ctd_", {"all_ctd": False, "property": "charge"}), + ("quasi_sequence_order", {"weight": 0.9, "lag": 5, "distance_matrix": "grantham"}), + ("sequence_order_coupling_number", {"lag": 10, "distance_matrix": "grantham"}), + ] + with mock.patch("protpy._helpers._unit_clip", side_effect=lambda v: v): + for name, kwargs in cases: + for seq in _sequences(): + with self.subTest(name=name, kwargs=kwargs, seq=seq[:12]): + values = getattr(protpy, name)(seq, normalized=True, **kwargs).to_numpy(dtype=float) + self.assertTrue(((values >= -1e-3) & (values <= 1 + 1e-3)).all()) + + def test_bounds_are_attained(self): + def one(name, seq, col=0, **kw): + return getattr(protpy, name)(seq, normalized=True, **kw).iloc[0, col] + self.assertEqual(one("gravy", "IIII"), 1.0) + self.assertEqual(one("gravy", "RRRR"), 0.0) + self.assertEqual(one("aliphatic_index", "LLLL"), 1.0) + self.assertEqual(one("aliphatic_index", "GGGG"), 0.0) + self.assertEqual(one("shannon_entropy", "".join(protpy.amino_acids)), 1.0) + self.assertEqual(one("shannon_entropy", "AAAA"), 0.0) + self.assertEqual(one("molecular_weight", "WWWW"), 1.0) + self.assertEqual(one("molecular_weight", "GGGG"), 0.0) + self.assertEqual(one("extinction_coefficient", "WWWW"), 1.0) + #A is in hydrophobicity group 2; column 9 is its 100% position (residue L of L) + self.assertEqual(one("ctd_distribution", "A" * 10, col=9), 1.0) + #all-Arg: positive charge equals its maximum; all-Asp: negative charge does + self.assertEqual(one("charge_distribution", "RRRR", col=0), 1.0) + self.assertEqual(one("charge_distribution", "DDDD", col=1), 1.0) + self.assertEqual(one("charge_distribution", "DDDD", col=2), 0.0) + self.assertEqual(one("charge_distribution", "RRRR", col=2), 1.0) + #a constant signal has no autocorrelation: Moran maps to the midpoint + self.assertEqual(one("moran_autocorrelation", "A" * 40), 0.5) + self.assertAlmostEqual(one("pseudo_amino_acid_composition", SEQ) * 100, + protpy.pseudo_amino_acid_composition(SEQ).iloc[0, 0], places=3) + + def test_socn_scalar(self): + raw = protpy.sequence_order_coupling_number_(SEQ, d=2, distance_matrix="grantham") + norm = protpy.sequence_order_coupling_number_(SEQ, d=2, distance_matrix="grantham", normalized=True) + self.assertAlmostEqual(norm, raw / ((len(SEQ) - 2) * 4.226 ** 2), places=4) + self.assertEqual(protpy.sequence_order_coupling_number_("AC", d=5, normalized=True), 0.0) + #"aabb" with d=2 gives only the pairs (a, b), (a, b): a maximally distant pair reaches the bound + dist = protpy.sequence_order._load_distance_matrix("grantham") + a, b = max(dist, key=dist.get) + self.assertEqual(protpy.sequence_order_coupling_number_(a + a + b + b, d=2, distance_matrix="grantham", + normalized=True), 1.0) + + def test_moran_geary_bounds_match_brute_force(self): + #exact theoretical bounds: |I| <= N/(N-d), 0 <= C <= 2(N-1)/(N-d) + rng = random.Random(3) + for _ in range(30): + seq = "".join(rng.choice("AKDW") for _ in range(rng.randint(8, 40))) + for name in ("moran_autocorrelation", "geary_autocorrelation"): + values = getattr(protpy, name)(seq, lag=5, normalized=True).to_numpy(dtype=float) + self.assertTrue(((values >= 0) & (values <= 1)).all(), (name, seq)) + + def test_all_descriptors_params(self): + df = protpy.all_descriptors(SEQ, params={"gravy": {"normalized": True}, "ctd_": {"normalized": True}}, + _show_descriptor_list=False) + self.assertIn("GRAVY_norm", df.columns) + self.assertNotIn("GRAVY", df.columns) + self.assertTrue(all(c.endswith("_norm") for c in df.columns if c.startswith("CTD_"))) + self.assertFalse(df.columns.duplicated().any()) + #default call is unaffected + self.assertIn("GRAVY", protpy.all_descriptors(SEQ, _show_descriptor_list=False).columns) + + def test_encode_dataset_will_not_mix_raw_and_normalized(self): + import os, tempfile + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "enc.csv") + protpy.encode_dataset([SEQ], output_file=out, verbose=False) + with self.assertRaises(ValueError): + protpy.encode_dataset([SEQ], output_file=out, verbose=False, params={"gravy": {"normalized": True}}) + + def test_apaac_has_no_normalized_option(self): + #its denominator 1 + w * sum(tau) can approach zero or go negative, so no fixed bound exists + self.assertNotIn("normalized", inspect.signature(protpy.amphiphilic_pseudo_amino_acid_composition).parameters) + + +if __name__ == "__main__": + unittest.main() From d5a9f3c60269f82222342dd74b0e3c4f42f73480 Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:12:15 +0100 Subject: [PATCH 15/17] Revert "Add opt-in normalized=True ([0, 1] output with _norm columns) to scalar, autocorrelation, CTD and sequence-order descriptors" This reverts commit 28992dc9e0880ef82869faf6727f3018268f745c. --- CHANGELOG.md | 7 -- DESCRIPTORS.md | 36 -------- README.md | 2 - docs/usage.rst | 64 --------------- protpy/_helpers.py | 28 ------- protpy/autocorrelation.py | 57 ++----------- protpy/composition.py | 106 ++++-------------------- protpy/ctd.py | 40 +++------ protpy/encode.py | 39 ++------- protpy/sequence_order.py | 47 +++-------- protpy/utils.py | 30 +------ tests/README.md | 1 - tests/test_encode.py | 2 +- tests/test_normalization.py | 159 ------------------------------------ 14 files changed, 55 insertions(+), 563 deletions(-) delete mode 100644 tests/test_normalization.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a46d67c..dfefcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Structural (`protpy/structural.py`): `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac` (port of the MIT-licensed PLAAC; reproduces its reference output), `antigenicity_surface`, `cysteine_features`. - **Extensions to existing functions** (defaults unchanged): `reduced_alphabet_composition(alphabet=, k=)` (GAAC/GDPC/GTPC), `conjoint_triad(gap=)` (KSCTriad), `ctd_(property_set="ifeature13")`, `amino_acid_composition`/`dipeptide_composition(region=, n=)` (split composition), `isoelectric_point`/`charge_distribution(pka_scale=)` ("emboss", "bjellqvist", "ipc_protein"), `gravy(scale=)`, `hydrophobic_moment(scale=, angle="helix"/"sheet")`, `extinction_coefficient(absorbance=True)`. - **`descriptor_set`, `include`, `exclude`** for `all_descriptors` and `encode_dataset`: `"classic"` (default, unchanged), `"extended"` (adds the new descriptors with ≤150 columns) and `"all"`. Set membership comes from the registry's new `descriptor_sets` field. Every parameterised v1.5.0 descriptor is accepted in `params`. -- **`normalized=False` option** (opt-in; addresses the GitHub request for [0, 1] features) on `gravy`, `instability_index`, `isoelectric_point`, `molecular_weight`, `charge_distribution`, `secondary_structure_propensity`, `aliphatic_index`, `extinction_coefficient`, `boman_index`, `shannon_entropy`, `hydrophobic_moment`, `pseudo_amino_acid_composition`, the three autocorrelation descriptors, `ctd_composition`/`ctd_transition`/`ctd_distribution`/`ctd_`, the SOCN functions and `quasi_sequence_order`/`quasi_sequence_order_all`. - - Outputs are rescaled with each descriptor's theoretical bounds (see DESCRIPTORS.md), and every column gets a `_norm` suffix so raw and normalised values can't be mixed in one CSV. - - Several bounds differ from the ones originally proposed, which would not have kept values in [0, 1]: the hydrophobic moment uses the scale's max abs(H); the charge bounds use L·f(pKa) rather than pKa·L; SOCN uses (N−d)·max(distance)²; Moran uses ±N/(N−d); Geary uses 2(N−1)/(N−d); QSO's composition part is divided by 100. - - `amphiphilic_pseudo_amino_acid_composition` has no such option because its outputs have no fixed bound. - - The existing autocorrelation `normalize` parameter (property standardisation) is unchanged. - - `all_descriptors` now forwards `params` to `gravy`, `instability_index`, `isoelectric_point`, `molecular_weight`, `secondary_structure_propensity`, `aliphatic_index`, `extinction_coefficient`, `boman_index` and `shannon_entropy`. - **Data files** with provenance (`name`, `values`, `reference`, `doi`, `source_note`): `protpy/data/aa_scales.json`, `reduced_alphabets.json`, `residue_tables.json`, `structural_params.json`. - Internal helpers in `protpy/_helpers.py`. The registry gains `doi`, `in_default_set` and `descriptor_sets`, and `print_descriptor_info` prints the DOI. New registry categories: Scales, Physicochemical, Complexity, Signal, Structural. - `docs/references.bib` with BibTeX for every registry DOI. @@ -34,7 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Documentation - `docs/conf.py` converts protpy's `:name: type` parameter style to numpy style before napoleon runs. Parameter names, types and defaults now render correctly throughout the API docs; previously napoleon emitted nameless `:param :` fields. The Sphinx build is now warning-free (previously 39 warnings). - `docs/index.rst` lists the v1.5.0 descriptor categories; `docs/api.rst` documents the new modules and `encode_dataset`; `docs/usage.rst` covers `descriptor_set` / `include` / `exclude`; `docs/descriptors.rst` and `DESCRIPTORS.md` have a v1.5.0 section generated from the registry and docstrings. -- The autocorrelation `normalize` parameter's docs now say what it does: it standardises the property values (it never rescaled the output to 0–1). - `tests/README.md` lists every test module and test data file; `protpy/data/README.md` describes the new data files. ### Changed diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index a1f178a..57044f6 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -702,42 +702,6 @@ result = protpy.quasi_sequence_order_all(protein_seq, lag=15, weight=0.05) --- -## Normalised output (`normalized=True`) - -The descriptors below take an opt-in `normalized=False` parameter. With `normalized=True` every output is rescaled to [0, 1] using the descriptor's theoretical bounds, not the bounds of any particular dataset, and every column gets a `_norm` suffix (e.g. `GRAVY_norm`). Raw and normalised outputs therefore never share a column name, and `encode_dataset` refuses to append one to a file holding the other. Default outputs are unchanged. - -```python -protpy.gravy(seq, normalized=True) # GRAVY_norm -protpy.ctd_(seq, normalized=True) # CTD_..._norm -protpy.all_descriptors(seq, params={"gravy": {"normalized": True}, "moran_autocorrelation": {"normalized": True}}) -``` - -| Descriptor | Rescaling | -|---|---| -| `gravy` | between the scale's min and max residue value (GRAVY is a mean) | -| `instability_index` | [10·min(DIWV, 0), 10·max(DIWV, 0)] | -| `isoelectric_point` | pI / 14 (the searched pH range) | -| `molecular_weight` | between the all-Gly and all-Trp sequences of the same length, i.e. (mean residue mass − min) / (max − min) | -| `charge_distribution` | positive / (L·f₊), negative / (L·f₋), net mapped from [−L·f₋, L·f₊]; f₊, f₋ are the charges of the most basic (highest pKa) and most acidic (lowest pKa) residue at that pH and pKa set | -| `secondary_structure_propensity` | each column between its table's min and max propensity | -| `aliphatic_index` | / 390 (all-Ile/Leu) | -| `extinction_coefficient` | coefficients / (5500·L) (all-Trp); absorbances / (5500 / (204.228 − 18.015)) | -| `boman_index` | between −max and −min of the solubility scale | -| `shannon_entropy` | / log₂20 | -| `hydrophobic_moment` | / max abs(H) of the scale (a window moment cannot exceed it) | -| `pseudo_amino_acid_composition` | / 100 (non-negative percentages summing to 100) | -| `moreaubroto_autocorrelation` | between min and max of P(a)·P(b) over all residue pairs (including 0 for lags without pairs) | -| `moran_autocorrelation` | from [−N/(N−d), N/(N−d)] (Cauchy–Schwarz); 0.5 = no autocorrelation | -| `geary_autocorrelation` | / (2(N−1)/(N−d)) | -| `ctd_composition`, `ctd_transition` | already fractions in [0, 1]; unchanged apart from the `_norm` suffix | -| `ctd_distribution`, `ctd_` | distribution / 100; composition and transition unchanged | -| `sequence_order_coupling_number_` (float), `sequence_order_coupling_number`, `sequence_order_coupling_number_all` | τ_d / ((N−d)·max(distance)²) | -| `quasi_sequence_order`, `quasi_sequence_order_all` | first 20 components / 100 (percentages over 1 + w·Στ ≥ 1); lag components unchanged (always < 1) | - -`amphiphilic_pseudo_amino_acid_composition` has no `normalized` option: its shared denominator 1 + w·Στ uses correlation factors that can be negative, so its outputs have no fixed bound. Bounds based on theory keep features comparable across datasets; to scale to the range actually seen in a training set, fit a min–max scaler (e.g. scikit-learn's `MinMaxScaler`) on that set instead. - ---- - ## v1.5.0 Descriptors Descriptors added in v1.5.0. Each lists its formula, output shape, parameters and primary reference. diff --git a/README.md b/README.md index 851e670..e792a5f 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,6 @@ The descriptors available in `protpy` include: df = protpy.all_descriptors(protein_seq, descriptor_set="extended") ``` -Most scalar, autocorrelation, CTD and sequence-order descriptors also take `normalized=True`, which rescales their output to [0, 1] with the descriptor's theoretical bounds and adds a `_norm` column suffix (see [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md#normalised-output-normalizedtrue)). - More detail of each descriptor is listed in the markdown file: [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md) diff --git a/docs/usage.rst b/docs/usage.rst index 40d935a..f75df39 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -410,70 +410,6 @@ Calculate **Quasi Sequence Order (QSO)**: # QSO_Grant1 QSO_Grant2 QSO_Grant3 ... # 0.123287 0.079967 0.04332 ... -Normalised Output ------------------ - -The descriptors in the table below take an opt-in ``normalized=False`` parameter. With ``normalized=True`` -every output is rescaled to [0, 1] using the descriptor's theoretical bounds and every column -gets a ``_norm`` suffix, so raw and normalised values never share a column name. Default outputs -are unchanged. - -.. code-block:: python - - protpy.gravy(protein_seq, normalized=True) # GRAVY_norm - protpy.all_descriptors(protein_seq, params={"gravy": {"normalized": True}, - "ctd_": {"normalized": True}}) - -.. list-table:: - :header-rows: 1 - :widths: 35 65 - - * - Descriptor - - Rescaling - * - ``gravy`` - - between the scale's min and max residue value (GRAVY is a mean) - * - ``instability_index`` - - [10·min(DIWV, 0), 10·max(DIWV, 0)] - * - ``isoelectric_point`` - - pI / 14 (the searched pH range) - * - ``molecular_weight`` - - between the all-Gly and all-Trp sequences of the same length, i.e. (mean residue mass − min) / (max − min) - * - ``charge_distribution`` - - positive / (L·f₊), negative / (L·f₋), net mapped from [−L·f₋, L·f₊]; f₊, f₋ are the charges of the most basic (highest pKa) and most acidic (lowest pKa) residue at that pH and pKa set - * - ``secondary_structure_propensity`` - - each column between its table's min and max propensity - * - ``aliphatic_index`` - - / 390 (all-Ile/Leu) - * - ``extinction_coefficient`` - - coefficients / (5500·L) (all-Trp); absorbances / (5500 / (204.228 − 18.015)) - * - ``boman_index`` - - between −max and −min of the solubility scale - * - ``shannon_entropy`` - - / log₂20 - * - ``hydrophobic_moment`` - - / max abs(H) of the scale (a window moment cannot exceed it) - * - ``pseudo_amino_acid_composition`` - - / 100 (non-negative percentages summing to 100) - * - ``moreaubroto_autocorrelation`` - - between min and max of P(a)·P(b) over all residue pairs (including 0 for lags without pairs) - * - ``moran_autocorrelation`` - - from [−N/(N−d), N/(N−d)] (Cauchy–Schwarz); 0.5 = no autocorrelation - * - ``geary_autocorrelation`` - - / (2(N−1)/(N−d)) - * - ``ctd_composition``, ``ctd_transition`` - - already fractions in [0, 1]; unchanged apart from the ``_norm`` suffix - * - ``ctd_distribution``, ``ctd_`` - - distribution / 100; composition and transition unchanged - * - ``sequence_order_coupling_number_`` (float), ``sequence_order_coupling_number``, ``sequence_order_coupling_number_all`` - - τ_d / ((N−d)·max(distance)²) - * - ``quasi_sequence_order``, ``quasi_sequence_order_all`` - - first 20 components / 100 (percentages over 1 + w·Στ ≥ 1); lag components unchanged (always < 1) - -``amphiphilic_pseudo_amino_acid_composition`` has no ``normalized`` option: its denominator -1 + w·Στ can approach zero or become negative, so its outputs have no fixed bound. To scale features -to the range seen in a training set instead, fit a min-max scaler (e.g. scikit-learn's -``MinMaxScaler``) on that set. - Encoding All Descriptors at Once --------------------------------- diff --git a/protpy/_helpers.py b/protpy/_helpers.py index 0bab9eb..ef9cf46 100644 --- a/protpy/_helpers.py +++ b/protpy/_helpers.py @@ -151,31 +151,3 @@ def _to_df(values: list, columns: list[str]) -> pd.DataFrame: values = [round(float(v), 3) if isinstance(v, (float, np.floating)) else int(v) if isinstance(v, (bool, np.bool_, np.integer)) else v for v in values] return pd.DataFrame([values], columns=columns) - - -def _minmax(df: pd.DataFrame, lo, hi) -> pd.DataFrame: - """ - Rescale a 1 x N descriptor DataFrame to [0, 1] with the descriptor's theoretical bounds - (scalars or per-column arrays) and suffix every column with ``_norm`` so normalised and - raw outputs can never share a column. Values are clipped to [0, 1] to absorb the 3 dp - rounding of the raw values, and rounded to 6 dp. - """ - lo = np.broadcast_to(np.asarray(lo, dtype=float), (df.shape[1],)) - hi = np.broadcast_to(np.asarray(hi, dtype=float), (df.shape[1],)) - span = hi - lo - raw = df.iloc[0].to_numpy(dtype=float) - scaled = np.divide(raw - lo, span, out=np.zeros_like(raw), where=span > 0) - return pd.DataFrame([np.round(_unit_clip(scaled), 6)], columns=[f"{c}_norm" for c in df.columns]) - - -def _unit_clip(values: np.ndarray) -> np.ndarray: - """Clip to [0, 1]; only ever absorbs the rounding of raw values (tests disable it to check the bounds).""" - return np.clip(values, 0.0, 1.0) - - -def _scale_range(scale: str | dict) -> tuple[float, float]: - """Minimum and maximum residue value of a single-value scale over the 20 amino acids.""" - values = _property_matrix("".join(amino_acids), scale) - if values.shape[1] != 1: - raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") - return float(values.min()), float(values.max()) diff --git a/protpy/autocorrelation.py b/protpy/autocorrelation.py index 53d06e2..e959aa1 100644 --- a/protpy/autocorrelation.py +++ b/protpy/autocorrelation.py @@ -25,14 +25,12 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int -from ._helpers import _minmax ############################### MoreauBroto Autocorrelation ############################### def moreaubroto_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, - normalized: bool = False) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: """ Calculate MoreauBrotoAuto Autocorrelation (MBAuto) descriptor for sequence. Autocorrelation descriptors are a class of topological descriptors, @@ -70,16 +68,7 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the 8 standard AAIndex properties listed above. :normalize: bool (default=True) - standardise each property (zero mean, unit standard deviation over the 20 - amino acids) before the calculation. This does not bound the output; use - ``normalized`` for [0, 1] output. - :normalized: bool (default=False) - rescale every value to [0, 1] with its theoretical bounds (see below); - columns get a ``_norm`` suffix. Independent of ``normalize``, which - standardises the property values before the calculation. - Bounds: the minimum and maximum product P(a) x P(b) over all amino-acid - pairs (extended to include 0, the value of lags with no residue pairs), - since each value is a mean of such products. + rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. Returns ======= @@ -117,22 +106,13 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, moreaubroto_autocorrelation_df = pd.DataFrame([list(moreaubroto_autocorrelation.values())], columns=list(moreaubroto_autocorrelation.keys())) - if normalized: - lo, hi = [], [] - for key in aai_properties: - products = [aai_properties[key][a] * aai_properties[key][b] for a in amino_acids for b in amino_acids] - #include 0: lags with no residue pairs are reported as 0 - lo += [min(min(products), 0.0)] * lag - hi += [max(max(products), 0.0)] * lag - return _minmax(moreaubroto_autocorrelation_df, lo, hi) return moreaubroto_autocorrelation_df ################################## Moran Autocorrelation ################################## def moran_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, - normalized: bool = False) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: """ Refer to MBAuto docstring for autocorrelation explanation. Moran autocorrelation (MAuto) utilizes property deviations from the @@ -147,15 +127,7 @@ def moran_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - standardise each property (zero mean, unit standard deviation over the 20 - amino acids) before the calculation. This does not bound the output; use - ``normalized`` for [0, 1] output. - :normalized: bool (default=False) - rescale every value to [0, 1] with its theoretical bounds (see below); - columns get a ``_norm`` suffix. Independent of ``normalize``, which - standardises the property values before the calculation. - Bounds: abs(I(d)) <= N / (N - d) by the Cauchy-Schwarz inequality, so I is - mapped from [-N/(N-d), N/(N-d)]; 0.5 means no autocorrelation. + rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. Returns ======= @@ -202,18 +174,13 @@ def moran_autocorrelation(sequence: str, lag: int = 30, moran_autocorrelation_df = pd.DataFrame([list(moran_autocorrelation.values())], columns=list(moran_autocorrelation.keys())) - if normalized: - N = len(sequence) - bound = [N / (N - i) if N > i else 1.0 for i in range(1, lag + 1)] * len(aai_properties) - return _minmax(moran_autocorrelation_df, [-b for b in bound], bound) return moran_autocorrelation_df ################################## Geary Autocorrelation ################################## def geary_autocorrelation(sequence: str, lag: int = 30, properties: list[str] = ["CIDH920105", "BHAR880101", "CHAM820101", "CHAM820102", - "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True, - normalized: bool = False) -> pd.DataFrame: + "CHOC760101", "BIGC670101", "CHAM810101", "DAYM780201"], normalize: bool = True) -> pd.DataFrame: """ Refer to MBAuto docstring for autocorrelation description. Geary Autocorrelation (GAuto) utilizes the square-difference of property @@ -228,15 +195,7 @@ def geary_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - standardise each property (zero mean, unit standard deviation over the 20 - amino acids) before the calculation. This does not bound the output; use - ``normalized`` for [0, 1] output. - :normalized: bool (default=False) - rescale every value to [0, 1] with its theoretical bounds (see below); - columns get a ``_norm`` suffix. Independent of ``normalize``, which - standardises the property values before the calculation. - Bounds: 0 <= C(d) <= 2(N - 1) / (N - d), since each squared difference is at - most twice the sum of the two squared deviations. + rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. Returns ======= @@ -279,10 +238,6 @@ def geary_autocorrelation(sequence: str, lag: int = 30, geary_autocorrelation_df = pd.DataFrame([list(geary_autocorrelation.values())], columns=list(geary_autocorrelation.keys())) - if normalized: - N = len(sequence) - bound = [2 * (N - 1) / (N - i) if N > i else 1.0 for i in range(1, lag + 1)] * len(aai_properties) - return _minmax(geary_autocorrelation_df, 0.0, bound) return geary_autocorrelation_df def _get_properties(properties: list[str] | str, normalize: bool) -> dict[str, dict[str, float]]: diff --git a/protpy/composition.py b/protpy/composition.py index 2d4eb49..73eecac 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -98,8 +98,7 @@ class prediction: new methods of statistical classification. Proc Int Conf """ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight -from ._helpers import (_load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df, _property_matrix, - _minmax, _scale_range) +from ._helpers import _load_json_table, _read_json, _gapped_pair_counts, _kmer_counts, _to_df, _property_matrix #physicochemical values for all amino acids for hydrophobicity, hydrophilicty and residue mass #values taken from http://www.csbio.sjtu.edu.cn/bioinf/PseAAC/ParaValue.htm @@ -320,7 +319,7 @@ def tripeptide_composition(sequence: str) -> pd.DataFrame: "W": -0.9, "Y": -1.3 } -def gravy(sequence: str, scale: str = "kyte_doolittle", normalized: bool = False) -> pd.DataFrame: +def gravy(sequence: str, scale: str = "kyte_doolittle") -> pd.DataFrame: """ Calculate the Grand Average of Hydropathy (GRAVY) for a protein sequence. GRAVY is the sum of the Kyte-Doolittle hydropathy values of all residues @@ -341,9 +340,6 @@ def gravy(sequence: str, scale: str = "kyte_doolittle", normalized: bool = False protein sequence. :scale: str (default="kyte_doolittle") hydrophobicity scale to average. - :normalized: bool (default=False) - rescale to [0, 1] with the scale's minimum and maximum residue values - (GRAVY is a mean, so it lies between them); columns get a ``_norm`` suffix. Returns ======= @@ -358,8 +354,7 @@ def gravy(sequence: str, scale: str = "kyte_doolittle", normalized: bool = False values = _property_matrix(sequence, scale) if values.shape[1] != 1: raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") - df = _to_df([values.mean()], [f"GRAVY_{scale}"]) - return _minmax(df, *_scale_range(scale)) if normalized else df + return _to_df([values.mean()], [f"GRAVY_{scale}"]) #sum Kyte-Doolittle hydropathy values and divide by sequence length gravy_score = round(sum(kyte_doolittle_[aa] for aa in sequence) / len(sequence), 3) @@ -367,8 +362,6 @@ def gravy(sequence: str, scale: str = "kyte_doolittle", normalized: bool = False #transform to DataFrame gravy_df = pd.DataFrame([[gravy_score]], columns=["GRAVY"]) - if normalized: - return _minmax(gravy_df, min(kyte_doolittle_.values()), max(kyte_doolittle_.values())) return gravy_df ################################## Aromaticity ############################################ @@ -472,7 +465,7 @@ def aromaticity(sequence: str) -> pd.DataFrame: "EP": 20.26,"EA": 1.0, "EI": 1.0, "EL": 1.0, "EG": 1.0, "EE": 20.26 } -def instability_index(sequence: str, normalized: bool = False) -> pd.DataFrame: +def instability_index(sequence: str) -> pd.DataFrame: """ Calculate the Instability Index (II) of a protein sequence. The instability index is a measure of in vivo stability, computed as a weighted sum of @@ -487,9 +480,6 @@ def instability_index(sequence: str, normalized: bool = False) -> pd.DataFrame: ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale to [0, 1] with the bounds 10 x min(DIWV, 0) and 10 x max(DIWV, 0) - (II = 10 x sum of L - 1 dipeptide weights / L); column ``InstabilityIndex_norm``. Returns ======= @@ -506,9 +496,6 @@ def instability_index(sequence: str, normalized: bool = False) -> pd.DataFrame: #transform to DataFrame instability_index_df = pd.DataFrame([[ii]], columns=["InstabilityIndex"]) - if normalized: - weights = list(diwv_.values()) + [1.0] - return _minmax(instability_index_df, 10 * min(min(weights), 0), 10 * max(max(weights), 0)) return instability_index_df ################################# Isoelectric Point ###################################### @@ -526,7 +513,7 @@ def _pka_set(pka_scale: str) -> dict: raise ValueError(f"Invalid pka_scale '{pka_scale}'. Valid scales are: {list(sets)}.") return sets[pka_scale] -def isoelectric_point(sequence: str, pka_scale: str = "protpy", normalized: bool = False) -> pd.DataFrame: +def isoelectric_point(sequence: str, pka_scale: str = "protpy") -> pd.DataFrame: """ Calculate the theoretical Isoelectric Point (pI) of a protein sequence. pI is the pH at which the net charge of the protein is zero. It is computed @@ -545,9 +532,6 @@ def isoelectric_point(sequence: str, pka_scale: str = "protpy", normalized: bool protein sequence. :pka_scale: str (default="protpy") pKa value set: "protpy", "emboss", "bjellqvist" or "ipc_protein". - :normalized: bool (default=False) - rescale the pI from the searched pH range 0-14 to [0, 1] (pI / 14); - column ``IsoelectricPoint_norm``. Returns ======= @@ -593,8 +577,6 @@ def _net_charge(ph: float) -> float: #transform to DataFrame isoelectric_point_df = pd.DataFrame([[round(ph, 3)]], columns=["IsoelectricPoint"]) - if normalized: - return _minmax(isoelectric_point_df, 0.0, 14.0) return isoelectric_point_df ############################### Molecular Weight ########################################## @@ -607,7 +589,7 @@ def _net_charge(ph: float) -> float: "S": 105.093, "T": 119.119, "V": 117.148, "W": 204.228, "Y": 181.191 } -def molecular_weight(sequence: str, normalized: bool = False) -> pd.DataFrame: +def molecular_weight(sequence: str) -> pd.DataFrame: """ Calculate the Molecular Weight (MW) of a protein sequence. MW is computed as the sum of average residue masses minus one water molecule per peptide @@ -619,11 +601,6 @@ def molecular_weight(sequence: str, normalized: bool = False) -> pd.DataFrame: ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale to [0, 1] between the lightest (all-Gly) and heaviest (all-Trp) - sequences of the same length, which reduces to - (mean residue mass - min mass) / (max mass - min mass); column - ``MolecularWeight_norm``. Returns ======= @@ -637,17 +614,11 @@ def molecular_weight(sequence: str, normalized: bool = False) -> pd.DataFrame: mw = sum(residue_molecular_weight_[aa] for aa in sequence) - (len(sequence) - 1) * 18.015 molecular_weight_df = pd.DataFrame([[round(mw, 3)]], columns=["MolecularWeight"]) - if normalized: - L = len(sequence) - lightest = L * min(residue_molecular_weight_.values()) - (L - 1) * 18.015 - heaviest = L * max(residue_molecular_weight_.values()) - (L - 1) * 18.015 - return _minmax(molecular_weight_df, lightest, heaviest) return molecular_weight_df ############################## Charge Distribution ######################################## -def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy", - normalized: bool = False) -> pd.DataFrame: +def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy") -> pd.DataFrame: """ Calculate Charge Distribution of a protein sequence at a given pH. Positive charge is contributed by Lys (K), Arg (R) and His (H); negative charge by @@ -666,11 +637,6 @@ def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy :pka_scale: str (default="protpy") pKa value set for K, R, H, D and E: "protpy", "emboss", "bjellqvist" or "ipc_protein" (see ``isoelectric_point``). Termini are not included. - :normalized: bool (default=False) - rescale to [0, 1] at the given pH: PositiveCharge / (L x f+), - NegativeCharge / (L x f-) and (NetCharge + L x f-) / (L x (f+ + f-)), where - f+ is the charge of the most basic residue (highest pKa) and f- that of the - most acidic (lowest pKa); columns get a ``_norm`` suffix. Returns ======= @@ -696,11 +662,6 @@ def charge_distribution(sequence: str, ph: float = 7.4, pka_scale: str = "protpy columns=["PositiveCharge", "NegativeCharge", "NetCharge"] ) - if normalized: - L = len(sequence) - f_pos = max(1 / (1 + 10 ** (ph - pka_charged_[aa])) for aa in ("K", "R", "H")) - f_neg = max(1 / (1 + 10 ** (pka_charged_[aa] - ph)) for aa in ("D", "E")) - return _minmax(charge_distribution_df, [0, 0, -L * f_neg], [L * f_pos, L * f_neg, L * f_pos]) return charge_distribution_df ####################### Hydrophobic/Polar/Charged Composition ############################ @@ -764,7 +725,7 @@ def hydrophobic_polar_charged_composition(sequence: str) -> pd.DataFrame: "R": 1.24, "S": 1.33, "T": 1.03, "V": 0.50, "W": 0.99, "Y": 1.29 } -def secondary_structure_propensity(sequence: str, normalized: bool = False) -> pd.DataFrame: +def secondary_structure_propensity(sequence: str) -> pd.DataFrame: """ Calculate Secondary Structure Propensity of a protein sequence. Each residue is assigned Chou-Fasman propensity values for alpha-helix (H), beta-sheet (E) @@ -775,9 +736,6 @@ def secondary_structure_propensity(sequence: str, normalized: bool = False) -> p ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale each column to [0, 1] with the minimum and maximum residue - propensity of its table; columns get a ``_norm`` suffix. Returns ======= @@ -795,9 +753,6 @@ def secondary_structure_propensity(sequence: str, normalized: bool = False) -> p ssp_df = pd.DataFrame([[h, e, c]], columns=["Helix", "Sheet", "Coil"]) - if normalized: - tables = (helix_propensity_, sheet_propensity_, coil_propensity_) - return _minmax(ssp_df, [min(t.values()) for t in tables], [max(t.values()) for t in tables]) return ssp_df ################################# k-mer Composition ###################################### @@ -1044,7 +999,7 @@ def _aa_class(aa): ################################## Aliphatic Index ######################################## -def aliphatic_index(sequence: str, normalized: bool = False) -> pd.DataFrame: +def aliphatic_index(sequence: str) -> pd.DataFrame: """ Calculate the Aliphatic Index (AI) of a protein sequence. The aliphatic index is a measure of thermostability, defined as the relative volume @@ -1059,9 +1014,6 @@ def aliphatic_index(sequence: str, normalized: bool = False) -> pd.DataFrame: ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale to [0, 1] by the maximum of 390 (an all-Ile/Leu sequence); - column ``AliphaticIndex_norm``. Returns ======= @@ -1083,13 +1035,11 @@ def aliphatic_index(sequence: str, normalized: bool = False) -> pd.DataFrame: aliphatic_index_df = pd.DataFrame([[ai]], columns=["AliphaticIndex"]) - if normalized: - return _minmax(aliphatic_index_df, 0.0, 390.0) return aliphatic_index_df ############################### Extinction Coefficient #################################### -def extinction_coefficient(sequence: str, absorbance: bool = False, normalized: bool = False) -> pd.DataFrame: +def extinction_coefficient(sequence: str, absorbance: bool = False) -> pd.DataFrame: """ Calculate the molar Extinction Coefficient of a protein sequence at 280 nm. The coefficient is derived from the number of Trp (W), Tyr (Y) and @@ -1109,10 +1059,6 @@ def extinction_coefficient(sequence: str, absorbance: bool = False, normalized: protein sequence. :absorbance: bool (default=False) also return ``Abs01_Reduced`` and ``Abs01_Oxidized``. - :normalized: bool (default=False) - rescale to [0, 1]: coefficients by the all-Trp maximum 5500 x L, and - absorbances by the per-residue maximum 5500 / (204.228 - 18.015) (Trp's - coefficient over its residue mass); columns get a ``_norm`` suffix. Returns ======= @@ -1142,9 +1088,6 @@ def extinction_coefficient(sequence: str, absorbance: bool = False, normalized: extinction_coefficient_df["Abs01_Reduced"] = round(ec_reduced / mw, 3) extinction_coefficient_df["Abs01_Oxidized"] = round(ec_oxidized / mw, 3) - if normalized: - hi = [5500 * len(sequence)] * 2 + [5500 / (residue_molecular_weight_["W"] - 18.015)] * (2 if absorbance else 0) - return _minmax(extinction_coefficient_df, 0.0, hi) return extinction_coefficient_df ################################ Boman Index ############################################## @@ -1157,7 +1100,7 @@ def extinction_coefficient(sequence: str, absorbance: bool = False, normalized: "W": 2.33, "Y": -0.14 } -def boman_index(sequence: str, normalized: bool = False) -> pd.DataFrame: +def boman_index(sequence: str) -> pd.DataFrame: """ Calculate the Boman Index (potential protein interaction index) for a protein sequence. The index is the sum of the solubility values of @@ -1173,9 +1116,6 @@ def boman_index(sequence: str, normalized: bool = False) -> pd.DataFrame: ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale to [0, 1] between -max and -min of the solubility scale - (BI is a negated mean); column ``BomanIndex_norm``. Returns ======= @@ -1190,8 +1130,6 @@ def boman_index(sequence: str, normalized: bool = False) -> pd.DataFrame: boman_index_df = pd.DataFrame([[bi]], columns=["BomanIndex"]) - if normalized: - return _minmax(boman_index_df, -max(boman_scale_.values()), -min(boman_scale_.values())) return boman_index_df ############################### Aggregation Propensity #################################### @@ -1254,7 +1192,7 @@ def aggregation_propensity(sequence: str, window: int = 5, hydrophobicity_thresh ################################## Shannon Entropy ######################################## -def shannon_entropy(sequence: str, normalized: bool = False) -> pd.DataFrame: +def shannon_entropy(sequence: str) -> pd.DataFrame: """ Calculate the Shannon Entropy of a protein sequence. Shannon entropy quantifies the diversity of the amino acid composition using the @@ -1273,9 +1211,6 @@ def shannon_entropy(sequence: str, normalized: bool = False) -> pd.DataFrame: ========== :sequence: str protein sequence. - :normalized: bool (default=False) - rescale to [0, 1] by the maximum log2(20) bits; column - ``ShannonEntropy_norm``. Returns ======= @@ -1300,14 +1235,12 @@ def shannon_entropy(sequence: str, normalized: bool = False) -> pd.DataFrame: shannon_entropy_df = pd.DataFrame([[round(entropy, 3)]], columns=["ShannonEntropy"]) - if normalized: - return _minmax(shannon_entropy_df, 0.0, math.log2(20)) return shannon_entropy_df ############################### Hydrophobic Moment ######################################## def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100, - scale: str | None = None, normalized: bool = False) -> pd.DataFrame: + scale: str | None = None) -> pd.DataFrame: """ Calculate the mean and maximum Hydrophobic Moment of a protein sequence. The hydrophobic moment measures the amphiphilicity of a helical segment — @@ -1338,9 +1271,6 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100 residue rotation angle in degrees, or "helix" (100) / "sheet" (160). :scale: str | None (default=None) hydrophobicity scale; None uses protpy's default table. - :normalized: bool (default=False) - rescale to [0, 1] by the largest absolute value of the hydrophobicity - scale (a window moment cannot exceed it); columns get a ``_norm`` suffix. Returns ======= @@ -1386,14 +1316,11 @@ def hydrophobic_moment(sequence: str, window: int = 11, angle: float | str = 100 columns=["HydrophobicMoment_Mean", "HydrophobicMoment_Max"] ) - if normalized: - return _minmax(hydrophobic_moment_df, 0.0, max(abs(v) for v in h_scale.values())) return hydrophobic_moment_df ############################## Pseudo Amino Acid Composition ############################## -def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float = 0.05, properties: list[str] | None = None, - normalized: bool = False) -> pd.DataFrame: +def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float = 0.05, properties: list[str] | None = None) -> pd.DataFrame: """ Pseudo amino acid composition (PAAComp) combines the vanilla amino acid composition descriptor with additional local features, such as correlation between residues of @@ -1415,9 +1342,6 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float weighting factor. :properties: list list of dicts of physicochemical/structural property values for amino acids. - :normalized: bool (default=False) - rescale to [0, 1] by dividing by 100 (components are non-negative - percentages summing to 100); columns get a ``_norm`` suffix. Returns ======= @@ -1483,8 +1407,6 @@ def pseudo_amino_acid_composition(sequence: str, lamda: int = 30, weight: float psuedo_amino_acid_composition_df = pd.DataFrame([list(psuedo_aac.values())], columns=list(psuedo_aac.keys())) - if normalized: - return _minmax(psuedo_amino_acid_composition_df, 0.0, 100.0) return psuedo_amino_acid_composition_df def sequence_order_correlation_factor(sequence: str, k: int = 1, properties: list[dict[str, float]] | None = None) -> float: diff --git a/protpy/ctd.py b/protpy/ctd.py index 0de16d9..ae3d43a 100644 --- a/protpy/ctd.py +++ b/protpy/ctd.py @@ -11,7 +11,7 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence -from ._helpers import _load_json_table, _minmax +from ._helpers import _load_json_table """ References @@ -110,7 +110,7 @@ def str_to_num(sequence: str, property: dict[str, str]) -> str: ##################################### CTD Composition ##################################### -def ctd_composition(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: +def ctd_composition(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: """ Calculate composition physicochemical/structural descriptor. Composition is determined as the number of amino acids of a particular property @@ -123,9 +123,6 @@ def ctd_composition(sequence: str, property: str = "hydrophobicity", normalized: protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. - :normalized: bool (default=False) - composition values are already fractions in [0, 1]; they are returned - unchanged with a ``_norm`` column suffix, for consistency. Returns ======= @@ -159,13 +156,11 @@ def ctd_composition(sequence: str, property: str = "hydrophobicity", normalized: #transform values and columns to DataFrame ctd_composition_df = pd.DataFrame([list(ctd_composition_.values())], columns=list(ctd_composition_.keys())) - if normalized: - return _minmax(ctd_composition_df, 0.0, 1.0) return ctd_composition_df ##################################### CTD Transition ###################################### -def ctd_transition(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: +def ctd_transition(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: """ Calculate transition physicochemical/structural descriptor. Transition is determined as the number of transitions from a particular property @@ -179,9 +174,6 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity", normalized: protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. - :normalized: bool (default=False) - transition values are already fractions in [0, 1]; they are returned - unchanged with a ``_norm`` column suffix, for consistency. Returns ======= @@ -223,13 +215,11 @@ def ctd_transition(sequence: str, property: str = "hydrophobicity", normalized: #transform values and columns to DataFrame ctd_transition_df = pd.DataFrame([list(ctd_transition_.values())], columns=list(ctd_transition_.keys())) - if normalized: - return _minmax(ctd_transition_df, 0.0, 1.0) return ctd_transition_df #################################### CTD Distribution ##################################### -def ctd_distribution(sequence: str, property: str = "hydrophobicity", normalized: bool = False) -> pd.DataFrame: +def ctd_distribution(sequence: str, property: str = "hydrophobicity") -> pd.DataFrame: """ Calculate distribution physicochemical/structural descriptor. Distribution is the chain length within which the first, 25%, 50%, 75% and 100% of the @@ -242,9 +232,6 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity", normalized protein sequence. :property: str (default="hydrophocity") physicochemical property name to use when calculating descriptor. - :normalized: bool (default=False) - rescale the distribution percentages to [0, 1] (/ 100); columns get a - ``_norm`` suffix. Returns ======= @@ -307,12 +294,10 @@ def ctd_distribution(sequence: str, property: str = "hydrophobicity", normalized #transform values and columns to DataFrame ctd_distribution_df = pd.DataFrame([list(ctd_distribution_.values())], columns=list(ctd_distribution_.keys())) - if normalized: - return _minmax(ctd_distribution_df, 0.0, 100.0) return ctd_distribution_df def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, - property_set: str = "default", normalized: bool = False) -> pd.DataFrame: + property_set: str = "default") -> pd.DataFrame: """ Calculate all Composition, Transition and Distribution (CTD) features of protein sequences. Composition is the number of amino acids of a particular property (e.g., hydrophobicity) @@ -341,9 +326,6 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, property set used when ``all_ctd=True``: "default" (the 7 properties above, 1 x 147) or "ifeature13" (adds the six AAindex hydrophobicity partitions used by iFeature, 13 properties, 1 x 273) [4]. - :normalized: bool (default=False) - rescale to [0, 1] (composition and transition unchanged, distribution - / 100); columns get a ``_norm`` suffix. Returns ======= @@ -364,17 +346,17 @@ def ctd_(sequence: str, property: str = "hydrophobicity", all_ctd: bool = True, #if using single property, calculate each of the CTD descriptors individually if not all_ctd: - comp_df = ctd_composition(sequence, property=property, normalized=normalized) - trans_df = ctd_transition(sequence, property=property, normalized=normalized) - distr_df = ctd_distribution(sequence, property=property, normalized=normalized) + comp_df = ctd_composition(sequence, property=property) + trans_df = ctd_transition(sequence, property=property) + distr_df = ctd_distribution(sequence, property=property) else: #if using all calculable properties, calculate CTD descriptors for each property for prop in ctd_property_sets[property_set]: - comp = ctd_composition(sequence, property=prop, normalized=normalized) + comp = ctd_composition(sequence, property=prop) comp_df = pd.concat([comp_df, comp], axis=1) - trans = ctd_transition(sequence, property=prop, normalized=normalized) + trans = ctd_transition(sequence, property=prop) trans_df = pd.concat([trans_df, trans], axis=1) - distr = ctd_distribution(sequence, property=prop, normalized=normalized) + distr = ctd_distribution(sequence, property=prop) distr_df = pd.concat([distr_df, distr], axis=1) #concatenate all descriptors diff --git a/protpy/encode.py b/protpy/encode.py index 36a01f2..135071d 100644 --- a/protpy/encode.py +++ b/protpy/encode.py @@ -53,15 +53,6 @@ #descriptors that accept extra keyword arguments beyond sequence _PARAMETERISED_DESCRIPTORS: frozenset[str] = frozenset({ - "gravy", - "instability_index", - "isoelectric_point", - "molecular_weight", - "secondary_structure_propensity", - "aliphatic_index", - "extinction_coefficient", - "boman_index", - "shannon_entropy", "charge_distribution", "kmer_composition", "reduced_alphabet_composition", @@ -174,18 +165,6 @@ def all_descriptors( sequence_order_coupling_number_all → lag quasi_sequence_order_all → lag, weight - Every descriptor above, and ``gravy``, ``instability_index``, - ``isoelectric_point``, ``molecular_weight``, - ``secondary_structure_propensity``, ``aliphatic_index``, - ``extinction_coefficient``, ``boman_index`` and ``shannon_entropy``, - also accepts ``normalized=True``, which rescales its output to [0, 1] - and suffixes its columns with ``_norm``:: - - df = protpy.all_descriptors(seq, params={ - 'gravy': {'normalized': True}, - 'ctd_': {'normalized': True}, - }) - plus any v1.5 descriptor with parameters (see ``protpy.get_descriptor_info(name)["parameters"]``), e.g. ``{"kappa": {"blob_sizes": (5,)}, "aggrescan": {"window": 7}}``. @@ -290,24 +269,24 @@ def _p(name: str) -> dict: ("[Composition] Amino acid composition", "amino_acid_composition", lambda: amino_acid_composition(sequence)), ("[Composition] Dipeptide composition", "dipeptide_composition", lambda: dipeptide_composition(sequence)), ("[Composition] Tripeptide composition", "tripeptide_composition", lambda: tripeptide_composition(sequence)), - ("[Composition] GRAVY index", "gravy", lambda: gravy(sequence, **_p("gravy"))), + ("[Composition] GRAVY index", "gravy", lambda: gravy(sequence)), ("[Composition] Aromaticity", "aromaticity", lambda: aromaticity(sequence)), - ("[Composition] Instability index", "instability_index", lambda: instability_index(sequence, **_p("instability_index"))), - ("[Composition] Isoelectric point", "isoelectric_point", lambda: isoelectric_point(sequence, **_p("isoelectric_point"))), - ("[Composition] Molecular weight", "molecular_weight", lambda: molecular_weight(sequence, **_p("molecular_weight"))), + ("[Composition] Instability index", "instability_index", lambda: instability_index(sequence)), + ("[Composition] Isoelectric point", "isoelectric_point", lambda: isoelectric_point(sequence)), + ("[Composition] Molecular weight", "molecular_weight", lambda: molecular_weight(sequence)), ("[Composition] Charge distribution", "charge_distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))), ("[Composition] Hydrophobic / polar / charged comp.", "hydrophobic_polar_charged_composition", lambda: hydrophobic_polar_charged_composition(sequence)), - ("[Composition] Secondary structure propensity", "secondary_structure_propensity", lambda: secondary_structure_propensity(sequence, **_p("secondary_structure_propensity"))), + ("[Composition] Secondary structure propensity", "secondary_structure_propensity", lambda: secondary_structure_propensity(sequence)), *([("[Composition] K-mer composition", "kmer_composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))] if _kmer_k not in (2, 3) else []), ("[Composition] Reduced alphabet composition", "reduced_alphabet_composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))), ("[Composition] Motif composition", "motif_composition", lambda: motif_composition(sequence, **_p("motif_composition"))), - ("[Composition] Aliphatic index", "aliphatic_index", lambda: aliphatic_index(sequence, **_p("aliphatic_index"))), - ("[Composition] Extinction coefficient", "extinction_coefficient", lambda: extinction_coefficient(sequence, **_p("extinction_coefficient"))), - ("[Composition] Boman index", "boman_index", lambda: boman_index(sequence, **_p("boman_index"))), + ("[Composition] Aliphatic index", "aliphatic_index", lambda: aliphatic_index(sequence)), + ("[Composition] Extinction coefficient", "extinction_coefficient", lambda: extinction_coefficient(sequence)), + ("[Composition] Boman index", "boman_index", lambda: boman_index(sequence)), ("[Composition] Aggregation propensity", "aggregation_propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))), ("[Composition] Hydrophobic moment", "hydrophobic_moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))), - ("[Composition] Shannon entropy", "shannon_entropy", lambda: shannon_entropy(sequence, **_p("shannon_entropy"))), + ("[Composition] Shannon entropy", "shannon_entropy", lambda: shannon_entropy(sequence)), ("[Composition] Pseudo amino acid composition", "pseudo_amino_acid_composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))), ("[Composition] Amphiphilic pseudo amino acid comp.", "amphiphilic_pseudo_amino_acid_composition", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))), # ── Autocorrelation ──────────────────────────────────────────────────── diff --git a/protpy/sequence_order.py b/protpy/sequence_order.py index cadc361..1629ec1 100644 --- a/protpy/sequence_order.py +++ b/protpy/sequence_order.py @@ -32,7 +32,6 @@ #list of amino acids from ._constants import amino_acids, _validate_sequence, _validate_int, _validate_weight -from ._helpers import _minmax @functools.lru_cache(maxsize=2) def _load_distance_matrix(name: str) -> dict: @@ -49,8 +48,7 @@ def _load_distance_matrix(name: str) -> dict: ############################# Sequence Order Coupling Number ############################## -def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: str = "schneider-wrede", - normalized: bool = False) -> float: +def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: str = "schneider-wrede") -> float: """ Calculate Sequence Order Coupling Number (SOCN) features for input protein sequence. SOCN computes the dissimilarity between amino acid pairs. The distance between @@ -71,9 +69,6 @@ def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: gap between two amino acids. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". - :normalized: bool (default=False) - rescale to [0, 1] by the maximum possible value (N - d) x max(distance)^2 - (tau sums N - d squared distances); 0 when N <= d. Rounded to 6 dp. Returns ======= @@ -103,13 +98,10 @@ def sequence_order_coupling_number_(sequence: str, d: int = 1, distance_matrix: next_aa = sequence[aa + d] tau = tau + math.pow(distance_matrix_data[current_aa + next_aa], 2) - if normalized: - bound = (len(sequence) - d) * max(distance_matrix_data.values()) ** 2 - return round(tau / bound, 6) if bound > 0 else 0.0 return round(tau, 3) def sequence_order_coupling_number(sequence: str, lag: int = 30, - distance_matrix: str = "schneider-wrede", normalized: bool = False) -> pd.DataFrame: + distance_matrix: str = "schneider-wrede") -> pd.DataFrame: """ Calculate Sequence Order Coupling Number (SOCN) features for input protein sequence. SOCN computes the dissimilarity between amino acid pairs. The distance between @@ -126,9 +118,6 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, maximum gap between 2 amino acids; the protein length should be larger than lag. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". - :normalized: bool (default=False) - rescale every SOCN to [0, 1] as in ``sequence_order_coupling_number_``; - columns get a ``_norm`` suffix. Returns ======= @@ -160,17 +149,17 @@ def sequence_order_coupling_number(sequence: str, lag: int = 30, #iterate over sequence with lag, calculating SOCN using input distance matrix values for i in range(lag): - tau = sequence_order_coupling_number_(sequence, i+1, distance_matrix, normalized=normalized) + tau = sequence_order_coupling_number_(sequence, i+1, distance_matrix) #append SOCN column and value to dict - seq_order[col_prefix + str(i + 1) + ("_norm" if normalized else "")] = tau if normalized else round(tau, 3) + seq_order[col_prefix + str(i + 1)] = round(tau, 3) #transform SOCN data into pandas dataframe sequence_order_df = pd.DataFrame([list(seq_order.values())], columns=list(seq_order.keys())) return sequence_order_df -def sequence_order_coupling_number_all(sequence: str, lag: int = 30, normalized: bool = False) -> pd.DataFrame: +def sequence_order_coupling_number_all(sequence: str, lag: int = 30) -> pd.DataFrame: """ Calculate Sequence Order Coupling Number (SOCN) descriptor values of input protein sequence using both matrices (schneider-wrede & grantham) [3]. The distance between @@ -186,9 +175,6 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30, normalized: protein sequence. :lag: int (default=30) maximum gap between 2 amino acids; the protein length should be larger than lag. - :normalized: bool (default=False) - rescale every SOCN to [0, 1] (see ``sequence_order_coupling_number_``); - columns get a ``_norm`` suffix. Returns ======= @@ -210,10 +196,10 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30, normalized: lag = 30 #calculate SOCN using schneider-wrede distance matrix - socn_schneider = sequence_order_coupling_number(sequence, lag, "schneider-wrede", normalized) + socn_schneider = sequence_order_coupling_number(sequence, lag, "schneider-wrede") #calculate SOCN using grantham distance matrix - socn_grantham = sequence_order_coupling_number(sequence, lag, "grantham", normalized) + socn_grantham = sequence_order_coupling_number(sequence, lag, "grantham") #merge 2 dataframes socn_all = pd.concat([socn_schneider, socn_grantham], axis=1) @@ -223,7 +209,7 @@ def sequence_order_coupling_number_all(sequence: str, lag: int = 30, normalized: ################################## Quasi Sequence Order ################################### def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, - distance_matrix: str = "schneider-wrede", normalized: bool = False) -> pd.DataFrame: + distance_matrix: str = "schneider-wrede") -> pd.DataFrame: """ Calculate Quasi Sequence Order (QSO) features for the protein sequences. The quasi-sequence-order descriptors were proposed by K.C. Chou, et.al. [1]. @@ -245,11 +231,6 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, weighting factor. :distance_matrix: str (default="schneider-wrede") physicochemical distance matrix to use; accepts "schneider-wrede" or "grantham". - :normalized: bool (default=False) - rescale to [0, 1]: the first 20 components (amino-acid percentages over - 1 + w x sum(tau), at most 100) are divided by 100 and the lag components - (w x tau_d / (1 + w x sum(tau)), always below 1) are kept; columns get a - ``_norm`` suffix. Returns ======= @@ -302,13 +283,9 @@ def quasi_sequence_order(sequence: str, lag: int = 30, weight: float = 0.1, quasi_sequence_order_df = pd.DataFrame([list(quasi_sequence_order.values())], columns=list(quasi_sequence_order.keys())) - if normalized: - #first 20 components: amino-acid percentages / (1 + w sum(tau)) <= 100; - #lag components: w tau_d / (1 + w sum(tau)) < 1 - return _minmax(quasi_sequence_order_df, 0.0, [100.0] * 20 + [1.0] * (quasi_sequence_order_df.shape[1] - 20)) return quasi_sequence_order_df -def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1, normalized: bool = False) -> pd.DataFrame: +def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1) -> pd.DataFrame: """ Calculate Quasi Sequence Order features for input protein sequence using both physicochemical distance matrices. Concatenate into one output dataframe. @@ -326,8 +303,6 @@ def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1, maximum gap between 2 amino acids; the protein length should be larger than lag. :weight: float (default=0.1) weighting factor. - :normalized: bool (default=False) - see ``quasi_sequence_order``; columns get a ``_norm`` suffix. Returns ======= @@ -352,11 +327,11 @@ def quasi_sequence_order_all(sequence: str, lag: int = 30, weight: float = 0.1, #calculate quasi seq order using schneider distance matrix quasi_schneider = quasi_sequence_order(sequence, lag, weight=weight, - distance_matrix="schneider-wrede", normalized=normalized) + distance_matrix="schneider-wrede") #calculate quasi seq order using schneider distance matrix quasi_grantham = quasi_sequence_order(sequence, lag, weight=weight, - distance_matrix="grantham", normalized=normalized) + distance_matrix="grantham") #concat quasi sequence order dataframes quasi_sequence_order_all_df = pd.concat([quasi_schneider, quasi_grantham], axis=1) diff --git a/protpy/utils.py b/protpy/utils.py index 8de1303..8957d51 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -77,7 +77,6 @@ "parameters": { "sequence": "str — protein sequence", "scale": "str (default='kyte_doolittle') — single-value scale or AAindex code (column GRAVY_{scale})", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -111,7 +110,6 @@ "abbreviation": "II", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -130,7 +128,6 @@ "parameters": { "sequence": "str — protein sequence", "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -148,7 +145,6 @@ "abbreviation": "MW", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -168,7 +164,6 @@ "sequence": "str — protein sequence", "ph": "float (default=7.4) — pH for charge calculation", "pka_scale": "str (default='protpy') — pKa set: 'protpy', 'emboss', 'bjellqvist' or 'ipc_protein'", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3", "speed": "Fast", @@ -204,7 +199,6 @@ "abbreviation": "SSP", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3", "speed": "Fast", @@ -297,7 +291,6 @@ "abbreviation": "AI", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -316,7 +309,6 @@ "parameters": { "sequence": "str — protein sequence", "absorbance": "bool (default=False) — add Abs01_Reduced / Abs01_Oxidized (A280 of 1 g/L)", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 2 [ExtCoeff_Reduced, ExtCoeff_Oxidized]", "speed": "Fast", @@ -335,7 +327,6 @@ "abbreviation": "BI", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -378,7 +369,6 @@ "window": "int (default=11) — helical window size", "angle": "float | str (default=100) — rotation angle in degrees, or 'helix' (100) / 'sheet' (160)", "scale": "str | None (default=None) — hydrophobicity scale; None keeps protpy's default table", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 2 [HydrophobicMoment_Mean, HydrophobicMoment_Max]", "speed": "Fast", @@ -396,7 +386,6 @@ "abbreviation": "ShannonH", "parameters": { "sequence": "str — protein sequence", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 1", "speed": "Fast", @@ -418,7 +407,6 @@ "lamda": "int (default=30) — rank of correlation (output = 20 + lamda features)", "weight": "float (default=0.05) — weighting factor for correlation terms", "properties": "list[str] (default=[]) — AAIndex1 accession codes; empty = use built-in hydrophobicity/hydrophilicity/mass", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (20 + lamda) (default 1 x 50)", "speed": "Moderate", @@ -461,8 +449,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — standardise property values (z-score) before calculation", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", + "normalize": "bool (default=True) — normalise property values before calculation", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -483,8 +470,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — standardise property values (z-score) before calculation", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", + "normalize": "bool (default=True) — normalise property values", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -505,8 +491,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — standardise property values (z-score) before calculation", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", + "normalize": "bool (default=True) — normalise property values", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -546,7 +531,6 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — one of: hydrophobicity, normalized_vdwv, polarity, charge, secondary_struct, solvent_accessibility, polarizability", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3 (per property)", "speed": "Fast", @@ -565,7 +549,6 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — CTD physicochemical property", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 3 (per property)", "speed": "Fast", @@ -585,7 +568,6 @@ "parameters": { "sequence": "str — protein sequence", "property": "str (default='hydrophobicity') — CTD physicochemical property", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 15 (per property)", "speed": "Fast", @@ -607,7 +589,6 @@ "property": "str (default='hydrophobicity') — CTD property (used when all_ctd=False)", "all_ctd": "bool (default=True) — if True, compute over all 7 properties", "property_set": "str (default='default') — 'default' (7 properties) or 'ifeature13' (13 properties, 1 x 273)", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x 21 (single property) / 1 x 147 (all 7 properties)", "speed": "Fast (single) / Moderate (all)", @@ -629,7 +610,6 @@ "sequence": "str — protein sequence", "d": "int (default=1) — gap between amino acid pairs", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "scalar float", "speed": "Fast", @@ -649,7 +629,6 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap; output features = lag", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x lag (default 1 x 30)", "speed": "Moderate", @@ -668,7 +647,6 @@ "parameters": { "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap; output features = lag × 2", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag × 2) (default 1 x 60)", "speed": "Moderate", @@ -690,7 +668,6 @@ "lag": "int (default=30) — maximum gap; output features = lag + 20", "weight": "float (default=0.1) — weighting factor for SOCN terms", "distance_matrix": "str (default='schneider-wrede') — 'schneider-wrede' or 'grantham'", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x (lag + 20) (default 1 x 50)", "speed": "Moderate", @@ -709,7 +686,6 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — maximum gap", "weight": "float (default=0.1) — weighting factor", - "normalized": "bool (default=False) — rescale outputs to [0, 1] with theoretical bounds; columns get a _norm suffix", }, "output_shape": "1 x ((lag + 20) × 2) (default 1 x 100)", "speed": "Moderate", diff --git a/tests/README.md b/tests/README.md index f883486..a681ed8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -23,7 +23,6 @@ Unit tests * `test_complexity.py` - Wootton-Federhen, linguistic and Lempel-Ziv complexity. * `test_signal.py` - FFT and DCT descriptors. * `test_structural.py` - AGGRESCAN, Uversky/FoldIndex, disorder propensity, TM and signal-peptide features, functional sites, PLAAC (against PLAAC's reference output), antigenicity and cysteine features. -* `test_normalization.py` - `normalized=True`: default outputs unchanged, `_norm` column suffixes, outputs within [0, 1] for random, homopolymer and very short sequences (with clipping disabled, to prove the bounds), bounds reached by extreme sequences, and the option through `all_descriptors(params=...)`. * `test_encode.py` - `all_descriptors`, `encode_dataset` and `read_sequences_from_file`. * `test_descriptor_sets.py` - `descriptor_set`, `include` and `exclude`, and every v1.5.0 descriptor on 1-8 residue sequences. * `test_utils.py` - descriptor registry and metadata functions. diff --git a/tests/test_encode.py b/tests/test_encode.py index b60bedc..a962690 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -268,7 +268,7 @@ def test_all_descriptors_params_validation(self): with self.assertRaises(ValueError): protpy.all_descriptors(self.protein_seq1, params={'conjoint_triad': {}}) with self.assertRaises(ValueError): - protpy.all_descriptors(self.protein_seq1, params={'aromaticity': {}}) + protpy.all_descriptors(self.protein_seq1, params={'gravy': {}}) #3.) params of wrong type raises TypeError with self.assertRaises(TypeError): protpy.all_descriptors(self.protein_seq1, params="lag=15") diff --git a/tests/test_normalization.py b/tests/test_normalization.py deleted file mode 100644 index 06f1a28..0000000 --- a/tests/test_normalization.py +++ /dev/null @@ -1,159 +0,0 @@ -########################################################################### -############ protPy - normalized=True output rescaling tests ######### -########################################################################### - -import inspect -import math -import random -import unittest - -import numpy as np - -import protpy - -SEQ = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDWC" - -#descriptors with a normalized parameter (dataframe outputs) -NORMALIZABLE = [ - "gravy", "instability_index", "isoelectric_point", "molecular_weight", "charge_distribution", - "secondary_structure_propensity", "aliphatic_index", "extinction_coefficient", "boman_index", - "shannon_entropy", "hydrophobic_moment", "pseudo_amino_acid_composition", - "moreaubroto_autocorrelation", "moran_autocorrelation", "geary_autocorrelation", - "ctd_composition", "ctd_transition", "ctd_distribution", "ctd_", - "sequence_order_coupling_number", "sequence_order_coupling_number_all", - "quasi_sequence_order", "quasi_sequence_order_all", -] - - -def _sequences(): - """Random sequences of several lengths plus homopolymers and very short edge cases.""" - rng = random.Random(7) - seqs = ["".join(rng.choice(protpy.amino_acids) for _ in range(n)) for n in (35, 60, 120, 400)] - seqs += [aa * 35 for aa in protpy.amino_acids] - seqs += ["A", "AC", "WWWWW", "KKKKDDDDKKKKDDDD"] - return seqs - - -class ProtpyNormalizationTests(unittest.TestCase): - - def test_default_off_and_unchanged(self): - for name in NORMALIZABLE: - with self.subTest(name=name): - func = getattr(protpy, name) - self.assertIs(inspect.signature(func).parameters["normalized"].default, False) - self.assertTrue(func(SEQ).equals(func(SEQ, normalized=False))) - - def test_columns_suffixed_and_in_unit_range(self): - for name in NORMALIZABLE: - func = getattr(protpy, name) - for seq in _sequences(): - with self.subTest(name=name, seq=seq[:12]): - raw, norm = func(seq), func(seq, normalized=True) - self.assertEqual(list(norm.columns), [f"{c}_norm" for c in raw.columns]) - values = norm.to_numpy(dtype=float) - self.assertFalse(np.isnan(values).any()) - self.assertTrue(((values >= 0) & (values <= 1)).all()) - - def test_bounds_hold_without_clipping(self): - #_minmax clips only to absorb 3 dp rounding of raw values; with clipping disabled every - #value must still lie within rounding distance of [0, 1], proving the bounds are correct - from unittest import mock - with mock.patch("protpy._helpers._unit_clip", side_effect=lambda v: v): - for name in NORMALIZABLE: - for seq in _sequences(): - with self.subTest(name=name, seq=seq[:12]): - values = getattr(protpy, name)(seq, normalized=True).to_numpy(dtype=float) - self.assertTrue(((values >= -1e-3) & (values <= 1 + 1e-3)).all(), values.min()) - - def test_options_combine_with_normalized(self): - #bounds must also hold, unclipped, for every option that changes the raw values - from unittest import mock - cases = [ - ("gravy", {"scale": "hopp_woods"}), ("gravy", {"scale": "KYTJ820101"}), - ("charge_distribution", {"ph": 3.0, "pka_scale": "ipc_protein"}), ("charge_distribution", {"ph": 11.0}), - ("isoelectric_point", {"pka_scale": "bjellqvist"}), - ("extinction_coefficient", {"absorbance": True}), ("hydrophobic_moment", {"scale": "eisenberg", "angle": "sheet"}), - ("hydrophobic_moment", {"window": 3, "angle": 160}), - ("pseudo_amino_acid_composition", {"lamda": 5, "weight": 0.9}), - ("moreaubroto_autocorrelation", {"normalize": False, "lag": 5}), ("moran_autocorrelation", {"normalize": False}), - ("geary_autocorrelation", {"lag": 40}), ("ctd_", {"property_set": "ifeature13"}), - ("ctd_", {"all_ctd": False, "property": "charge"}), - ("quasi_sequence_order", {"weight": 0.9, "lag": 5, "distance_matrix": "grantham"}), - ("sequence_order_coupling_number", {"lag": 10, "distance_matrix": "grantham"}), - ] - with mock.patch("protpy._helpers._unit_clip", side_effect=lambda v: v): - for name, kwargs in cases: - for seq in _sequences(): - with self.subTest(name=name, kwargs=kwargs, seq=seq[:12]): - values = getattr(protpy, name)(seq, normalized=True, **kwargs).to_numpy(dtype=float) - self.assertTrue(((values >= -1e-3) & (values <= 1 + 1e-3)).all()) - - def test_bounds_are_attained(self): - def one(name, seq, col=0, **kw): - return getattr(protpy, name)(seq, normalized=True, **kw).iloc[0, col] - self.assertEqual(one("gravy", "IIII"), 1.0) - self.assertEqual(one("gravy", "RRRR"), 0.0) - self.assertEqual(one("aliphatic_index", "LLLL"), 1.0) - self.assertEqual(one("aliphatic_index", "GGGG"), 0.0) - self.assertEqual(one("shannon_entropy", "".join(protpy.amino_acids)), 1.0) - self.assertEqual(one("shannon_entropy", "AAAA"), 0.0) - self.assertEqual(one("molecular_weight", "WWWW"), 1.0) - self.assertEqual(one("molecular_weight", "GGGG"), 0.0) - self.assertEqual(one("extinction_coefficient", "WWWW"), 1.0) - #A is in hydrophobicity group 2; column 9 is its 100% position (residue L of L) - self.assertEqual(one("ctd_distribution", "A" * 10, col=9), 1.0) - #all-Arg: positive charge equals its maximum; all-Asp: negative charge does - self.assertEqual(one("charge_distribution", "RRRR", col=0), 1.0) - self.assertEqual(one("charge_distribution", "DDDD", col=1), 1.0) - self.assertEqual(one("charge_distribution", "DDDD", col=2), 0.0) - self.assertEqual(one("charge_distribution", "RRRR", col=2), 1.0) - #a constant signal has no autocorrelation: Moran maps to the midpoint - self.assertEqual(one("moran_autocorrelation", "A" * 40), 0.5) - self.assertAlmostEqual(one("pseudo_amino_acid_composition", SEQ) * 100, - protpy.pseudo_amino_acid_composition(SEQ).iloc[0, 0], places=3) - - def test_socn_scalar(self): - raw = protpy.sequence_order_coupling_number_(SEQ, d=2, distance_matrix="grantham") - norm = protpy.sequence_order_coupling_number_(SEQ, d=2, distance_matrix="grantham", normalized=True) - self.assertAlmostEqual(norm, raw / ((len(SEQ) - 2) * 4.226 ** 2), places=4) - self.assertEqual(protpy.sequence_order_coupling_number_("AC", d=5, normalized=True), 0.0) - #"aabb" with d=2 gives only the pairs (a, b), (a, b): a maximally distant pair reaches the bound - dist = protpy.sequence_order._load_distance_matrix("grantham") - a, b = max(dist, key=dist.get) - self.assertEqual(protpy.sequence_order_coupling_number_(a + a + b + b, d=2, distance_matrix="grantham", - normalized=True), 1.0) - - def test_moran_geary_bounds_match_brute_force(self): - #exact theoretical bounds: |I| <= N/(N-d), 0 <= C <= 2(N-1)/(N-d) - rng = random.Random(3) - for _ in range(30): - seq = "".join(rng.choice("AKDW") for _ in range(rng.randint(8, 40))) - for name in ("moran_autocorrelation", "geary_autocorrelation"): - values = getattr(protpy, name)(seq, lag=5, normalized=True).to_numpy(dtype=float) - self.assertTrue(((values >= 0) & (values <= 1)).all(), (name, seq)) - - def test_all_descriptors_params(self): - df = protpy.all_descriptors(SEQ, params={"gravy": {"normalized": True}, "ctd_": {"normalized": True}}, - _show_descriptor_list=False) - self.assertIn("GRAVY_norm", df.columns) - self.assertNotIn("GRAVY", df.columns) - self.assertTrue(all(c.endswith("_norm") for c in df.columns if c.startswith("CTD_"))) - self.assertFalse(df.columns.duplicated().any()) - #default call is unaffected - self.assertIn("GRAVY", protpy.all_descriptors(SEQ, _show_descriptor_list=False).columns) - - def test_encode_dataset_will_not_mix_raw_and_normalized(self): - import os, tempfile - with tempfile.TemporaryDirectory() as tmp: - out = os.path.join(tmp, "enc.csv") - protpy.encode_dataset([SEQ], output_file=out, verbose=False) - with self.assertRaises(ValueError): - protpy.encode_dataset([SEQ], output_file=out, verbose=False, params={"gravy": {"normalized": True}}) - - def test_apaac_has_no_normalized_option(self): - #its denominator 1 + w * sum(tau) can approach zero or go negative, so no fixed bound exists - self.assertNotIn("normalized", inspect.signature(protpy.amphiphilic_pseudo_amino_acid_composition).parameters) - - -if __name__ == "__main__": - unittest.main() From f95c1f17a2ebb28375a0468f8dbd330b82611b3a Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:14:08 +0100 Subject: [PATCH 16/17] Docs: scaling features for machine learning; correct autocorrelation normalize description --- CHANGELOG.md | 2 ++ README.md | 17 +++++++++++++++++ docs/usage.rst | 34 ++++++++++++++++++++++++++++++++++ protpy/autocorrelation.py | 15 +++++++++------ protpy/utils.py | 6 +++--- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfefcf0..9eb267b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Documentation - `docs/conf.py` converts protpy's `:name: type` parameter style to numpy style before napoleon runs. Parameter names, types and defaults now render correctly throughout the API docs; previously napoleon emitted nameless `:param :` fields. The Sphinx build is now warning-free (previously 39 warnings). - `docs/index.rst` lists the v1.5.0 descriptor categories; `docs/api.rst` documents the new modules and `encode_dataset`; `docs/usage.rst` covers `descriptor_set` / `include` / `exclude`; `docs/descriptors.rst` and `DESCRIPTORS.md` have a v1.5.0 section generated from the registry and docstrings. +- New "Scaling features for machine learning" section in `docs/usage.rst` and the README: fit a scikit-learn scaler on the training data rather than rescaling descriptors with fixed theoretical bounds, which compress real values into narrow bands. +- The autocorrelation `normalize` parameter's docs now say what it does: it standardises the property values (z-score); it never rescaled the output to 0–1. - `tests/README.md` lists every test module and test data file; `protpy/data/README.md` describes the new data files. ### Changed diff --git a/README.md b/README.md index e792a5f..cc0edf7 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,23 @@ qso = protpy.quasi_sequence_order(protein_seq, lag=10, weight=0.2, distance_matr +### Scaling features for machine learning + +protpy returns each descriptor on its natural scale (percentages, counts, indices, pH, Da, ...). If your classifier needs features on a common scale such as [0, 1], fit a scaler on your **training** data and reuse it for test and new sequences. This scales every column to the range seen in your data without leaking information from the test set: + +```python +from sklearn.preprocessing import MinMaxScaler + +X_train = protpy.encode_dataset(train_seqs, output_file=None, verbose=False).set_index("sequence_id") +X_test = protpy.encode_dataset(test_seqs, output_file=None, verbose=False).set_index("sequence_id") + +scaler = MinMaxScaler(clip=True).fit(X_train) # fit on training data only +X_train_scaled = scaler.transform(X_train) # every column in [0, 1] +X_test_scaled = scaler.transform(X_test) # clip=True keeps unseen extremes in [0, 1] +``` + +`StandardScaler` is a common alternative, and tree-based models don't need scaling. When cross-validating, put the scaler in a scikit-learn `Pipeline` so it is re-fitted on each training fold. Impute the few `NaN` values that `descriptor_set="extended"` can produce (e.g. with `SimpleImputer`) before scaling. + Documentation ------------- The documentation for `protpy` is hosted on **ReadTheDocs** and is available [here][readthedocs]. diff --git a/docs/usage.rst b/docs/usage.rst index f75df39..bbf1c5b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -518,5 +518,39 @@ A few extended descriptors are ``NaN`` where their method leaves them undefined: ``plaac`` LLR/COREscore for sequences shorter than the core length or without a prion-like domain, and ``half_life`` (E. coli) for an N-terminal proline. +Scaling Features for Machine Learning +------------------------------------- + +protpy returns each descriptor on its natural scale: percentages, counts, indices, pH +values, Da and so on. Many classifiers expect features on a common scale, for example +[0, 1]. Rather than rescaling inside protpy, fit a scaler on your **training** data and +reuse it for validation, test and new sequences. This scales every column (whatever +descriptors you computed) to the range actually seen in your data, and it is the +standard way to avoid leaking information from the test set: + +.. code-block:: python + + from sklearn.preprocessing import MinMaxScaler + + X_train = protpy.encode_dataset(train_seqs, output_file=None, verbose=False).set_index("sequence_id") + X_test = protpy.encode_dataset(test_seqs, output_file=None, verbose=False).set_index("sequence_id") + + scaler = MinMaxScaler(clip=True).fit(X_train) # fit on training data only + X_train_scaled = scaler.transform(X_train) # every column in [0, 1] + X_test_scaled = scaler.transform(X_test) # clip=True keeps unseen extremes in [0, 1] + +Notes: + +* ``StandardScaler`` (zero mean, unit variance) is a common alternative; tree-based models + (random forests, gradient boosting) do not need scaling at all. +* Wrap the scaler and model in a scikit-learn ``Pipeline`` when cross-validating, so the + scaler is re-fitted on each training fold. +* ``descriptor_set="extended"`` can produce a few ``NaN`` values (see above); impute them + first, e.g. with ``sklearn.impute.SimpleImputer``. +* Scaling with each descriptor's theoretical minimum and maximum is generally a poor + substitute: real proteins sit far from those extremes, so values end up compressed into + a narrow band (e.g. instability-index bounds are roughly [-159, 583] while typical values + are 20-60). + .. note:: A demo of the software is available `here `_. \ No newline at end of file diff --git a/protpy/autocorrelation.py b/protpy/autocorrelation.py index e959aa1..1281c37 100644 --- a/protpy/autocorrelation.py +++ b/protpy/autocorrelation.py @@ -45,9 +45,9 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, The MBAuto descriptor is a type of Autocorrelation descriptor that uses the property values as the basis for measurement [2]. Each autocorrelation will generate the number of features depending on the lag value and number of - properties input with total features = lag * number of properties. The output - autocorrelation can also be normalized by setting the normalize parameter to - true, this occurs by default. Using the default 8 properties with default lag + properties input with total features = lag * number of properties. By default + (normalize=True) each property is standardised over the 20 amino acids before the + calculation; the output itself is not bounded. Using the default 8 properties with default lag value of 30, 240 features are generated, the default 8 properties are: AccNo. CIDH920105 - Normalized Average Hydrophobicity Scales @@ -68,7 +68,8 @@ def moreaubroto_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the 8 standard AAIndex properties listed above. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output to [0, 1]. Returns ======= @@ -127,7 +128,8 @@ def moran_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output to [0, 1]. Returns ======= @@ -195,7 +197,8 @@ def geary_autocorrelation(sequence: str, lag: int = 30, :properties: list list of AAI index record codes/accession numbers for the physicochemical properties to use in the calculation. Defaults to the same 8 standard AAIndex properties as MBAuto. :normalize: bool (default=True) - rescale/normalize MoreauBroto Autocorrelation values into range of 0-1. + standardise each property (zero mean, unit standard deviation over the 20 + amino acids) before the calculation. This does not bound the output to [0, 1]. Returns ======= diff --git a/protpy/utils.py b/protpy/utils.py index 8957d51..35e9ddf 100644 --- a/protpy/utils.py +++ b/protpy/utils.py @@ -449,7 +449,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values before calculation", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -470,7 +470,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", @@ -491,7 +491,7 @@ "sequence": "str — protein sequence", "lag": "int (default=30) — max lag between residue pairs", "properties": "list[str] (default=8 AAIndex codes) — AAIndex1 accession numbers", - "normalize": "bool (default=True) — normalise property values", + "normalize": "bool (default=True) — standardise property values (z-score) before calculation", }, "output_shape": "1 x (lag × n_properties) (default 1 x 240)", "speed": "Moderate", From f9663ea5ffb0b15895c4a4394e011b72a3f7cbff Mon Sep 17 00:00:00 2001 From: AJ <46843456+amckenna41@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:26:51 +0100 Subject: [PATCH 17/17] Rewrite docstrings of all new v1.5.0 functions in the package docstring style; define factor-scale functions individually --- CHANGELOG.md | 1 + DESCRIPTORS.md | 752 ++++++++++++++++++++++++-------------- docs/descriptors.rst | 748 +++++++++++++++++++++++-------------- protpy/_helpers.py | 220 +++++++++-- protpy/complexity.py | 88 +++-- protpy/composition.py | 239 ++++++++---- protpy/encode.py | 16 +- protpy/physicochemical.py | 307 +++++++++++----- protpy/scales.py | 418 ++++++++++++++++++--- protpy/signal.py | 72 +++- protpy/structural.py | 354 ++++++++++++------ 11 files changed, 2275 insertions(+), 940 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb267b..0272257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `docs/index.rst` lists the v1.5.0 descriptor categories; `docs/api.rst` documents the new modules and `encode_dataset`; `docs/usage.rst` covers `descriptor_set` / `include` / `exclude`; `docs/descriptors.rst` and `DESCRIPTORS.md` have a v1.5.0 section generated from the registry and docstrings. - New "Scaling features for machine learning" section in `docs/usage.rst` and the README: fit a scikit-learn scaler on the training data rather than rescaling descriptors with fixed theoretical bounds, which compress real values into narrow bands. - The autocorrelation `normalize` parameter's docs now say what it does: it standardises the property values (z-score); it never rescaled the output to 0–1. +- Docstrings of all new v1.5.0 functions (descriptors and internal helpers) follow the package's docstring style: a description with the formula and a numbered reference, then Parameters and Returns sections. The nine factor-scale functions (`vhse`, `z_scales_5`, ...) are now defined individually, each with its own docstring, instead of being generated by a factory. - `tests/README.md` lists every test module and test data file; `protpy/data/README.md` describes the new data files. ### Changed diff --git a/DESCRIPTORS.md b/DESCRIPTORS.md index 57044f6..e17c67a 100644 --- a/DESCRIPTORS.md +++ b/DESCRIPTORS.md @@ -715,14 +715,19 @@ Items from the implementation plan that could not be implemented without unverif Composition of k-spaced amino acid pairs: CKSAAP(XY,g) = N_g(XY) / (L-g-1) x 100 for g = 0..gap. ```text -Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein -sequence. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) -is counted and divided by the number of such pairs: +Calculate Composition of k-Spaced Amino Acid Pairs (CKSAAP) of protein +sequence. CKSAAP describes the frequency of each amino acid pair separated +by g residues within a protein sequence, for each gap g in the range +0..gap. With 400 possible amino acid pairs per gap this creates a +400 * (gap+1) Dimensional vector, calculated as: -CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 +CKSAAP(XY, g) = N_g(XY) / (L - g - 1) * 100 -g = 0 is the dipeptide composition. Values are percentages; a spacing with no -pairs (L - g - 1 <= 0) gives zeros. +where CKSAAP(XY, g) is the composition of the amino acid pair XY with g +residues between X and Y, N_g(XY) is the number of such pairs in the sequence +and L is the length of the sequence. A gap of 0 is the same as the +dipeptide composition. A gap with no pairs (L - g - 1 <= 0) gives values +of 0. ``` ```python @@ -742,15 +747,17 @@ result = protpy.cksaap(protein_seq) Composition of k-spaced amino acid group pairs over the iFeature 5-group alphabet. ```text -Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP). -As CKSAAP, but residues are first mapped to the five iFeature groups: -aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and -uncharged (STCPNQ). +Calculate Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP) of protein +sequence. CKSAAGP is the same as CKSAAP but the amino acids are first mapped +to 5 groups: aliphatic (GAVLMI), aromatic (FYW), positive charge (KRH), +negative charge (DE) and uncharged (STCPNQ). With 25 possible group pairs +per gap this creates a 25 * (gap+1) Dimensional vector, calculated as: -CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 +CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) * 100 -Values are percentages and the 25 values for each spacing sum to 100 -(0 when the spacing has no pairs). +where CKSAAGP(G1G2, g) is the composition of the group pair G1G2 with g +residues between them, N_g(G1G2) is the number of such pairs in the sequence +and L is the length of the sequence. The 25 values for each gap sum to 100. ``` ```python @@ -770,17 +777,24 @@ result = protpy.cksaagp(protein_seq) Dipeptide deviation from expected mean: (Dc - Tm) / sqrt(Tv), with Tm from codon counts. ```text -Calculate the Dipeptide Deviation from Expected Mean (DDE). The observed -dipeptide composition is standardised against the composition expected from -the number of codons coding each residue: +Calculate Dipeptide Deviation from Expected Mean (DDE) of protein sequence. +DDE compares the observed dipeptide composition of a protein sequence with +the composition expected from the number of codons that code for each amino +acid. For each of the 400 dipeptides it is calculated as: Dc(XY) = N(XY) / (L - 1) -Tm(XY) = (C_X / 61) x (C_Y / 61) -Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) + +Tm(XY) = (C_X / 61) * (C_Y / 61) + +Tv(XY) = Tm(XY) * (1 - Tm(XY)) / (L - 1) + DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) -where C_X is the number of sense codons for residue X in the standard genetic -code (61 in total). DDE is a standardised statistic, not a percentage. +where Dc(XY) is the observed composition of dipeptide XY, N(XY) is the +number of XY dipeptides, L is the length of the sequence, Tm(XY) and Tv(XY) +are the theoretical mean and variance, and C_X and C_Y are the number of +codons for amino acids X and Y out of the 61 sense codons. Unlike the +other composition descriptors, DDE values are not percentages. ``` ```python @@ -800,13 +814,17 @@ result = protpy.dde(protein_seq) Adaptive skip dipeptide composition: composition of all ordered pairs (s_i, s_j), i < j. ```text -Calculate the Adaptive Skip Dipeptide Composition (ASDC): the composition -of every ordered residue pair (s_i, s_j) with i < j, over all gaps: +Calculate Adaptive Skip Dipeptide Composition (ASDC) of protein sequence. +ASDC is a modified dipeptide composition that counts every ordered pair of +amino acids (s_i, s_j), where i < j, at all gaps along the sequence, rather +than just adjacent pairs, calculated as: -ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 +ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) * 100 -The denominator is the total number of ordered pairs, L(L-1)/2. Values are -percentages and sum to 100 (zeros for a single residue). +where ASDC(XY) is the composition of the amino acid pair XY, N_g(XY) is the +number of XY pairs separated by g residues, and the denominator is the total +number of ordered pairs in the sequence, L(L-1)/2, where L is the length of +the sequence. The 400 values sum to 100. ``` ```python @@ -826,20 +844,23 @@ result = protpy.asdc(protein_seq) PseAAC of distance-pairs over a reduced cluster profile (cp13/cp14/cp19/cp20). ```text -Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet -(iDNA-Prot|dis). Residues are mapped to the clusters of a cluster -profile cp(n) and the vector is +Calculate Distance Pair (DP) descriptor of protein sequence. The descriptor +incorporates amino acid distance pairs and a reduced amino acid alphabet into +the general pseudo amino acid composition, as used in iDNA-Prot|dis. +The amino acids are first mapped to the clusters of a cluster profile cp(n), +and the descriptor is the composition of the clusters followed by the +composition of cluster pairs at each distance d = 1..rng: -[f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] +DP(c) = N(c) / L * 100 -where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the -composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the -nearest-neighbour pair. This follows Eqs. 5-12 of, giving G + G^2·rng -values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. +DP(c1c2, d) = N_d(c1c2) / (L - d) * 100 -Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of. The -printed cp(14) lists E twice; the stray E in the first cluster is dropped, -the only reading with 14 clusters. +where N(c) is the number of residues in cluster c, N_d(c1c2) is the number of +residue pairs (s_i, s_i+d) whose residues are in clusters c1 and c2, and L is +the length of the sequence. A distance of 1 is the nearest-neighbour pair. +The available cluster profiles are cp13, cp14, cp19 and cp20 (no reduction). +The published cp(14) lists E twice; the extra E in the first cluster is +dropped, as it is the only reading that gives 14 clusters. ``` ```python @@ -859,13 +880,19 @@ result = protpy.distance_pair(protein_seq) Natural vector: count, mean position and normalised second central moment of each amino acid. ```text -Calculate the Natural Vector of a protein sequence. For each amino acid k: +Calculate Natural Vector (NV) of protein sequence. The natural vector +describes the number of each amino acid in a protein sequence together with +the mean and the normalized central moment of their positions, calculated as: + +n_k = number of occurrences of amino acid k -n_k = number of occurrences of k -mu_k = mean (1-based) position of k -D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) +mu_k = sum of the positions of k / n_k -mu_k and D2_k are 0 when k does not occur. Values are not percentages. +D2_k = sum of (position of k - mu_k)^2 / (n_k * L) + +where positions start at 1, L is the length of the sequence and k is each of +the 20 canonical amino acids. mu_k and D2_k are 0 when amino acid k is +not in the sequence. ``` ```python @@ -885,16 +912,18 @@ result = protpy.natural_vector(protein_seq) Percentage of C, H, N, O and S atoms over the residues' free amino-acid formulas. ```text -Calculate the Atomic Composition (ATC) of a protein sequence: the -percentage of C, H, N, O and S atoms, summed over the molecular formulas of -the residues' free amino acids. +Calculate Atomic Composition (ATC) of protein sequence. ATC describes the +percentage of carbon, hydrogen, nitrogen, oxygen and sulphur atoms in a +protein sequence, using the molecular formulas of the free amino acids, +calculated as: -ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 +ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) * 100 -By default free amino-acid formulas are summed without removing water, -matching Pfeature. With ``subtract_water=True``, (L - 1) H2O are removed -for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" --> C4H8N2O3). +where ATC(e) is the atomic composition of element e and N_e is the number of +atoms of element e summed over all residues. By default the formulas of +the free amino acids are summed without removing water, matching Pfeature +. With subtract_water=True, one water molecule is removed for each of the +L - 1 peptide bonds, giving the formula of the peptide (e.g. "GG" -> C4H8N2O3). ``` ```python @@ -916,8 +945,21 @@ result = protpy.atomic_composition(protein_seq) Mean of the Sandberg 5 z-scales over the sequence (optionally per domain). ```text -Calculate the mean z_scales_5 factor-scale descriptor of a protein sequence (Sandberg 5 z-scales). -See ``aa_scale_descriptors`` for the formula. +Calculate Sandberg 5 z-scales (Z5) of protein sequence. The z-scales are the +principal components of a set of physicochemical properties measured for 87 +coded and non-coded amino acids. z1 mainly describes lipophilicity, z2 +steric bulk and polarizability, z3 polarity and charge, and z4 and z5 +electronegativity, heat of formation, electrophilicity and hardness. + +Each amino acid in the sequence is replaced by its 5 z-scales and the mean of +each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -937,8 +979,21 @@ result = protpy.z_scales_5(protein_seq) Mean of the VHSE (hydrophobic, steric, electronic) scales over the sequence (optionally per domain). ```text -Calculate the mean vhse factor-scale descriptor of a protein sequence (principal components score Vectors of Hydrophobic, Steric and Electronic properties). -See ``aa_scale_descriptors`` for the formula. +Calculate Vectors of Hydrophobic, Steric and Electronic properties (VHSE) of +protein sequence. The VHSE scales are principal components score vectors derived +from physicochemical variables of the 20 coded amino acids. VHSE1-2 describe +hydrophobic properties, VHSE3-4 steric properties and VHSE5-8 electronic +properties. + +Each amino acid in the sequence is replaced by its 8 VHSE scales and the mean of +each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -958,8 +1013,21 @@ result = protpy.vhse(protein_seq) Mean of the Atchley factors I-V over the sequence (optionally per domain). ```text -Calculate the mean atchley_factors factor-scale descriptor of a protein sequence (Atchley factors I-V). -See ``aa_scale_descriptors`` for the formula. +Calculate Atchley Factors (AF) of protein sequence. The Atchley factors are 5 +factors obtained from a factor analysis of 54 amino acid attributes. Factor +I mainly reflects polarity, accessibility and hydrophobicity, factor II +propensity for secondary structure, factor III molecular size, factor IV +relative amino acid composition and factor V electrostatic charge. + +Each amino acid in the sequence is replaced by its 5 Atchley factors and the +mean of each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -979,8 +1047,19 @@ result = protpy.atchley_factors(protein_seq) Mean of the topological T-scales over the sequence (optionally per domain). ```text -Calculate the mean t_scales factor-scale descriptor of a protein sequence (topological T-scales). -See ``aa_scale_descriptors`` for the formula. +Calculate Topological Scales (T-scales) of protein sequence. The T-scales are +principal components of topological descriptors of amino acids, describing their +structural and topological characteristics. + +Each amino acid in the sequence is replaced by its 5 T-scales and the mean of +each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1000,8 +1079,19 @@ result = protpy.t_scales(protein_seq) Mean of the structural topology ST-scales over the sequence (optionally per domain). ```text -Calculate the mean st_scales factor-scale descriptor of a protein sequence (structural topology ST-scales). -See ``aa_scale_descriptors`` for the formula. +Calculate Structural Topology Scales (ST-scales) of protein sequence. The ST- +scales are principal components of 3D structural and topological properties of +amino acids. + +Each amino acid in the sequence is replaced by its 8 ST-scales and the mean of +each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1021,8 +1111,19 @@ result = protpy.st_scales(protein_seq) Mean of the ProtFP (PCA, 8 components) over the sequence (optionally per domain). ```text -Calculate the mean protfp factor-scale descriptor of a protein sequence (ProtFP PCA, 8 components). -See ``aa_scale_descriptors`` for the formula. +Calculate ProtFP (ProtFP) of protein sequence. ProtFP is a set of 8 principal +components calculated from a selection of physicochemical properties in the +AAindex database. + +Each amino acid in the sequence is replaced by its 8 ProtFP components and the +mean of each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1042,8 +1143,21 @@ result = protpy.protfp(protein_seq) Mean of the FASGAI factor scales over the sequence (optionally per domain). ```text -Calculate the mean fasgai factor-scale descriptor of a protein sequence (factor analysis scales of generalized amino acid information). -See ``aa_scale_descriptors`` for the formula. +Calculate Factor Analysis Scales of Generalized Amino Acid Information (FASGAI) +of protein sequence. The FASGAI scales are 6 factors obtained from a factor +analysis of amino acid properties, describing hydrophobicity, alpha and turn +propensities, bulky properties, compositional characteristics, local flexibility +and electronic properties. + +Each amino acid in the sequence is replaced by its 6 FASGAI factors and the mean +of each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1063,8 +1177,20 @@ result = protpy.fasgai(protein_seq) Mean of the BLOSUM62-derived indices over the sequence (optionally per domain). ```text -Calculate the mean blosum_indices factor-scale descriptor of a protein sequence (BLOSUM62-derived indices). -See ``aa_scale_descriptors`` for the formula. +Calculate BLOSUM Indices (BLOSUM) of protein sequence. The BLOSUM indices are 10 +factors derived from the BLOSUM62 substitution matrix by principal component +analysis and VARIMAX rotation, so they describe the evolutionary similarity of +amino acids. + +Each amino acid in the sequence is replaced by its 10 BLOSUM indices and the +mean of each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1084,8 +1210,20 @@ result = protpy.blosum_indices(protein_seq) Mean of the MS-WHIM scores over the sequence (optionally per domain). ```text -Calculate the mean ms_whim factor-scale descriptor of a protein sequence (MS-WHIM scores). -See ``aa_scale_descriptors`` for the formula. +Calculate Molecular Surface Weighted Holistic Invariant Molecular Scores (MS- +WHIM) of protein sequence. The MS-WHIM scores are 3 principal components of 3D +descriptors calculated from the molecular electrostatic potential on the surface +of each amino acid. + +Each amino acid in the sequence is replaced by its 3 MS-WHIM scores and the mean +of each is taken over the sequence, calculated as: + +S_j = (1/L) * sum_i z_j(s_i) + +where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale +j for the amino acid at position i and L is the length of the sequence. With +domains > 1 the sequence is split into that many near-equal segments and the +means of each segment are concatenated. ``` ```python @@ -1105,15 +1243,19 @@ result = protpy.ms_whim(protein_seq) Auto/cross-covariance of an amino-acid scale: sum z_j(i) z_k(i+l) / (L-l). ```text -Calculate the auto- and cross-covariance (ACC) transform of an amino-acid -scale. For lag l and scale dimensions j, k: +Calculate Auto Cross Covariance (ACC) of protein sequence. ACC transforms the +amino acid scale values of a sequence into a fixed-length vector that +describes the covariance between the scale dimensions of residues separated +by l positions, for each lag l in the range 1..lag. It is calculated as: -ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) +ACC_jk(l) = sum_{i=1}^{L-l} z_j(s_i) * z_k(s_i+l) / (L - l) -``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto -covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a -protpy factor-scale family (used as published) or an AAindex1 code / list -of codes, which are standardised over the 20 amino acids. +where ACC_jk(l) is the covariance between dimensions j and k at lag l, z_j(s_i) +is the value of dimension j for the amino acid at position i and L is the length +of the sequence. When j = k this is the auto covariance and when j != k the cross +covariance. The scale can be one of the factor-scale families, which are used as +published, or one or more AAindex accession numbers, whose values are first +standardised over the 20 amino acids. ``` ```python @@ -1133,10 +1275,17 @@ result = protpy.auto_cross_covariance(protein_seq) Physicochemical distance transformation: mean squared difference of standardised AAindex properties at lag l. ```text -Calculate the Physicochemical Distance Transformation (PDT). For each -AAindex property P_j (standardised over the 20 amino acids) and lag l: +Calculate Physicochemical Distance Transformation (PDT) of protein sequence. +PDT describes how much a physicochemical property changes between residues +separated by l positions, for each lag l in the range 1..lag. For each +AAindex property it is calculated as: + +PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_i+l))^2 / (L - l) -PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) +where PDT_j(l) is the descriptor value for property j at lag l, P_j(s_i) is the +value of property j for the amino acid at position i, standardised over the 20 +amino acids, and L is the length of the sequence. By default the same 8 AAindex +properties as the autocorrelation descriptors are used. ``` ```python @@ -1158,13 +1307,21 @@ result = protpy.physicochemical_distance_transformation(protein_seq) Fraction of charged residues (FCR), positive/negative fractions and charge asymmetry sigma = NCPR²/FCR. ```text -Calculate the charge fractions of a protein sequence: +Calculate the Charge Fractions (FCR) of protein sequence. The fraction of +charged residues (FCR), the fractions of positive and negative residues and +the overall charge asymmetry (sigma) describe the charge content of a protein +sequence, and are key parameters of the conformations of intrinsically +disordered proteins. They are calculated as: -FCR = (n+ + n-) / L fraction of charged residues -FPlus = n+ / L, FMinus = n- / L -Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) +FCR = (n+ + n-) / L -where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. +FPlus = n+ / L, FMinus = n- / L + +Sigma = NCPR^2 / FCR + +where n+ is the number of positive residues (K, R), n- the number of negative +residues (D, E), L is the length of the sequence and NCPR = (n+ - n-) / L. His +is treated as neutral and sigma is 0 when there are no charged residues. ``` ```python @@ -1184,11 +1341,13 @@ result = protpy.charge_fractions(protein_seq) Net charge per residue: (n+ - n-) / L with K/R positive and D/E negative. ```text -Calculate the Net Charge Per Residue (NCPR) of a protein sequence: +Calculate the Net Charge Per Residue (NCPR) of protein sequence. NCPR is the +net charge of a protein sequence divided by its length, calculated as: NCPR = (n+ - n-) / L -with n+ counting K/R and n- counting D/E. +where n+ is the number of positive residues (K, R), n- the number of negative +residues (D, E) and L is the length of the sequence. ``` ```python @@ -1208,24 +1367,26 @@ result = protpy.ncpr(protein_seq) Das-Pappu kappa: normalised blob charge-asymmetry deviation measuring segregation of opposite charges (0 mixed, 1 segregated). ```text -Calculate the kappa charge-patterning parameter of Das & Pappu. For blob -size g, the sequence is split into the L - g + 1 overlapping blobs and +Calculate the Kappa (Kappa) charge patterning parameter of protein sequence. +Kappa describes how well oppositely charged residues are mixed or segregated +along a protein sequence, which strongly affects the conformations of +intrinsically disordered proteins. The sequence is split into overlapping +blobs of g residues and kappa is calculated as: -sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) -delta_g = mean over blobs of (sigma_b - sigma)^2 -kappa_g = delta_g / delta_max,g +sigma_b = (f+ - f-)^2 / (f+ + f-) -where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the -largest delta over the maximally segregated arrangements of the same -composition (a positive block and a negative block with the neutral residues -distributed over the three surrounding gaps; every distinct arrangement is -evaluated). kappa is the mean of kappa_g -over ``blob_sizes`` (5 and 6 in); 0 means well mixed and 1 fully -segregated. Charges: K, R = +1; D, E = -1; H = 0. +delta_g = mean over all blobs of (sigma_b - sigma)^2 -kappa is reported as 0 (not NaN) when the sequence has no charges or only one -charge type, where oppositely charged residues cannot be segregated. A blob -size longer than the sequence is reduced to the sequence length. +kappa_g = delta_g / delta_max,g + +where sigma_b is the charge asymmetry of a blob, f+ and f- are its fractions of +positive (K, R) and negative (D, E) residues, sigma = NCPR^2 / FCR is the charge +asymmetry of the whole sequence and delta_max,g is the largest delta over the +most segregated arrangements of the same composition. The final kappa is the +mean of kappa_g over the blob sizes (5 and 6 by default). Kappa ranges from 0 +(well mixed) to 1 (fully segregated). It is reported as 0 when the sequence has +no charged residues or only one type of charge, and blobs longer than the +sequence are reduced to its length. ``` ```python @@ -1245,12 +1406,16 @@ result = protpy.kappa(protein_seq) Sequence charge decoration: (1/L) sum_{m>n} q_m q_n sqrt(m-n). ```text -Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh: +Calculate the Sequence Charge Decoration (SCD) of protein sequence. SCD +describes the patterning of charges along a protein sequence, weighting each +pair of charges by the square root of their separation. It is calculated as: -SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} +SCD = (1/L) * sum_{m=2}^{L} sum_{n=1}^{m-1} q_m * q_n * (m - n)^(1/2) -with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values -indicate more segregated opposite charges; uncharged sequences give 0. +where q_m and q_n are the charges of the residues at positions m and n (+1 for +K and R, -1 for D and E, 0 otherwise) and L is the length of the sequence. More +negative values indicate more segregated opposite charges and uncharged +sequences have an SCD of 0. ``` ```python @@ -1270,11 +1435,14 @@ result = protpy.sequence_charge_decoration(protein_seq) Net charge at the given pH divided by molecular weight (charges per Da). ```text -Calculate the charge density of a protein sequence: the net charge at -``ph`` (from ``charge_distribution``) divided by the molecular weight (from -``molecular_weight``), in charges per Da. +Calculate the Charge Density of protein sequence. Charge density is the net +charge of a protein sequence at a given pH divided by its molecular weight, +calculated as: ChargeDensity = NetCharge(pH) / MW + +where NetCharge(pH) is calculated using the charge_distribution function and MW +using the molecular_weight function. ``` ```python @@ -1294,13 +1462,20 @@ result = protpy.charge_density(protein_seq) Summary statistics of a sliding-window hydrophobicity profile (ProtScale-style edge weighting). ```text -Summarise a sliding-window hydrophobicity profile. Each residue is -replaced by its scale value and a window of length ``window`` is slid along -the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` -(ExPASy ProtScale semantics). The profile is reduced to its mean, max, min -and standard deviation, plus the fraction of windows above ``threshold`` -when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). -Windows longer than the sequence use the whole sequence. +Calculate the Hydrophobicity Profile (HP) of protein sequence. Each amino acid +in the sequence is replaced by its value on a hydrophobicity scale and the +values are averaged over a sliding window, giving a profile along the sequence +. The profile is summarised by its mean, maximum, minimum and standard +deviation, and optionally the fraction of windows above a threshold, e.g. 1.6 +for transmembrane segments with the Kyte-Doolittle scale. The window value +is calculated as: + +HP(i) = sum_j w_j * H(s_i+j) / sum_j w_j + +where H is the hydrophobicity scale and w_j are the window weights. With +edge=1.0 all weights are 1 (a plain mean); with edge < 1 the weights fall +linearly from 1 at the centre of the window to edge at its ends, as in ExPASy +ProtScale. Windows longer than the sequence use the whole sequence. ``` ```python @@ -1320,10 +1495,14 @@ result = protpy.hydrophobicity_profile(protein_seq) APD hydrophobic ratio: percentage of L, I, V, F, M, A, C, W residues. ```text -Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) -: the percentage of residues that are L, I, V, F, M, A, C or W. +Calculate the Hydrophobic Ratio of protein sequence. The hydrophobic ratio is +the percentage of hydrophobic residues in a protein sequence, as defined by +the Antimicrobial Peptide Database (APD), calculated as: -HydrophobicRatio = N(LIVFMACW) / L x 100 +HydrophobicRatio = N(LIVFMACW) / L * 100 + +where N(LIVFMACW) is the number of L, I, V, F, M, A, C and W residues and L +is the length of the sequence. ``` ```python @@ -1343,14 +1522,15 @@ result = protpy.hydrophobic_ratio(protein_seq) N-end rule half-life (hours) in mammalian, yeast and E. coli systems from the N-terminal residue. ```text -Estimate the in vivo half-life of a protein from its N-terminal residue by -the N-end rule, as tabulated by ExPASy ProtParam: mammalian -reticulocytes (in vitro), yeast and E. coli (in vivo). - -Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" -in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli -value for an N-terminal Pro is unknown in the source and returned as NaN. -Not applicable to N-terminally modified proteins. +Calculate the estimated Half Life of protein sequence. The half-life is a +prediction of the time it takes for half of the amount of a protein in a cell +to disappear after its synthesis. It is estimated from the N-terminal residue +using the N-end rule, for 3 model systems: mammalian reticulocytes +(in vitro), yeast and E. coli (in vivo), using the values tabulated by ExPASy +ProtParam. Values given as lower bounds in ProtParam (">20 hours" for yeast, +">10 hours" for E. coli) are reported as the bound. The E. coli value for an +N-terminal proline is unknown in ProtParam and returned as NaN. The estimate is +not applicable to N-terminally modified proteins. ``` ```python @@ -1370,11 +1550,15 @@ result = protpy.half_life(protein_seq) Fraction of I, V, Y, W, R, E, L residues (thermophilic adaptation index). ```text -Calculate the IVYWREL index of Zeldovich et al.: the fraction of residues -that are I, V, Y, W, R, E or L, which correlates with optimal growth -temperature. +Calculate the IVYWREL Index of protein sequence. The IVYWREL index is the +fraction of I, V, Y, W, R, E and L residues in a protein sequence, which +correlates with the optimal growth temperature of the organism. It is +calculated as: IVYWREL = N(IVYWREL) / L + +where N(IVYWREL) is the number of I, V, Y, W, R, E and L residues and L is the +length of the sequence. ``` ```python @@ -1394,10 +1578,13 @@ result = protpy.ivywrel_index(protein_seq) Charged versus polar bias: %(D+E+K+R) - %(N+Q+S+T). ```text -Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie, an -indicator of hyperthermostability: +Calculate the Charged versus Polar Bias (CvP) of protein sequence. The CvP bias +is the difference between the percentages of charged and polar residues in a +protein sequence, an indicator of hyperthermostability, calculated as: CvP = %(D + E + K + R) - %(N + Q + S + T) + +where %(X) is the percentage of residues of type X in the sequence. ``` ```python @@ -1417,17 +1604,16 @@ result = protpy.cvp_bias(protein_seq) Summary statistics of the Vihinen average-B-value flexibility profile (window 9, end weight 0.25). ```text -Summarise the flexibility profile of Vihinen et al.. Average normalised -B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged -over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, -0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced -to its mean, max, min and standard deviation. - -The neighbour-dependent parameter sets of (residues with 0, 1 or 2 rigid -neighbours) are not used. Compared with Biopython's -``ProteinAnalysis.flexibility``, which uses the same values and weights, -protpy centres each window on its middle residue and includes the last -window; Biopython takes the residue after the centre and omits the last window. +Calculate the Flexibility (Flex) profile of protein sequence. Each amino acid +in the sequence is replaced by its normalised flexibility parameter (B-value) +and the values are averaged over a sliding window of 9 residues whose end +weights fall linearly to 0.25 (weights 0.25, 0.4375, 0.625, 0.8125, 1, ..., +0.25), as in Vihinen et al.. The profile is summarised by its mean, +maximum, minimum and standard deviation. The average B-values of Vihinen et +al. are used; their parameter sets for residues with 0, 1 or 2 rigid +neighbours are not. Compared with Biopython's ProteinAnalysis.flexibility, +which uses the same values and weights, each window is centred on its middle +residue and the last window is included. ``` ```python @@ -1449,17 +1635,22 @@ result = protpy.flexibility(protein_seq) Wootton-Federhen local complexity (K1, K2) over sliding windows and the SEG-trigger low-complexity fraction. ```text -Calculate Wootton-Federhen local compositional complexity. For every -window of length W with residue counts n_i: +Calculate the Wootton-Federhen Complexity (WF) of protein sequence. The +Wootton-Federhen measures describe the local compositional complexity of a +protein sequence over a sliding window, and are the basis of the SEG program +for finding low-complexity regions. For each window of W residues +they are calculated as: -K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) -K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) +K1 = (1/W) * log_20(W! / prod_i n_i!) -The low-complexity fraction is the fraction of residues covered by at least -one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: -SEG's extension to the K2 extension threshold and its interval merging and -optimisation are not performed. A window longer than the sequence uses the -whole sequence. +K2 = -sum_i (n_i / W) * log2(n_i / W) + +where K1 is the complexity state, K2 is the Shannon entropy of the window in +bits and n_i is the number of residues of amino acid i in the window. The +low-complexity fraction is the fraction of residues covered by at least one +window with K2 <= k2_trigger. This is the trigger stage of SEG only; SEG's +extension and merging of low-complexity segments are not performed. A window +longer than the sequence uses the whole sequence. ``` ```python @@ -1479,15 +1670,17 @@ result = protpy.wootton_federhen_complexity(protein_seq) Linguistic complexity: distinct subwords observed / maximum possible (ratio of sums over word lengths). ```text -Calculate the linguistic complexity (LC) of a protein sequence: the -number of distinct subwords (k-mers) observed, over the maximum number -possible for a sequence of that length over the 20-letter alphabet: +Calculate the Linguistic Complexity (LC) of protein sequence. Linguistic +complexity describes how repetitive a sequence is, as the number of distinct +subwords (k-mers) it contains relative to the maximum possible number for a +sequence of its length. It is calculated as: LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) -where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. -all subwords). This is the ratio-of-sums form, counting all subwords as in -; repetitive sequences have low LC. +where U_k is the number of distinct k-mers in the sequence, L is the length of +the sequence and K is the longest subword length (by default L, i.e. all +subwords). Repetitive sequences have a low LC and random sequences an LC +close to 1. ``` ```python @@ -1507,15 +1700,16 @@ result = protpy.linguistic_complexity(protein_seq) Lempel-Ziv (LZ76) complexity c(n) and its normalised value c(n)·log20(n)/n. ```text -Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence: the -number c(n) of new components found when the sequence is parsed from left to -right, each component being the shortest substring not producible by -copying from the preceding history. The normalised value for a 20-letter -alphabet is +Calculate the Lempel-Ziv Complexity (LZ) of protein sequence. The Lempel-Ziv +(LZ76) complexity is the number of new patterns, c(n), found when a sequence is +read from left to right, each pattern being the shortest substring that cannot +be copied from the preceding part of the sequence. As c(n) grows with the +length of the sequence, a normalised value is also calculated as: -LZ_Normalized = c(n) x log_20(n) / n +LZ_Normalized = c(n) * log_20(n) / n -(0 for a single residue). +where n is the length of the sequence and 20 is the size of the amino acid +alphabet. The normalised value is 0 for a single residue. ``` ```python @@ -1537,16 +1731,19 @@ result = protpy.lempel_ziv_complexity(protein_seq) Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) in equal-width frequency bins. ```text -Calculate a length-invariant binned power spectrum of a property signal, as -in the resonant recognition model of Cosic. Each residue is replaced by -its scale value (default: electron-ion interaction potential, EIIP), the -signal is mean-centred, and its power spectrum is +Calculate Fast Fourier Transform (FFT) descriptors of protein sequence. Each +amino acid in the sequence is replaced by its value on a property scale, by +default the electron-ion interaction potential (EIIP) used in the resonant +recognition model, and the power spectrum of the mean-centred signal is +calculated as: -P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue +P(f) = abs(FFT(x)(f))^2 / L -The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width -bins (the last bin includes 0.5) and the mean power in each bin is returned; -bins containing no frequency are 0. +where x is the property signal, f is the frequency in cycles per residue +(0 to 0.5) and L is the length of the sequence. To give the same number of +features for any sequence length, the frequency range 0-0.5 is split into +n_bins equal-width bins and the mean power in each bin is returned; bins +containing no frequencies are 0. ``` ```python @@ -1566,13 +1763,18 @@ result = protpy.fft_descriptors(protein_seq) First n orthonormal DCT-II coefficients of a mean-centred property signal (default Kyte-Doolittle). ```text -Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II -discrete cosine transform (DCT-II) of a mean-centred property signal: +Calculate Discrete Cosine Transform (DCT) descriptors of protein sequence. +Each amino acid in the sequence is replaced by its value on a property scale, +by default the Kyte-Doolittle hydropathy, and the first n_coeffs coefficients +of the orthonormal type-II DCT of the mean-centred signal are returned. The +DCT compresses a sequence-length signal into a fixed number of coefficients +, calculated as: -X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), -s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 +X_k = s_k * sum_{n=0}^{L-1} x_n * cos(pi * k * (2n + 1) / (2L)) -X_0 is 0 because the signal is mean-centred. +where X_k is coefficient k, x_n is the property value at position n, L is the +length of the sequence, and s_0 = sqrt(1/L) and s_k = sqrt(2/L) for k > 0. X_0 +is 0 because the signal is mean-centred. ``` ```python @@ -1594,29 +1796,27 @@ result = protpy.dct_descriptors(protein_seq) AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot spots: >=5 residues above -0.02, no Pro). ```text -Calculate AGGRESCAN aggregation "hot spot" descriptors. Each residue is -given its experimental aggregation propensity a3v; the a4v profile (AP) is the -a3v mean over a sliding window, assigned to the window's central residue. -Off-centre N- and C-terminal residues take the first and last window values. -A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot -threshold HST = -0.02 and containing no proline. Areas use trapezoidal -integration of the AP with HST as the zero axis. - -Outputs:: - a3vSA mean a3v over the sequence - nHS number of hot spots; NnHS = nHS / L x 100 - AAT area of the AP above HST over the whole sequence; AATr = AAT / L - THSA total hot-spot area (sum of HSA); THSAr = THSA / L - TA total area of the AP (HST as zero axis, signed) - a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 - a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) - HSA_max largest single hot-spot area (0 if none) - -AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and -gives the two extreme residues specific charge-corrected values; neither -rule is given in the published supplementary material, so ``window`` is a -parameter (default 5) and the extreme residues are treated like the other -off-centre residues. +Calculate AGGRESCAN (AGG) aggregation descriptors of protein sequence. +AGGRESCAN predicts aggregation "hot spots", short stretches that promote +protein aggregation, from experimental aggregation propensities (a3v) of the +20 amino acids. The a3v values are averaged over a sliding window, giving +the aggregation profile (a4v), with each window value assigned to its central +residue and the off-centre N- and C-terminal residues given the first and last +window values. A hot spot is a run of 5 or more residues with an a4v above the +hot-spot threshold (HST) of -0.02 and containing no proline. Areas are +calculated by trapezoidal integration of the profile, with HST as the zero axis. + +The 12 output values are: the mean a3v of the sequence (a3vSA); the number of +hot spots (nHS) and the number per 100 residues (NnHS); the area of the profile +above HST (AAT) and per residue (AATr); the total area of the hot spots (THSA) +and per residue (THSAr); the total area of the profile (TA); the sum of a4v +(a4vSS) and per 100 residues (Na4vSS); the mean a4v in the hot spots (a4vAHS, +0 if there are none); and the largest single hot-spot area (HSA_max). + +AGGRESCAN chooses a window of 5, 7, 9 or 11 residues from the sequence length +and gives the two extreme residues specific values; neither rule is given in +the published supplementary material, so the window is a parameter and the +extreme residues are treated like the other off-centre residues. ``` ```python @@ -1636,14 +1836,21 @@ result = protpy.aggrescan(protein_seq) Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.785 - - 1.151. ```text -Calculate the charge-hydropathy descriptors of Uversky et al.: +Calculate the Uversky Charge-Hydropathy (UV) descriptors of protein sequence. +Uversky et al. showed that natively unfolded proteins can be separated from +folded proteins by their mean net charge and mean hydropathy. These are +calculated as: + + = mean of (KD + 4.5) / 9 - = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = abs(n+ - n-) / L (K/R positive, D/E negative) -HQ = 2.785 - - 1.151 + = abs(n+ - n-) / L -The boundary = 2.785 - 1.151 separates natively unfolded from folded -proteins (as restated in); ``UV_Unfolded`` is 1 when HQ < 0. +HQ = 2.785 - - 1.151 + +where KD is the Kyte-Doolittle hydropathy of each residue, n+ and n- are the +numbers of positive (K, R) and negative (D, E) residues and L is the length +of the sequence. HQ is the distance from the boundary = 2.785 - 1.151 +(as restated in); the protein is predicted to be unfolded when HQ < 0. ``` ```python @@ -1663,15 +1870,17 @@ result = protpy.uversky_charge_hydropathy(protein_seq) FoldIndex profile summary: windowed 2.785 - abs() - 1.151 and disordered-residue statistics. ```text -Calculate FoldIndex profile descriptors. For each window of length -``window``: +Calculate the FoldIndex (FI) of protein sequence. FoldIndex applies the +Uversky charge-hydropathy boundary over a sliding window to find disordered +regions of a protein sequence. For each window it is calculated as: FI = 2.785 - abs() - 1.151 -with and as in ``uversky_charge_hydropathy`` but computed over the -window. Residues covered by at least one window with FI < 0 are counted as -disordered. A window longer than the sequence uses the whole sequence, so -FI then equals ``UV_HQ``. +where is the mean Kyte-Doolittle hydropathy of the window rescaled to 0-1 +and is the mean net charge of the window (K, R positive; D, E negative). +Residues covered by at least one window with FI < 0 are counted as disordered. +A window longer than the sequence uses the whole sequence, so FI then equals +UV_HQ from uversky_charge_hydropathy. ``` ```python @@ -1691,10 +1900,18 @@ result = protpy.fold_index(protein_seq) Fractions of disorder- and order-promoting residues and mean TOP-IDP value. ```text -Calculate disorder-propensity descriptors: the fractions of -disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting -(W, C, F, I, Y, V, L, N) residues, and the mean TOP-IDP value -(higher = more disorder-prone). +Calculate the Disorder Propensity (DP) of protein sequence. Intrinsically +disordered proteins are enriched in disorder-promoting residues (A, R, G, Q, +S, P, E, K) and depleted in order-promoting residues (W, C, F, I, Y, V, L, N) +. The descriptor gives the fraction of each, together with the mean value of +the TOP-IDP scale, a scale of amino acid propensity for intrinsic disorder, +calculated as: + +DP_TOP_IDP_mean = sum_i TOP-IDP(s_i) / L + +where TOP-IDP(s_i) is the TOP-IDP value of the amino acid at position i and L +is the length of the sequence. Higher values indicate a higher propensity for +disorder. ``` ```python @@ -1714,20 +1931,17 @@ result = protpy.disorder_propensity(protein_seq) Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, TM fraction, positive-inside bias). ```text -Calculate heuristic transmembrane-segment features from the Kyte-Doolittle -hydropathy profile. Windows of length ``window`` with mean hydropathy -above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative -transmembrane segments; overlapping windows are merged into segments. - -Columns:: - - TM_n_segments number of merged segments - TM_max_KD maximum window hydropathy - TM_frac_in_TM fraction of residues inside a segment - TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment - -This is a hydropathy heuristic, not a topology predictor. +Calculate Transmembrane (TM) segment features of protein sequence. Putative +transmembrane segments are found from the Kyte-Doolittle hydropathy profile: +windows of 19 residues with a mean hydropathy above 1.6 indicate a likely +transmembrane segment, and overlapping windows are merged into segments. +The positive-inside rule states that the cytoplasmic side of a transmembrane +segment is enriched in positive residues. The 4 output values are: the +number of segments; the maximum window hydropathy; the fraction of residues +inside a segment; and the positive-inside bias, the number of K and R residues +in the 15 residues before the first segment minus the number in the 15 residues +after it (0 if there is no segment). This is a hydropathy heuristic and not a +topology predictor. ``` ```python @@ -1747,15 +1961,12 @@ result = protpy.transmembrane_features(protein_seq) N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue hydropathy. ```text -Calculate simple signal-peptide features of the N-terminus: - -Columns:: - - SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) - SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues - -The von Heijne cleavage-site weight matrix is not included. +Calculate Signal Peptide (SP) features of protein sequence. Signal peptides +have a positively charged N-terminal n-region followed by a hydrophobic +h-region. The 2 output values are: the net charge of the first 5 residues +(K and R +1, D and E -1); and the maximum mean Kyte-Doolittle hydropathy over +8-residue windows within the first n_term residues. The von Heijne +cleavage-site weight matrix is not included. ``` ```python @@ -1775,11 +1986,14 @@ result = protpy.signal_peptide_features(protein_seq) Counts and per-100-residue densities of PROSITE N-glycosylation, ER-retention and PTS1 motifs and tryptic sites. ```text -Count functional sequence motifs and their density per 100 residues: -N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal -(PS00014) and peroxisomal targeting signal 1 (PS00342), and tryptic -cleavage sites (after K/R, not before P). Overlapping matches are -counted. These complement the default motifs of ``motif_composition``. +Calculate Functional Site (FS) counts of protein sequence. The number of each +functional sequence motif, and its density per 100 residues, is counted for: +N-glycosylation sites (PROSITE PS00001, N-{P}-[ST]-{P}), the C-terminal +endoplasmic reticulum retention signal (PS00014, [KRHQSA]-[DENQ]-E-L at the +C-terminus), the C-terminal peroxisomal targeting signal 1 (PS00342, +[STAGCN]-[RKH]-[LIVMAFY] at the C-terminus) and tryptic cleavage sites +(after K or R, not before P). Overlapping matches are counted. These motifs +complement the default motifs of motif_composition. ``` ```python @@ -1799,20 +2013,23 @@ result = protpy.functional_site_counts(protein_seq) PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and presence. ```text -Calculate prion-like domain descriptors with the PLAAC model, ported from -the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each -residue has the log-likelihood ratio LLR = ln(f_prion / f_background). - -Columns:: +Calculate the Prion-Like Amino Acid Composition (PLAAC) descriptors of protein +sequence. PLAAC identifies prion-like domains, whose composition resembles +that of yeast prion domains, using the amino acid frequencies of prion domains +and of the S. cerevisiae proteome. Each residue has a log-likelihood ratio: - PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues - PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse - PLAAC_PrD_length length of the prion-like run containing the best core - PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists +LLR(s_i) = ln(f_prion(s_i) / f_background(s_i)) -As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than -``core_length`` / no prion-like run of that length); PrD_length is then 0. +The 4 output values are: the maximum sum of LLR over any window of core_length +residues (PLAAC_LLR); the maximum sum of LLR over a window of core_length +residues lying entirely inside a prion-like region of the two-state hidden +Markov model Viterbi parse (PLAAC_COREscore); the length of the prion-like +region containing that core (PLAAC_PrD_length); and whether the sequence has a +prion-like region of at least core_length residues (PLAAC_has_PrD). As in +PLAAC, PLAAC_LLR and PLAAC_COREscore are NaN when they are undefined (a +sequence shorter than core_length, or no prion-like region of that length). +The parameters are ported from the PLAAC source code (MIT License), using the +S. cerevisiae background (alpha = 1). ``` ```python @@ -1832,11 +2049,14 @@ result = protpy.plaac(protein_seq) Profile statistics of the Parker hydrophilicity and Janin surface scales over 7-residue windows. ```text -Summarise hydrophilicity/surface profiles associated with antigenic sites: -the Parker HPLC hydrophilicity scale averaged over 7-residue windows and -the Janin free energy of transfer from inside to outside of a globular -protein over the same window. Each profile is reduced to its mean, max, -min and standard deviation. +Calculate the Antigenicity and Surface (Antigenicity) profiles of protein +sequence. Antigenic sites tend to be hydrophilic and exposed on the protein +surface. Each amino acid in the sequence is replaced by its value on the +Parker hydrophilicity scale, derived from HPLC peptide retention times, +and on the Janin scale of free energy of transfer from the inside to the +outside of a globular protein. Each is averaged over a sliding window of 7 +residues, as used by Parker et al., and the profile is summarised by its mean, +maximum, minimum and standard deviation. ``` ```python @@ -1856,14 +2076,12 @@ result = protpy.antigenicity_surface(protein_seq) Cysteine count, fraction, parity, maximum disulfides, CxxC motifs and mean spacing. ```text -Calculate cysteine features: - -Cys_count number of C -Cys_frac C / L -Cys_even 1 if the count is even -Cys_max_disulfides floor(count / 2) -Cys_CxxC overlapping C-x-x-C motifs -Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) +Calculate Cysteine (Cys) features of protein sequence. Cysteines form +disulfide bonds and functional motifs such as the CxxC motif of thiol +oxidoreductases. The 6 output values are: the number of cysteines; their +fraction; whether the number is even; the maximum number of disulfide bonds +(the number of cysteines // 2); the number of overlapping C-x-x-C motifs; and +the mean gap between consecutive cysteines (0 if there are fewer than 2). ``` ```python diff --git a/docs/descriptors.rst b/docs/descriptors.rst index b103192..8d43de9 100644 --- a/docs/descriptors.rst +++ b/docs/descriptors.rst @@ -1124,14 +1124,19 @@ Composition of k-spaced amino acid pairs: CKSAAP(XY,g) = N_g(XY) / (L-g-1) x 100 .. code-block:: text - Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein - sequence. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) - is counted and divided by the number of such pairs: + Calculate Composition of k-Spaced Amino Acid Pairs (CKSAAP) of protein + sequence. CKSAAP describes the frequency of each amino acid pair separated + by g residues within a protein sequence, for each gap g in the range + 0..gap. With 400 possible amino acid pairs per gap this creates a + 400 * (gap+1) Dimensional vector, calculated as: - CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 + CKSAAP(XY, g) = N_g(XY) / (L - g - 1) * 100 - g = 0 is the dipeptide composition. Values are percentages; a spacing with no - pairs (L - g - 1 <= 0) gives zeros. + where CKSAAP(XY, g) is the composition of the amino acid pair XY with g + residues between X and Y, N_g(XY) is the number of such pairs in the sequence + and L is the length of the sequence. A gap of 0 is the same as the + dipeptide composition. A gap with no pairs (L - g - 1 <= 0) gives values + of 0. .. code-block:: python @@ -1151,15 +1156,17 @@ Composition of k-spaced amino acid group pairs over the iFeature 5-group alphabe .. code-block:: text - Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP). - As CKSAAP, but residues are first mapped to the five iFeature groups: - aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and - uncharged (STCPNQ). + Calculate Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP) of protein + sequence. CKSAAGP is the same as CKSAAP but the amino acids are first mapped + to 5 groups: aliphatic (GAVLMI), aromatic (FYW), positive charge (KRH), + negative charge (DE) and uncharged (STCPNQ). With 25 possible group pairs + per gap this creates a 25 * (gap+1) Dimensional vector, calculated as: - CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 + CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) * 100 - Values are percentages and the 25 values for each spacing sum to 100 - (0 when the spacing has no pairs). + where CKSAAGP(G1G2, g) is the composition of the group pair G1G2 with g + residues between them, N_g(G1G2) is the number of such pairs in the sequence + and L is the length of the sequence. The 25 values for each gap sum to 100. .. code-block:: python @@ -1179,17 +1186,24 @@ Dipeptide deviation from expected mean: (Dc - Tm) / sqrt(Tv), with Tm from codon .. code-block:: text - Calculate the Dipeptide Deviation from Expected Mean (DDE). The observed - dipeptide composition is standardised against the composition expected from - the number of codons coding each residue: + Calculate Dipeptide Deviation from Expected Mean (DDE) of protein sequence. + DDE compares the observed dipeptide composition of a protein sequence with + the composition expected from the number of codons that code for each amino + acid. For each of the 400 dipeptides it is calculated as: Dc(XY) = N(XY) / (L - 1) - Tm(XY) = (C_X / 61) x (C_Y / 61) - Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) + + Tm(XY) = (C_X / 61) * (C_Y / 61) + + Tv(XY) = Tm(XY) * (1 - Tm(XY)) / (L - 1) + DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) - where C_X is the number of sense codons for residue X in the standard genetic - code (61 in total). DDE is a standardised statistic, not a percentage. + where Dc(XY) is the observed composition of dipeptide XY, N(XY) is the + number of XY dipeptides, L is the length of the sequence, Tm(XY) and Tv(XY) + are the theoretical mean and variance, and C_X and C_Y are the number of + codons for amino acids X and Y out of the 61 sense codons. Unlike the + other composition descriptors, DDE values are not percentages. .. code-block:: python @@ -1209,13 +1223,17 @@ Adaptive skip dipeptide composition: composition of all ordered pairs (s_i, s_j) .. code-block:: text - Calculate the Adaptive Skip Dipeptide Composition (ASDC): the composition - of every ordered residue pair (s_i, s_j) with i < j, over all gaps: + Calculate Adaptive Skip Dipeptide Composition (ASDC) of protein sequence. + ASDC is a modified dipeptide composition that counts every ordered pair of + amino acids (s_i, s_j), where i < j, at all gaps along the sequence, rather + than just adjacent pairs, calculated as: - ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 + ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) * 100 - The denominator is the total number of ordered pairs, L(L-1)/2. Values are - percentages and sum to 100 (zeros for a single residue). + where ASDC(XY) is the composition of the amino acid pair XY, N_g(XY) is the + number of XY pairs separated by g residues, and the denominator is the total + number of ordered pairs in the sequence, L(L-1)/2, where L is the length of + the sequence. The 400 values sum to 100. .. code-block:: python @@ -1235,20 +1253,23 @@ PseAAC of distance-pairs over a reduced cluster profile (cp13/cp14/cp19/cp20). .. code-block:: text - Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet - (iDNA-Prot|dis). Residues are mapped to the clusters of a cluster - profile cp(n) and the vector is + Calculate Distance Pair (DP) descriptor of protein sequence. The descriptor + incorporates amino acid distance pairs and a reduced amino acid alphabet into + the general pseudo amino acid composition, as used in iDNA-Prot|dis. + The amino acids are first mapped to the clusters of a cluster profile cp(n), + and the descriptor is the composition of the clusters followed by the + composition of cluster pairs at each distance d = 1..rng: - [f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] + DP(c) = N(c) / L * 100 - where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the - composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the - nearest-neighbour pair. This follows Eqs. 5-12 of, giving G + G^2·rng - values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. + DP(c1c2, d) = N_d(c1c2) / (L - d) * 100 - Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of. The - printed cp(14) lists E twice; the stray E in the first cluster is dropped, - the only reading with 14 clusters. + where N(c) is the number of residues in cluster c, N_d(c1c2) is the number of + residue pairs (s_i, s_i+d) whose residues are in clusters c1 and c2, and L is + the length of the sequence. A distance of 1 is the nearest-neighbour pair. + The available cluster profiles are cp13, cp14, cp19 and cp20 (no reduction). + The published cp(14) lists E twice; the extra E in the first cluster is + dropped, as it is the only reading that gives 14 clusters. .. code-block:: python @@ -1268,13 +1289,19 @@ Natural vector: count, mean position and normalised second central moment of eac .. code-block:: text - Calculate the Natural Vector of a protein sequence. For each amino acid k: + Calculate Natural Vector (NV) of protein sequence. The natural vector + describes the number of each amino acid in a protein sequence together with + the mean and the normalized central moment of their positions, calculated as: + + n_k = number of occurrences of amino acid k - n_k = number of occurrences of k - mu_k = mean (1-based) position of k - D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) + mu_k = sum of the positions of k / n_k - mu_k and D2_k are 0 when k does not occur. Values are not percentages. + D2_k = sum of (position of k - mu_k)^2 / (n_k * L) + + where positions start at 1, L is the length of the sequence and k is each of + the 20 canonical amino acids. mu_k and D2_k are 0 when amino acid k is + not in the sequence. .. code-block:: python @@ -1294,16 +1321,18 @@ Percentage of C, H, N, O and S atoms over the residues' free amino-acid formulas .. code-block:: text - Calculate the Atomic Composition (ATC) of a protein sequence: the - percentage of C, H, N, O and S atoms, summed over the molecular formulas of - the residues' free amino acids. + Calculate Atomic Composition (ATC) of protein sequence. ATC describes the + percentage of carbon, hydrogen, nitrogen, oxygen and sulphur atoms in a + protein sequence, using the molecular formulas of the free amino acids, + calculated as: - ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 + ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) * 100 - By default free amino-acid formulas are summed without removing water, - matching Pfeature. With ``subtract_water=True``, (L - 1) H2O are removed - for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" - -> C4H8N2O3). + where ATC(e) is the atomic composition of element e and N_e is the number of + atoms of element e summed over all residues. By default the formulas of + the free amino acids are summed without removing water, matching Pfeature + . With subtract_water=True, one water molecule is removed for each of the + L - 1 peptide bonds, giving the formula of the peptide (e.g. "GG" -> C4H8N2O3). .. code-block:: python @@ -1323,8 +1352,21 @@ Mean of the Sandberg 5 z-scales over the sequence (optionally per domain). .. code-block:: text - Calculate the mean z_scales_5 factor-scale descriptor of a protein sequence (Sandberg 5 z-scales). - See ``aa_scale_descriptors`` for the formula. + Calculate Sandberg 5 z-scales (Z5) of protein sequence. The z-scales are the + principal components of a set of physicochemical properties measured for 87 + coded and non-coded amino acids. z1 mainly describes lipophilicity, z2 + steric bulk and polarizability, z3 polarity and charge, and z4 and z5 + electronegativity, heat of formation, electrophilicity and hardness. + + Each amino acid in the sequence is replaced by its 5 z-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1344,8 +1386,21 @@ Mean of the VHSE (hydrophobic, steric, electronic) scales over the sequence (opt .. code-block:: text - Calculate the mean vhse factor-scale descriptor of a protein sequence (principal components score Vectors of Hydrophobic, Steric and Electronic properties). - See ``aa_scale_descriptors`` for the formula. + Calculate Vectors of Hydrophobic, Steric and Electronic properties (VHSE) of + protein sequence. The VHSE scales are principal components score vectors derived + from physicochemical variables of the 20 coded amino acids. VHSE1-2 describe + hydrophobic properties, VHSE3-4 steric properties and VHSE5-8 electronic + properties. + + Each amino acid in the sequence is replaced by its 8 VHSE scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1365,8 +1420,21 @@ Mean of the Atchley factors I-V over the sequence (optionally per domain). .. code-block:: text - Calculate the mean atchley_factors factor-scale descriptor of a protein sequence (Atchley factors I-V). - See ``aa_scale_descriptors`` for the formula. + Calculate Atchley Factors (AF) of protein sequence. The Atchley factors are 5 + factors obtained from a factor analysis of 54 amino acid attributes. Factor + I mainly reflects polarity, accessibility and hydrophobicity, factor II + propensity for secondary structure, factor III molecular size, factor IV + relative amino acid composition and factor V electrostatic charge. + + Each amino acid in the sequence is replaced by its 5 Atchley factors and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1386,8 +1454,19 @@ Mean of the topological T-scales over the sequence (optionally per domain). .. code-block:: text - Calculate the mean t_scales factor-scale descriptor of a protein sequence (topological T-scales). - See ``aa_scale_descriptors`` for the formula. + Calculate Topological Scales (T-scales) of protein sequence. The T-scales are + principal components of topological descriptors of amino acids, describing their + structural and topological characteristics. + + Each amino acid in the sequence is replaced by its 5 T-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1407,8 +1486,19 @@ Mean of the structural topology ST-scales over the sequence (optionally per doma .. code-block:: text - Calculate the mean st_scales factor-scale descriptor of a protein sequence (structural topology ST-scales). - See ``aa_scale_descriptors`` for the formula. + Calculate Structural Topology Scales (ST-scales) of protein sequence. The ST- + scales are principal components of 3D structural and topological properties of + amino acids. + + Each amino acid in the sequence is replaced by its 8 ST-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1428,8 +1518,19 @@ Mean of the ProtFP (PCA, 8 components) over the sequence (optionally per domain) .. code-block:: text - Calculate the mean protfp factor-scale descriptor of a protein sequence (ProtFP PCA, 8 components). - See ``aa_scale_descriptors`` for the formula. + Calculate ProtFP (ProtFP) of protein sequence. ProtFP is a set of 8 principal + components calculated from a selection of physicochemical properties in the + AAindex database. + + Each amino acid in the sequence is replaced by its 8 ProtFP components and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1449,8 +1550,21 @@ Mean of the FASGAI factor scales over the sequence (optionally per domain). .. code-block:: text - Calculate the mean fasgai factor-scale descriptor of a protein sequence (factor analysis scales of generalized amino acid information). - See ``aa_scale_descriptors`` for the formula. + Calculate Factor Analysis Scales of Generalized Amino Acid Information (FASGAI) + of protein sequence. The FASGAI scales are 6 factors obtained from a factor + analysis of amino acid properties, describing hydrophobicity, alpha and turn + propensities, bulky properties, compositional characteristics, local flexibility + and electronic properties. + + Each amino acid in the sequence is replaced by its 6 FASGAI factors and the mean + of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1470,8 +1584,20 @@ Mean of the BLOSUM62-derived indices over the sequence (optionally per domain). .. code-block:: text - Calculate the mean blosum_indices factor-scale descriptor of a protein sequence (BLOSUM62-derived indices). - See ``aa_scale_descriptors`` for the formula. + Calculate BLOSUM Indices (BLOSUM) of protein sequence. The BLOSUM indices are 10 + factors derived from the BLOSUM62 substitution matrix by principal component + analysis and VARIMAX rotation, so they describe the evolutionary similarity of + amino acids. + + Each amino acid in the sequence is replaced by its 10 BLOSUM indices and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1491,8 +1617,20 @@ Mean of the MS-WHIM scores over the sequence (optionally per domain). .. code-block:: text - Calculate the mean ms_whim factor-scale descriptor of a protein sequence (MS-WHIM scores). - See ``aa_scale_descriptors`` for the formula. + Calculate Molecular Surface Weighted Holistic Invariant Molecular Scores (MS- + WHIM) of protein sequence. The MS-WHIM scores are 3 principal components of 3D + descriptors calculated from the molecular electrostatic potential on the surface + of each amino acid. + + Each amino acid in the sequence is replaced by its 3 MS-WHIM scores and the mean + of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. .. code-block:: python @@ -1512,15 +1650,19 @@ Auto/cross-covariance of an amino-acid scale: sum z_j(i) z_k(i+l) / (L-l). .. code-block:: text - Calculate the auto- and cross-covariance (ACC) transform of an amino-acid - scale. For lag l and scale dimensions j, k: + Calculate Auto Cross Covariance (ACC) of protein sequence. ACC transforms the + amino acid scale values of a sequence into a fixed-length vector that + describes the covariance between the scale dimensions of residues separated + by l positions, for each lag l in the range 1..lag. It is calculated as: - ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) + ACC_jk(l) = sum_{i=1}^{L-l} z_j(s_i) * z_k(s_i+l) / (L - l) - ``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto - covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a - protpy factor-scale family (used as published) or an AAindex1 code / list - of codes, which are standardised over the 20 amino acids. + where ACC_jk(l) is the covariance between dimensions j and k at lag l, z_j(s_i) + is the value of dimension j for the amino acid at position i and L is the length + of the sequence. When j = k this is the auto covariance and when j != k the cross + covariance. The scale can be one of the factor-scale families, which are used as + published, or one or more AAindex accession numbers, whose values are first + standardised over the 20 amino acids. .. code-block:: python @@ -1540,10 +1682,17 @@ Physicochemical distance transformation: mean squared difference of standardised .. code-block:: text - Calculate the Physicochemical Distance Transformation (PDT). For each - AAindex property P_j (standardised over the 20 amino acids) and lag l: + Calculate Physicochemical Distance Transformation (PDT) of protein sequence. + PDT describes how much a physicochemical property changes between residues + separated by l positions, for each lag l in the range 1..lag. For each + AAindex property it is calculated as: - PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) + PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_i+l))^2 / (L - l) + + where PDT_j(l) is the descriptor value for property j at lag l, P_j(s_i) is the + value of property j for the amino acid at position i, standardised over the 20 + amino acids, and L is the length of the sequence. By default the same 8 AAindex + properties as the autocorrelation descriptors are used. .. code-block:: python @@ -1563,13 +1712,21 @@ Fraction of charged residues (FCR), positive/negative fractions and charge asymm .. code-block:: text - Calculate the charge fractions of a protein sequence: + Calculate the Charge Fractions (FCR) of protein sequence. The fraction of + charged residues (FCR), the fractions of positive and negative residues and + the overall charge asymmetry (sigma) describe the charge content of a protein + sequence, and are key parameters of the conformations of intrinsically + disordered proteins. They are calculated as: + + FCR = (n+ + n-) / L - FCR = (n+ + n-) / L fraction of charged residues - FPlus = n+ / L, FMinus = n- / L - Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) + FPlus = n+ / L, FMinus = n- / L - where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. + Sigma = NCPR^2 / FCR + + where n+ is the number of positive residues (K, R), n- the number of negative + residues (D, E), L is the length of the sequence and NCPR = (n+ - n-) / L. His + is treated as neutral and sigma is 0 when there are no charged residues. .. code-block:: python @@ -1589,11 +1746,13 @@ Net charge per residue: (n+ - n-) / L with K/R positive and D/E negative. .. code-block:: text - Calculate the Net Charge Per Residue (NCPR) of a protein sequence: + Calculate the Net Charge Per Residue (NCPR) of protein sequence. NCPR is the + net charge of a protein sequence divided by its length, calculated as: NCPR = (n+ - n-) / L - with n+ counting K/R and n- counting D/E. + where n+ is the number of positive residues (K, R), n- the number of negative + residues (D, E) and L is the length of the sequence. .. code-block:: python @@ -1613,24 +1772,26 @@ Das-Pappu kappa: normalised blob charge-asymmetry deviation measuring segregatio .. code-block:: text - Calculate the kappa charge-patterning parameter of Das & Pappu. For blob - size g, the sequence is split into the L - g + 1 overlapping blobs and + Calculate the Kappa (Kappa) charge patterning parameter of protein sequence. + Kappa describes how well oppositely charged residues are mixed or segregated + along a protein sequence, which strongly affects the conformations of + intrinsically disordered proteins. The sequence is split into overlapping + blobs of g residues and kappa is calculated as: - sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) - delta_g = mean over blobs of (sigma_b - sigma)^2 - kappa_g = delta_g / delta_max,g + sigma_b = (f+ - f-)^2 / (f+ + f-) - where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the - largest delta over the maximally segregated arrangements of the same - composition (a positive block and a negative block with the neutral residues - distributed over the three surrounding gaps; every distinct arrangement is - evaluated). kappa is the mean of kappa_g - over ``blob_sizes`` (5 and 6 in); 0 means well mixed and 1 fully - segregated. Charges: K, R = +1; D, E = -1; H = 0. + delta_g = mean over all blobs of (sigma_b - sigma)^2 + + kappa_g = delta_g / delta_max,g - kappa is reported as 0 (not NaN) when the sequence has no charges or only one - charge type, where oppositely charged residues cannot be segregated. A blob - size longer than the sequence is reduced to the sequence length. + where sigma_b is the charge asymmetry of a blob, f+ and f- are its fractions of + positive (K, R) and negative (D, E) residues, sigma = NCPR^2 / FCR is the charge + asymmetry of the whole sequence and delta_max,g is the largest delta over the + most segregated arrangements of the same composition. The final kappa is the + mean of kappa_g over the blob sizes (5 and 6 by default). Kappa ranges from 0 + (well mixed) to 1 (fully segregated). It is reported as 0 when the sequence has + no charged residues or only one type of charge, and blobs longer than the + sequence are reduced to its length. .. code-block:: python @@ -1650,12 +1811,16 @@ Sequence charge decoration: (1/L) sum_{m>n} q_m q_n sqrt(m-n). .. code-block:: text - Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh: + Calculate the Sequence Charge Decoration (SCD) of protein sequence. SCD + describes the patterning of charges along a protein sequence, weighting each + pair of charges by the square root of their separation. It is calculated as: - SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} + SCD = (1/L) * sum_{m=2}^{L} sum_{n=1}^{m-1} q_m * q_n * (m - n)^(1/2) - with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values - indicate more segregated opposite charges; uncharged sequences give 0. + where q_m and q_n are the charges of the residues at positions m and n (+1 for + K and R, -1 for D and E, 0 otherwise) and L is the length of the sequence. More + negative values indicate more segregated opposite charges and uncharged + sequences have an SCD of 0. .. code-block:: python @@ -1675,12 +1840,15 @@ Net charge at the given pH divided by molecular weight (charges per Da). .. code-block:: text - Calculate the charge density of a protein sequence: the net charge at - ``ph`` (from ``charge_distribution``) divided by the molecular weight (from - ``molecular_weight``), in charges per Da. + Calculate the Charge Density of protein sequence. Charge density is the net + charge of a protein sequence at a given pH divided by its molecular weight, + calculated as: ChargeDensity = NetCharge(pH) / MW + where NetCharge(pH) is calculated using the charge_distribution function and MW + using the molecular_weight function. + .. code-block:: python result = protpy.charge_density(protein_seq) @@ -1699,13 +1867,20 @@ Summary statistics of a sliding-window hydrophobicity profile (ProtScale-style e .. code-block:: text - Summarise a sliding-window hydrophobicity profile. Each residue is - replaced by its scale value and a window of length ``window`` is slid along - the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` - (ExPASy ProtScale semantics). The profile is reduced to its mean, max, min - and standard deviation, plus the fraction of windows above ``threshold`` - when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). - Windows longer than the sequence use the whole sequence. + Calculate the Hydrophobicity Profile (HP) of protein sequence. Each amino acid + in the sequence is replaced by its value on a hydrophobicity scale and the + values are averaged over a sliding window, giving a profile along the sequence + . The profile is summarised by its mean, maximum, minimum and standard + deviation, and optionally the fraction of windows above a threshold, e.g. 1.6 + for transmembrane segments with the Kyte-Doolittle scale. The window value + is calculated as: + + HP(i) = sum_j w_j * H(s_i+j) / sum_j w_j + + where H is the hydrophobicity scale and w_j are the window weights. With + edge=1.0 all weights are 1 (a plain mean); with edge < 1 the weights fall + linearly from 1 at the centre of the window to edge at its ends, as in ExPASy + ProtScale. Windows longer than the sequence use the whole sequence. .. code-block:: python @@ -1725,10 +1900,14 @@ APD hydrophobic ratio: percentage of L, I, V, F, M, A, C, W residues. .. code-block:: text - Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) - : the percentage of residues that are L, I, V, F, M, A, C or W. + Calculate the Hydrophobic Ratio of protein sequence. The hydrophobic ratio is + the percentage of hydrophobic residues in a protein sequence, as defined by + the Antimicrobial Peptide Database (APD), calculated as: + + HydrophobicRatio = N(LIVFMACW) / L * 100 - HydrophobicRatio = N(LIVFMACW) / L x 100 + where N(LIVFMACW) is the number of L, I, V, F, M, A, C and W residues and L + is the length of the sequence. .. code-block:: python @@ -1748,14 +1927,15 @@ N-end rule half-life (hours) in mammalian, yeast and E. coli systems from the N- .. code-block:: text - Estimate the in vivo half-life of a protein from its N-terminal residue by - the N-end rule, as tabulated by ExPASy ProtParam: mammalian - reticulocytes (in vitro), yeast and E. coli (in vivo). - - Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" - in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli - value for an N-terminal Pro is unknown in the source and returned as NaN. - Not applicable to N-terminally modified proteins. + Calculate the estimated Half Life of protein sequence. The half-life is a + prediction of the time it takes for half of the amount of a protein in a cell + to disappear after its synthesis. It is estimated from the N-terminal residue + using the N-end rule, for 3 model systems: mammalian reticulocytes + (in vitro), yeast and E. coli (in vivo), using the values tabulated by ExPASy + ProtParam. Values given as lower bounds in ProtParam (">20 hours" for yeast, + ">10 hours" for E. coli) are reported as the bound. The E. coli value for an + N-terminal proline is unknown in ProtParam and returned as NaN. The estimate is + not applicable to N-terminally modified proteins. .. code-block:: python @@ -1775,12 +1955,16 @@ Fraction of I, V, Y, W, R, E, L residues (thermophilic adaptation index). .. code-block:: text - Calculate the IVYWREL index of Zeldovich et al.: the fraction of residues - that are I, V, Y, W, R, E or L, which correlates with optimal growth - temperature. + Calculate the IVYWREL Index of protein sequence. The IVYWREL index is the + fraction of I, V, Y, W, R, E and L residues in a protein sequence, which + correlates with the optimal growth temperature of the organism. It is + calculated as: IVYWREL = N(IVYWREL) / L + where N(IVYWREL) is the number of I, V, Y, W, R, E and L residues and L is the + length of the sequence. + .. code-block:: python result = protpy.ivywrel_index(protein_seq) @@ -1799,11 +1983,14 @@ Charged versus polar bias: %(D+E+K+R) - %(N+Q+S+T). .. code-block:: text - Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie, an - indicator of hyperthermostability: + Calculate the Charged versus Polar Bias (CvP) of protein sequence. The CvP bias + is the difference between the percentages of charged and polar residues in a + protein sequence, an indicator of hyperthermostability, calculated as: CvP = %(D + E + K + R) - %(N + Q + S + T) + where %(X) is the percentage of residues of type X in the sequence. + .. code-block:: python result = protpy.cvp_bias(protein_seq) @@ -1822,17 +2009,16 @@ Summary statistics of the Vihinen average-B-value flexibility profile (window 9, .. code-block:: text - Summarise the flexibility profile of Vihinen et al.. Average normalised - B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged - over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, - 0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced - to its mean, max, min and standard deviation. - - The neighbour-dependent parameter sets of (residues with 0, 1 or 2 rigid - neighbours) are not used. Compared with Biopython's - ``ProteinAnalysis.flexibility``, which uses the same values and weights, - protpy centres each window on its middle residue and includes the last - window; Biopython takes the residue after the centre and omits the last window. + Calculate the Flexibility (Flex) profile of protein sequence. Each amino acid + in the sequence is replaced by its normalised flexibility parameter (B-value) + and the values are averaged over a sliding window of 9 residues whose end + weights fall linearly to 0.25 (weights 0.25, 0.4375, 0.625, 0.8125, 1, ..., + 0.25), as in Vihinen et al.. The profile is summarised by its mean, + maximum, minimum and standard deviation. The average B-values of Vihinen et + al. are used; their parameter sets for residues with 0, 1 or 2 rigid + neighbours are not. Compared with Biopython's ProteinAnalysis.flexibility, + which uses the same values and weights, each window is centred on its middle + residue and the last window is included. .. code-block:: python @@ -1852,17 +2038,22 @@ Wootton-Federhen local complexity (K1, K2) over sliding windows and the SEG-trig .. code-block:: text - Calculate Wootton-Federhen local compositional complexity. For every - window of length W with residue counts n_i: + Calculate the Wootton-Federhen Complexity (WF) of protein sequence. The + Wootton-Federhen measures describe the local compositional complexity of a + protein sequence over a sliding window, and are the basis of the SEG program + for finding low-complexity regions. For each window of W residues + they are calculated as: + + K1 = (1/W) * log_20(W! / prod_i n_i!) - K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) - K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) + K2 = -sum_i (n_i / W) * log2(n_i / W) - The low-complexity fraction is the fraction of residues covered by at least - one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: - SEG's extension to the K2 extension threshold and its interval merging and - optimisation are not performed. A window longer than the sequence uses the - whole sequence. + where K1 is the complexity state, K2 is the Shannon entropy of the window in + bits and n_i is the number of residues of amino acid i in the window. The + low-complexity fraction is the fraction of residues covered by at least one + window with K2 <= k2_trigger. This is the trigger stage of SEG only; SEG's + extension and merging of low-complexity segments are not performed. A window + longer than the sequence uses the whole sequence. .. code-block:: python @@ -1882,15 +2073,17 @@ Linguistic complexity: distinct subwords observed / maximum possible (ratio of s .. code-block:: text - Calculate the linguistic complexity (LC) of a protein sequence: the - number of distinct subwords (k-mers) observed, over the maximum number - possible for a sequence of that length over the 20-letter alphabet: + Calculate the Linguistic Complexity (LC) of protein sequence. Linguistic + complexity describes how repetitive a sequence is, as the number of distinct + subwords (k-mers) it contains relative to the maximum possible number for a + sequence of its length. It is calculated as: LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) - where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. - all subwords). This is the ratio-of-sums form, counting all subwords as in - ; repetitive sequences have low LC. + where U_k is the number of distinct k-mers in the sequence, L is the length of + the sequence and K is the longest subword length (by default L, i.e. all + subwords). Repetitive sequences have a low LC and random sequences an LC + close to 1. .. code-block:: python @@ -1910,15 +2103,16 @@ Lempel-Ziv (LZ76) complexity c(n) and its normalised value c(n)·log20(n)/n. .. code-block:: text - Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence: the - number c(n) of new components found when the sequence is parsed from left to - right, each component being the shortest substring not producible by - copying from the preceding history. The normalised value for a 20-letter - alphabet is + Calculate the Lempel-Ziv Complexity (LZ) of protein sequence. The Lempel-Ziv + (LZ76) complexity is the number of new patterns, c(n), found when a sequence is + read from left to right, each pattern being the shortest substring that cannot + be copied from the preceding part of the sequence. As c(n) grows with the + length of the sequence, a normalised value is also calculated as: - LZ_Normalized = c(n) x log_20(n) / n + LZ_Normalized = c(n) * log_20(n) / n - (0 for a single residue). + where n is the length of the sequence and 20 is the size of the amino acid + alphabet. The normalised value is 0 for a single residue. .. code-block:: python @@ -1938,16 +2132,19 @@ Mean power of the FFT spectrum of a mean-centred property signal (default EIIP) .. code-block:: text - Calculate a length-invariant binned power spectrum of a property signal, as - in the resonant recognition model of Cosic. Each residue is replaced by - its scale value (default: electron-ion interaction potential, EIIP), the - signal is mean-centred, and its power spectrum is + Calculate Fast Fourier Transform (FFT) descriptors of protein sequence. Each + amino acid in the sequence is replaced by its value on a property scale, by + default the electron-ion interaction potential (EIIP) used in the resonant + recognition model, and the power spectrum of the mean-centred signal is + calculated as: - P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue + P(f) = abs(FFT(x)(f))^2 / L - The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width - bins (the last bin includes 0.5) and the mean power in each bin is returned; - bins containing no frequency are 0. + where x is the property signal, f is the frequency in cycles per residue + (0 to 0.5) and L is the length of the sequence. To give the same number of + features for any sequence length, the frequency range 0-0.5 is split into + n_bins equal-width bins and the mean power in each bin is returned; bins + containing no frequencies are 0. .. code-block:: python @@ -1967,13 +2164,18 @@ First n orthonormal DCT-II coefficients of a mean-centred property signal (defau .. code-block:: text - Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II - discrete cosine transform (DCT-II) of a mean-centred property signal: + Calculate Discrete Cosine Transform (DCT) descriptors of protein sequence. + Each amino acid in the sequence is replaced by its value on a property scale, + by default the Kyte-Doolittle hydropathy, and the first n_coeffs coefficients + of the orthonormal type-II DCT of the mean-centred signal are returned. The + DCT compresses a sequence-length signal into a fixed number of coefficients + , calculated as: - X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), - s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 + X_k = s_k * sum_{n=0}^{L-1} x_n * cos(pi * k * (2n + 1) / (2L)) - X_0 is 0 because the signal is mean-centred. + where X_k is coefficient k, x_n is the property value at position n, L is the + length of the sequence, and s_0 = sqrt(1/L) and s_k = sqrt(2/L) for k > 0. X_0 + is 0 because the signal is mean-centred. .. code-block:: python @@ -1993,29 +2195,27 @@ AGGRESCAN aggregation hot-spot descriptors from the a3v propensity profile (hot .. code-block:: text - Calculate AGGRESCAN aggregation "hot spot" descriptors. Each residue is - given its experimental aggregation propensity a3v; the a4v profile (AP) is the - a3v mean over a sliding window, assigned to the window's central residue. - Off-centre N- and C-terminal residues take the first and last window values. - A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot - threshold HST = -0.02 and containing no proline. Areas use trapezoidal - integration of the AP with HST as the zero axis. + Calculate AGGRESCAN (AGG) aggregation descriptors of protein sequence. + AGGRESCAN predicts aggregation "hot spots", short stretches that promote + protein aggregation, from experimental aggregation propensities (a3v) of the + 20 amino acids. The a3v values are averaged over a sliding window, giving + the aggregation profile (a4v), with each window value assigned to its central + residue and the off-centre N- and C-terminal residues given the first and last + window values. A hot spot is a run of 5 or more residues with an a4v above the + hot-spot threshold (HST) of -0.02 and containing no proline. Areas are + calculated by trapezoidal integration of the profile, with HST as the zero axis. - Outputs:: - a3vSA mean a3v over the sequence - nHS number of hot spots; NnHS = nHS / L x 100 - AAT area of the AP above HST over the whole sequence; AATr = AAT / L - THSA total hot-spot area (sum of HSA); THSAr = THSA / L - TA total area of the AP (HST as zero axis, signed) - a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 - a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) - HSA_max largest single hot-spot area (0 if none) + The 12 output values are: the mean a3v of the sequence (a3vSA); the number of + hot spots (nHS) and the number per 100 residues (NnHS); the area of the profile + above HST (AAT) and per residue (AATr); the total area of the hot spots (THSA) + and per residue (THSAr); the total area of the profile (TA); the sum of a4v + (a4vSS) and per 100 residues (Na4vSS); the mean a4v in the hot spots (a4vAHS, + 0 if there are none); and the largest single hot-spot area (HSA_max). - AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and - gives the two extreme residues specific charge-corrected values; neither - rule is given in the published supplementary material, so ``window`` is a - parameter (default 5) and the extreme residues are treated like the other - off-centre residues. + AGGRESCAN chooses a window of 5, 7, 9 or 11 residues from the sequence length + and gives the two extreme residues specific values; neither rule is given in + the published supplementary material, so the window is a parameter and the + extreme residues are treated like the other off-centre residues. .. code-block:: python @@ -2035,14 +2235,21 @@ Uversky mean hydropathy , mean net charge and boundary distance HQ = 2.78 .. code-block:: text - Calculate the charge-hydropathy descriptors of Uversky et al.: + Calculate the Uversky Charge-Hydropathy (UV) descriptors of protein sequence. + Uversky et al. showed that natively unfolded proteins can be separated from + folded proteins by their mean net charge and mean hydropathy. These are + calculated as: + + = mean of (KD + 4.5) / 9 + + = abs(n+ - n-) / L - = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = abs(n+ - n-) / L (K/R positive, D/E negative) - HQ = 2.785 - - 1.151 + HQ = 2.785 - - 1.151 - The boundary = 2.785 - 1.151 separates natively unfolded from folded - proteins (as restated in); ``UV_Unfolded`` is 1 when HQ < 0. + where KD is the Kyte-Doolittle hydropathy of each residue, n+ and n- are the + numbers of positive (K, R) and negative (D, E) residues and L is the length + of the sequence. HQ is the distance from the boundary = 2.785 - 1.151 + (as restated in); the protein is predicted to be unfolded when HQ < 0. .. code-block:: python @@ -2062,15 +2269,17 @@ FoldIndex profile summary: windowed 2.785 - abs() - 1.151 and disordered-r .. code-block:: text - Calculate FoldIndex profile descriptors. For each window of length - ``window``: + Calculate the FoldIndex (FI) of protein sequence. FoldIndex applies the + Uversky charge-hydropathy boundary over a sliding window to find disordered + regions of a protein sequence. For each window it is calculated as: FI = 2.785 - abs() - 1.151 - with and as in ``uversky_charge_hydropathy`` but computed over the - window. Residues covered by at least one window with FI < 0 are counted as - disordered. A window longer than the sequence uses the whole sequence, so - FI then equals ``UV_HQ``. + where is the mean Kyte-Doolittle hydropathy of the window rescaled to 0-1 + and is the mean net charge of the window (K, R positive; D, E negative). + Residues covered by at least one window with FI < 0 are counted as disordered. + A window longer than the sequence uses the whole sequence, so FI then equals + UV_HQ from uversky_charge_hydropathy. .. code-block:: python @@ -2090,10 +2299,18 @@ Fractions of disorder- and order-promoting residues and mean TOP-IDP value. .. code-block:: text - Calculate disorder-propensity descriptors: the fractions of - disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting - (W, C, F, I, Y, V, L, N) residues, and the mean TOP-IDP value - (higher = more disorder-prone). + Calculate the Disorder Propensity (DP) of protein sequence. Intrinsically + disordered proteins are enriched in disorder-promoting residues (A, R, G, Q, + S, P, E, K) and depleted in order-promoting residues (W, C, F, I, Y, V, L, N) + . The descriptor gives the fraction of each, together with the mean value of + the TOP-IDP scale, a scale of amino acid propensity for intrinsic disorder, + calculated as: + + DP_TOP_IDP_mean = sum_i TOP-IDP(s_i) / L + + where TOP-IDP(s_i) is the TOP-IDP value of the amino acid at position i and L + is the length of the sequence. Higher values indicate a higher propensity for + disorder. .. code-block:: python @@ -2113,20 +2330,17 @@ Kyte-Doolittle transmembrane-segment heuristics (segment count, max hydropathy, .. code-block:: text - Calculate heuristic transmembrane-segment features from the Kyte-Doolittle - hydropathy profile. Windows of length ``window`` with mean hydropathy - above ``kd_threshold`` (1.6 for 19-residue windows in) mark putative - transmembrane segments; overlapping windows are merged into segments. - - Columns:: - - TM_n_segments number of merged segments - TM_max_KD maximum window hydropathy - TM_frac_in_TM fraction of residues inside a segment - TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment - - This is a hydropathy heuristic, not a topology predictor. + Calculate Transmembrane (TM) segment features of protein sequence. Putative + transmembrane segments are found from the Kyte-Doolittle hydropathy profile: + windows of 19 residues with a mean hydropathy above 1.6 indicate a likely + transmembrane segment, and overlapping windows are merged into segments. + The positive-inside rule states that the cytoplasmic side of a transmembrane + segment is enriched in positive residues. The 4 output values are: the + number of segments; the maximum window hydropathy; the fraction of residues + inside a segment; and the positive-inside bias, the number of K and R residues + in the 15 residues before the first segment minus the number in the 15 residues + after it (0 if there is no segment). This is a hydropathy heuristic and not a + topology predictor. .. code-block:: python @@ -2146,15 +2360,12 @@ N-terminal signal-peptide heuristics: n-region net charge and maximum 8-residue .. code-block:: text - Calculate simple signal-peptide features of the N-terminus: - - Columns:: - - SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) - SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues - - The von Heijne cleavage-site weight matrix is not included. + Calculate Signal Peptide (SP) features of protein sequence. Signal peptides + have a positively charged N-terminal n-region followed by a hydrophobic + h-region. The 2 output values are: the net charge of the first 5 residues + (K and R +1, D and E -1); and the maximum mean Kyte-Doolittle hydropathy over + 8-residue windows within the first n_term residues. The von Heijne + cleavage-site weight matrix is not included. .. code-block:: python @@ -2174,11 +2385,14 @@ Counts and per-100-residue densities of PROSITE N-glycosylation, ER-retention an .. code-block:: text - Count functional sequence motifs and their density per 100 residues: - N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal - (PS00014) and peroxisomal targeting signal 1 (PS00342), and tryptic - cleavage sites (after K/R, not before P). Overlapping matches are - counted. These complement the default motifs of ``motif_composition``. + Calculate Functional Site (FS) counts of protein sequence. The number of each + functional sequence motif, and its density per 100 residues, is counted for: + N-glycosylation sites (PROSITE PS00001, N-{P}-[ST]-{P}), the C-terminal + endoplasmic reticulum retention signal (PS00014, [KRHQSA]-[DENQ]-E-L at the + C-terminus), the C-terminal peroxisomal targeting signal 1 (PS00342, + [STAGCN]-[RKH]-[LIVMAFY] at the C-terminus) and tryptic cleavage sites + (after K or R, not before P). Overlapping matches are counted. These motifs + complement the default motifs of motif_composition. .. code-block:: python @@ -2198,20 +2412,23 @@ PLAAC prion-like domain scores: max window LLR, HMM core score, PrD length and p .. code-block:: text - Calculate prion-like domain descriptors with the PLAAC model, ported from - the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each - residue has the log-likelihood ratio LLR = ln(f_prion / f_background). - - Columns:: + Calculate the Prion-Like Amino Acid Composition (PLAAC) descriptors of protein + sequence. PLAAC identifies prion-like domains, whose composition resembles + that of yeast prion domains, using the amino acid frequencies of prion domains + and of the S. cerevisiae proteome. Each residue has a log-likelihood ratio: - PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues - PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse - PLAAC_PrD_length length of the prion-like run containing the best core - PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists + LLR(s_i) = ln(f_prion(s_i) / f_background(s_i)) - As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than - ``core_length`` / no prion-like run of that length); PrD_length is then 0. + The 4 output values are: the maximum sum of LLR over any window of core_length + residues (PLAAC_LLR); the maximum sum of LLR over a window of core_length + residues lying entirely inside a prion-like region of the two-state hidden + Markov model Viterbi parse (PLAAC_COREscore); the length of the prion-like + region containing that core (PLAAC_PrD_length); and whether the sequence has a + prion-like region of at least core_length residues (PLAAC_has_PrD). As in + PLAAC, PLAAC_LLR and PLAAC_COREscore are NaN when they are undefined (a + sequence shorter than core_length, or no prion-like region of that length). + The parameters are ported from the PLAAC source code (MIT License), using the + S. cerevisiae background (alpha = 1). .. code-block:: python @@ -2231,11 +2448,14 @@ Profile statistics of the Parker hydrophilicity and Janin surface scales over 7- .. code-block:: text - Summarise hydrophilicity/surface profiles associated with antigenic sites: - the Parker HPLC hydrophilicity scale averaged over 7-residue windows and - the Janin free energy of transfer from inside to outside of a globular - protein over the same window. Each profile is reduced to its mean, max, - min and standard deviation. + Calculate the Antigenicity and Surface (Antigenicity) profiles of protein + sequence. Antigenic sites tend to be hydrophilic and exposed on the protein + surface. Each amino acid in the sequence is replaced by its value on the + Parker hydrophilicity scale, derived from HPLC peptide retention times, + and on the Janin scale of free energy of transfer from the inside to the + outside of a globular protein. Each is averaged over a sliding window of 7 + residues, as used by Parker et al., and the profile is summarised by its mean, + maximum, minimum and standard deviation. .. code-block:: python @@ -2255,14 +2475,12 @@ Cysteine count, fraction, parity, maximum disulfides, CxxC motifs and mean spaci .. code-block:: text - Calculate cysteine features: - - Cys_count number of C - Cys_frac C / L - Cys_even 1 if the count is even - Cys_max_disulfides floor(count / 2) - Cys_CxxC overlapping C-x-x-C motifs - Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) + Calculate Cysteine (Cys) features of protein sequence. Cysteines form + disulfide bonds and functional motifs such as the CxxC motif of thiol + oxidoreductases. The 6 output values are: the number of cysteines; their + fraction; whether the number is even; the maximum number of disulfide bonds + (the number of cysteines // 2); the number of overlapping C-x-x-C motifs; and + the mean gap between consecutive cysteines (0 if there are fewer than 2). .. code-block:: python diff --git a/protpy/_helpers.py b/protpy/_helpers.py index ef9cf46..7a0a977 100644 --- a/protpy/_helpers.py +++ b/protpy/_helpers.py @@ -25,19 +25,46 @@ @lru_cache(maxsize=None) def _read_json(filename: str) -> dict: + """ + Read a JSON data file from the protpy/data directory. Files are cached + after the first read. + + Parameters + ========== + :filename: str + name of the JSON file in protpy/data. + + Returns + ======= + :tables: dict + contents of the JSON file. + """ with open(os.path.join(_DATA_DIR, filename)) as f: return json.load(f) def _load_json_table(filename: str, key: str) -> dict: """ - Return a copy of table ``key`` from ``protpy/data/``. Each table - has the keys name, values, reference, doi and source_note. + Get a copy of a data table from a JSON file in the protpy/data directory. + Each table has the keys name, values, reference, doi and source_note. A copy + is returned so the cached table can't be modified. + + Parameters + ========== + :filename: str + name of the JSON file in protpy/data. + :key: str + name of the table in the file. + + Returns + ======= + :table: dict + copy of the data table. Raises ====== KeyError - if ``key`` is not a table in the file; the message lists the valid keys. + if the table is not in the file; the message lists the available tables. """ tables = _read_json(filename) if key not in tables: @@ -48,8 +75,24 @@ def _load_json_table(filename: str, key: str) -> dict: def _scale_values(scale: str | dict) -> dict: """ - Resolve a scale to {aa: float | list[float]}. ``scale`` may be a table name - in aa_scales.json, an AAindex1 accession code, or a user dict. + Get the values of an amino acid scale for each amino acid. The scale can be a + table name in the aa_scales.json data file, an AAindex accession number or a + dict of amino acid values. + + Parameters + ========== + :scale: str | dict + scale name, AAindex accession number or dict of amino acid values. + + Returns + ======= + :values: dict + dict of scale values (a float or a list of floats) for each amino acid. + + Raises + ====== + ValueError + if the scale is not a protpy scale or an AAindex accession number. """ if isinstance(scale, dict): return scale @@ -66,12 +109,33 @@ def _scale_values(scale: str | dict) -> dict: def _property_matrix(sequence: str, scales: str | list[str] | dict, standardize: bool = False) -> np.ndarray: """ - Return the L x d matrix of property values for ``sequence``. + Convert a protein sequence into a matrix of amino acid property values, with + one row per residue and one column per property dimension. The scales can be a + factor-scale family or single-value scale from the aa_scales.json data file, + an AAindex accession number, a list of AAindex accession numbers or a dict of + amino acid values (a float or a list of floats per amino acid). + + Parameters + ========== + :sequence: str + protein sequence. + :scales: str | list[str] | dict + scale name, AAindex accession number, list of accession numbers or dict of + amino acid values. + :standardize: bool (default=False) + standardise each dimension to zero mean and unit standard deviation over + the 20 amino acids. + + Returns + ======= + :matrix: np.ndarray + property values of the sequence, of the shape L x d, where L is the length + of the sequence and d is the number of property dimensions. - ``scales`` may be a factor-scale family / single-value scale name from - aa_scales.json, an AAindex1 accession code, a list of AAindex1 codes, or a - user dict {aa: float} / {aa: list[float]}. With ``standardize=True`` each - dimension is z-scored over the 20 canonical amino acids (population std). + Raises + ====== + ValueError + if the scale is missing values for any of the 20 amino acids. """ if isinstance(scales, (list, tuple)): tables = [_scale_values(s) for s in scales] @@ -93,12 +157,25 @@ def _property_matrix(sequence: str, scales: str | list[str] | dict, standardize: def _sliding_windows(values: np.ndarray, window: int, edge: float = 1.0) -> np.ndarray: """ - Weighted sliding-window means of a 1-D profile. + Calculate the weighted mean of a profile over a sliding window. With edge=1.0 + the weights are all 1 (a plain mean); with edge < 1 the weights fall linearly + from 1 at the centre of the window to edge at its ends, as in ExPASy ProtScale + and Biopython's protein_scale. A window longer than the profile uses the whole + profile. - Weights fall linearly from ``edge`` at the window ends to 1 at the centre - (ExPASy ProtScale / Biopython ``protein_scale`` semantics); ``edge=1`` is a - plain mean. If ``window`` exceeds the profile length a single window of the - full length is used. + Parameters + ========== + :values: np.ndarray + profile values along the sequence. + :window: int + length of the sliding window. + :edge: float (default=1.0) + relative weight of the ends of the window, between 0 and 1. + + Returns + ======= + :windows: np.ndarray + weighted mean of each window, of length L - window + 1. """ values = np.asarray(values, dtype=float) window = min(window, len(values)) @@ -111,7 +188,26 @@ def _sliding_windows(values: np.ndarray, window: int, edge: float = 1.0) -> np.n def _profile_stats(profile: np.ndarray, prefix: str, stats=("mean", "max", "min", "std"), threshold: float | None = None) -> dict: - """Reduce a profile to {prefix_stat: value}; adds ``frac_above`` when ``threshold`` is set.""" + """ + Summarise a profile by its mean, maximum, minimum and standard deviation, and + optionally the fraction of values above a threshold. + + Parameters + ========== + :profile: np.ndarray + profile values. + :prefix: str + prefix for the names of the statistics. + :stats: tuple (default=("mean", "max", "min", "std")) + statistics to calculate. + :threshold: float | None (default=None) + if set, also calculate the fraction of values above it (frac_above). + + Returns + ======= + :stats: dict + dict of statistic names (prefix_stat) and values. + """ profile = np.asarray(profile, dtype=float) funcs = {"mean": np.mean, "max": np.max, "min": np.min, "std": np.std} out = {f"{prefix}_{s}": float(funcs[s](profile)) for s in stats} @@ -121,33 +217,115 @@ def _profile_stats(profile: np.ndarray, prefix: str, stats=("mean", "max", "min" def _gapped_pair_counts(sequence: str, gap: int, alphabet: dict[str, str] | None = None) -> Counter: - """Count pairs (x_i, x_{i+gap+1}), optionally mapped through a residue -> group alphabet.""" + """ + Count the pairs of residues (s_i, s_i+gap+1) in a protein sequence, with gap + residues between them. The residues can first be mapped to groups using a + reduced alphabet. + + Parameters + ========== + :sequence: str + protein sequence. + :gap: int + number of residues between the two residues of a pair. + :alphabet: dict | None (default=None) + dict mapping each amino acid to its group; None counts the amino acids. + + Returns + ======= + :counts: Counter + count of each pair, keyed by a tuple of the two residues or groups. + """ seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence) step = gap + 1 return Counter((seq[i], seq[i + step]) for i in range(len(seq) - step)) def _kmer_counts(sequence: str, k: int, alphabet: dict[str, str] | None = None) -> Counter: - """Overlapping k-mer counts (as tuples) over the raw or reduced alphabet.""" + """ + Count the overlapping k-mers of a protein sequence. The residues can first be + mapped to groups using a reduced alphabet. + + Parameters + ========== + :sequence: str + protein sequence. + :k: int + length of the k-mers. + :alphabet: dict | None (default=None) + dict mapping each amino acid to its group; None counts the amino acids. + + Returns + ======= + :counts: Counter + count of each k-mer, keyed by a tuple of its residues or groups. + """ seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence) return Counter(tuple(seq[i:i + k]) for i in range(len(seq) - k + 1)) def _charge_vector(sequence: str, his: bool = False) -> np.ndarray: - """+1 for K/R (and H when ``his``), -1 for D/E, else 0.""" + """ + Get the charge of each residue of a protein sequence: +1 for K and R, -1 for + D and E and 0 otherwise. His is neutral unless his=True. + + Parameters + ========== + :sequence: str + protein sequence. + :his: bool (default=False) + treat His as positive. + + Returns + ======= + :charges: np.ndarray + charge of each residue. + """ positive = "KRH" if his else "KR" return np.array([1 if aa in positive else -1 if aa in "DE" else 0 for aa in sequence], dtype=float) def _runs(mask: np.ndarray, min_len: int = 1) -> list[tuple[int, int]]: - """Return (start, end) half-open index pairs of consecutive True runs of length >= min_len.""" + """ + Find the runs of consecutive True values in a boolean array, e.g. the + segments of a profile above a threshold. + + Parameters + ========== + :mask: np.ndarray + boolean values along the sequence. + :min_len: int (default=1) + minimum length of a run. + + Returns + ======= + :runs: list + list of (start, end) indices of each run, where end is exclusive. + """ mask = np.concatenate(([False], np.asarray(mask, dtype=bool), [False])) edges = np.flatnonzero(np.diff(mask.astype(int))) return [(int(s), int(e)) for s, e in zip(edges[::2], edges[1::2]) if e - s >= min_len] def _to_df(values: list, columns: list[str]) -> pd.DataFrame: - """Round floats to 3 dp and return a 1 x N DataFrame.""" + """ + Convert a list of descriptor values into a single-row dataframe. Floats are + rounded to 3 decimal places and booleans and numpy integers are converted to + ints. + + Parameters + ========== + :values: list + descriptor values. + :columns: list[str] + column names. + + Returns + ======= + :descriptor_df: pd.DataFrame + pandas dataframe of the descriptor values, of the shape 1 x N, where N is + the number of values. + """ values = [round(float(v), 3) if isinstance(v, (float, np.floating)) else int(v) if isinstance(v, (bool, np.bool_, np.integer)) else v for v in values] return pd.DataFrame([values], columns=columns) diff --git a/protpy/complexity.py b/protpy/complexity.py index cdcf242..cb0839b 100644 --- a/protpy/complexity.py +++ b/protpy/complexity.py @@ -46,32 +46,39 @@ def wootton_federhen_complexity(sequence: str, window: int = 12, k2_trigger: float = 2.2) -> pd.DataFrame: """ - Calculate Wootton-Federhen local compositional complexity [1, 2]. For every - window of length W with residue counts n_i: + Calculate the Wootton-Federhen Complexity (WF) of protein sequence. The + Wootton-Federhen measures describe the local compositional complexity of a + protein sequence over a sliding window, and are the basis of the SEG program + for finding low-complexity regions [1, 2]. For each window of W residues + they are calculated as: - K1 = (1/W) log_20( W! / prod_i n_i! ) (complexity state, 0-1) - K2 = -sum_i (n_i/W) log2(n_i/W) (Shannon entropy, bits) + K1 = (1/W) * log_20(W! / prod_i n_i!) - The low-complexity fraction is the fraction of residues covered by at least - one window with K2 <= ``k2_trigger``. This is the SEG trigger stage only: - SEG's extension to the K2 extension threshold and its interval merging and - optimisation are not performed. A window longer than the sequence uses the - whole sequence. + K2 = -sum_i (n_i / W) * log2(n_i / W) + + where K1 is the complexity state, K2 is the Shannon entropy of the window in + bits and n_i is the number of residues of amino acid i in the window. The + low-complexity fraction is the fraction of residues covered by at least one + window with K2 <= k2_trigger. This is the trigger stage of SEG only; SEG's + extension and merging of low-complexity segments are not performed. A window + longer than the sequence uses the whole sequence. Parameters ========== :sequence: str protein sequence. :window: int (default=12) - window length W (SEG default 12). + length of the sliding window W (the SEG default). :k2_trigger: float (default=2.2) - K2 trigger complexity in bits (SEG default 2.2). + K2 value in bits at or below which a window is low complexity (the SEG + default). Returns ======= :wf_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``WF_K1_mean``, ``WF_K2_mean``, - ``WF_K2_min`` and ``WF_LowComplexity_frac``. + pandas dataframe of Wootton-Federhen complexity for protein sequence. + Dataframe will be of the shape 1 x 4, with the columns WF_K1_mean, + WF_K2_mean, WF_K2_min and WF_LowComplexity_frac. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -99,27 +106,30 @@ def wootton_federhen_complexity(sequence: str, window: int = 12, k2_trigger: flo def linguistic_complexity(sequence: str, max_k: int | None = None) -> pd.DataFrame: """ - Calculate the linguistic complexity (LC) of a protein sequence [3]: the - number of distinct subwords (k-mers) observed, over the maximum number - possible for a sequence of that length over the 20-letter alphabet: + Calculate the Linguistic Complexity (LC) of protein sequence. Linguistic + complexity describes how repetitive a sequence is, as the number of distinct + subwords (k-mers) it contains relative to the maximum possible number for a + sequence of its length [3]. It is calculated as: LC = sum_{k=1}^{K} U_k / sum_{k=1}^{K} min(20^k, L - k + 1) - where U_k is the number of distinct k-mers and K = ``max_k`` (default L, i.e. - all subwords). This is the ratio-of-sums form, counting all subwords as in - [3]; repetitive sequences have low LC. + where U_k is the number of distinct k-mers in the sequence, L is the length of + the sequence and K is the longest subword length (by default L, i.e. all + subwords). Repetitive sequences have a low LC and random sequences an LC + close to 1. Parameters ========== :sequence: str protein sequence. :max_k: int | None (default=None) - longest subword length; None uses the sequence length. + longest subword length K; None uses the length of the sequence. Returns ======= :lc_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``LinguisticComplexity`` (0-1). + pandas dataframe of LC for protein sequence. Dataframe will be of the + shape 1 x 1, with the column LinguisticComplexity. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -137,7 +147,22 @@ def linguistic_complexity(sequence: str, max_k: int | None = None) -> pd.DataFra ############################### Lempel-Ziv Complexity ##################################### def _lz76(sequence: str) -> int: - """Number of components in the LZ76 exhaustive-history parsing (Kaspar-Schuster scheme).""" + """ + Calculate the number of components in the Lempel-Ziv (LZ76) parsing of a + sequence [4], using the Kaspar-Schuster algorithm. The sequence is read from + left to right and each component is the shortest substring that cannot be + copied from the preceding part of the sequence. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :c: int + number of components c(n) in the LZ76 parsing. + """ n = len(sequence) if n == 1: return 1 @@ -165,15 +190,16 @@ def _lz76(sequence: str) -> int: def lempel_ziv_complexity(sequence: str) -> pd.DataFrame: """ - Calculate the Lempel-Ziv (LZ76) complexity of a protein sequence [4, 5]: the - number c(n) of new components found when the sequence is parsed from left to - right, each component being the shortest substring not producible by - copying from the preceding history. The normalised value for a 20-letter - alphabet is + Calculate the Lempel-Ziv Complexity (LZ) of protein sequence. The Lempel-Ziv + (LZ76) complexity is the number of new patterns, c(n), found when a sequence is + read from left to right, each pattern being the shortest substring that cannot + be copied from the preceding part of the sequence [4, 5]. As c(n) grows with the + length of the sequence, a normalised value is also calculated as: - LZ_Normalized = c(n) x log_20(n) / n + LZ_Normalized = c(n) * log_20(n) / n - (0 for a single residue). + where n is the length of the sequence and 20 is the size of the amino acid + alphabet. The normalised value is 0 for a single residue. Parameters ========== @@ -183,8 +209,8 @@ def lempel_ziv_complexity(sequence: str) -> pd.DataFrame: Returns ======= :lz_df: pd.DataFrame - pandas DataFrame of shape 1 x 2, columns ``LZ_Complexity`` (int) and - ``LZ_Normalized``. + pandas dataframe of LZ complexity for protein sequence. Dataframe will be + of the shape 1 x 2, with the columns LZ_Complexity and LZ_Normalized. """ #validate input protein sequence sequence = _validate_sequence(sequence) diff --git a/protpy/composition.py b/protpy/composition.py index 73eecac..27befde 100644 --- a/protpy/composition.py +++ b/protpy/composition.py @@ -239,7 +239,33 @@ def dipeptide_composition(sequence: str, region: str | None = None, n: int = 25) return dipeptide_composition_df def _regional(func, sequence: str, region: str, n: int) -> pd.DataFrame: - """Apply ``func`` to the N-terminal, C-terminal or split (N/middle/C) regions of ``sequence``.""" + """ + Calculate a composition descriptor over part of a protein sequence. The + N-terminal ("nterm"), C-terminal ("cterm") or N-terminal, middle and + C-terminal ("split") regions are passed to the descriptor function and the + resulting columns are prefixed with NT_, MID_ and CT_, respectively. Split + composition was used for predicting mitochondrial proteins and antibacterial + peptides [28, 29]. + + Parameters + ========== + :func: function + composition descriptor function to apply to each region, e.g. + amino_acid_composition. + :sequence: str + validated protein sequence. + :region: str + region of the sequence to use: "nterm", "cterm" or "split". + :n: int + length of the N- and C-terminal regions. + + Returns + ======= + :regional_df: pd.DataFrame + pandas dataframe of the descriptor values for each region, with the + region prefix added to each column. For "split" the dataframe will be + of the shape 1 x 3N, where N is the number of features of the descriptor. + """ n = _validate_int("n", n, 1) parts = {"nterm": [("NT_", sequence[:n])], "cterm": [("CT_", sequence[-n:])], "split": [("NT_", sequence[:n]), ("MID_", sequence[n:-n]), ("CT_", sequence[-n:])]} @@ -507,7 +533,20 @@ def instability_index(sequence: str) -> pd.DataFrame: } def _pka_set(pka_scale: str) -> dict: - """Return the pKa set named ``pka_scale`` from residue_tables.json.""" + """ + Get a set of pKa values used in the isoelectric point and charge + calculations from the residue_tables.json data file. + + Parameters + ========== + :pka_scale: str + name of the pKa set: "protpy", "emboss", "bjellqvist" or "ipc_protein". + + Returns + ======= + :pka_set: dict + dict of pKa values for each ionisable group. + """ sets = _load_json_table("residue_tables.json", "pka_sets")["values"] if pka_scale not in sets: raise ValueError(f"Invalid pka_scale '{pka_scale}'. Valid scales are: {list(sets)}.") @@ -890,7 +929,22 @@ def reduced_alphabet_composition(sequence: str, alphabet_size: int = 6, alphabet def _named_alphabet(alphabet: str) -> list[tuple[str, str]]: - """Return [(label, residues), ...] for a named partition alphabet in reduced_alphabets.json.""" + """ + Get the groups of a named reduced amino acid alphabet from the + reduced_alphabets.json data file. The available alphabets are the 5-group + iFeature alphabet (ifeature5) [21] and the cp13, cp14, cp19 and cp20 cluster + profiles [24]. + + Parameters + ========== + :alphabet: str + name of the reduced alphabet: "ifeature5", "cp13", "cp14", "cp19" or "cp20". + + Returns + ======= + :groups: list + list of (label, residues) tuples, one per group of the alphabet. + """ valid = ["ifeature5", "cp13", "cp14", "cp19", "cp20"] if alphabet not in valid: raise ValueError(f"Invalid alphabet '{alphabet}'. Valid alphabets are: {valid}.") @@ -1687,26 +1741,35 @@ def amphiphilic_correllation_function(Ri: str = "S", Rj: str = "D", def cksaap(sequence: str, gap: int = 5) -> pd.DataFrame: """ - Calculate the Composition of k-Spaced Amino Acid Pairs (CKSAAP) of a protein - sequence [20]. For each spacing g = 0..gap, every residue pair (s_i, s_{i+g+1}) - is counted and divided by the number of such pairs: + Calculate Composition of k-Spaced Amino Acid Pairs (CKSAAP) of protein + sequence. CKSAAP describes the frequency of each amino acid pair separated + by g residues within a protein sequence, for each gap g in the range + 0..gap. With 400 possible amino acid pairs per gap this creates a + 400 * (gap+1) Dimensional vector, calculated as: - CKSAAP(XY, g) = N_g(XY) / (L - g - 1) x 100 + CKSAAP(XY, g) = N_g(XY) / (L - g - 1) * 100 - g = 0 is the dipeptide composition. Values are percentages; a spacing with no - pairs (L - g - 1 <= 0) gives zeros. + where CKSAAP(XY, g) is the composition of the amino acid pair XY with g + residues between X and Y, N_g(XY) is the number of such pairs in the sequence + and L is the length of the sequence [20]. A gap of 0 is the same as the + dipeptide composition. A gap with no pairs (L - g - 1 <= 0) gives values + of 0. Parameters ========== :sequence: str protein sequence. :gap: int (default=5) - maximum number of residues between the two residues of a pair. + maximum number of residues between the two amino acids of a pair. Returns ======= :cksaap_df: pd.DataFrame - pandas DataFrame of shape 1 x 400·(gap+1), columns ``CKSAAP_{XY}_gap{g}``. + pandas dataframe of CKSAAP values for protein sequence. Dataframe will + be of the shape 1 x 400 * (gap+1), where 400 * (gap+1) is the number of + features calculated from the descriptor (400 amino acid pairs for each gap) + and 1 is the input sequence. By default, the shape will be 1 x 2400. Column + names are of the form CKSAAP_XY_gapg, e.g. CKSAAP_AC_gap2. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1725,27 +1788,33 @@ def cksaap(sequence: str, gap: int = 5) -> pd.DataFrame: def cksaagp(sequence: str, gap: int = 5) -> pd.DataFrame: """ - Calculate the Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP) [21]. - As CKSAAP, but residues are first mapped to the five iFeature groups: - aliphatic (GAVLMI), aromatic (FYW), positive (KRH), negative (DE) and - uncharged (STCPNQ). + Calculate Composition of k-Spaced Amino Acid Group Pairs (CKSAAGP) of protein + sequence. CKSAAGP is the same as CKSAAP but the amino acids are first mapped + to 5 groups: aliphatic (GAVLMI), aromatic (FYW), positive charge (KRH), + negative charge (DE) and uncharged (STCPNQ). With 25 possible group pairs + per gap this creates a 25 * (gap+1) Dimensional vector, calculated as: - CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) x 100 + CKSAAGP(G1G2, g) = N_g(G1G2) / (L - g - 1) * 100 - Values are percentages and the 25 values for each spacing sum to 100 - (0 when the spacing has no pairs). + where CKSAAGP(G1G2, g) is the composition of the group pair G1G2 with g + residues between them, N_g(G1G2) is the number of such pairs in the sequence + and L is the length of the sequence [21]. The 25 values for each gap sum to 100. Parameters ========== :sequence: str protein sequence. :gap: int (default=5) - maximum number of residues between the two residues of a pair. + maximum number of residues between the two amino acids of a pair. Returns ======= :cksaagp_df: pd.DataFrame - pandas DataFrame of shape 1 x 25·(gap+1), columns ``CKSAAGP_{G1}.{G2}_gap{g}``. + pandas dataframe of CKSAAGP values for protein sequence. Dataframe will + be of the shape 1 x 25 * (gap+1), where 25 * (gap+1) is the number of + features calculated from the descriptor (25 group pairs for each gap) and + 1 is the input sequence. By default, the shape will be 1 x 150. Column + names are of the form CKSAAGP_G1.G2_gapg, e.g. CKSAAGP_aliphatic.aromatic_gap0. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1766,27 +1835,37 @@ def cksaagp(sequence: str, gap: int = 5) -> pd.DataFrame: def dde(sequence: str) -> pd.DataFrame: """ - Calculate the Dipeptide Deviation from Expected Mean (DDE) [22]. The observed - dipeptide composition is standardised against the composition expected from - the number of codons coding each residue: + Calculate Dipeptide Deviation from Expected Mean (DDE) of protein sequence. + DDE compares the observed dipeptide composition of a protein sequence with + the composition expected from the number of codons that code for each amino + acid. For each of the 400 dipeptides it is calculated as: Dc(XY) = N(XY) / (L - 1) - Tm(XY) = (C_X / 61) x (C_Y / 61) - Tv(XY) = Tm(XY) x (1 - Tm(XY)) / (L - 1) + + Tm(XY) = (C_X / 61) * (C_Y / 61) + + Tv(XY) = Tm(XY) * (1 - Tm(XY)) / (L - 1) + DDE(XY) = (Dc(XY) - Tm(XY)) / sqrt(Tv(XY)) - where C_X is the number of sense codons for residue X in the standard genetic - code (61 in total). DDE is a standardised statistic, not a percentage. + where Dc(XY) is the observed composition of dipeptide XY, N(XY) is the + number of XY dipeptides, L is the length of the sequence, Tm(XY) and Tv(XY) + are the theoretical mean and variance, and C_X and C_Y are the number of + codons for amino acids X and Y out of the 61 sense codons [22]. Unlike the + other composition descriptors, DDE values are not percentages. Parameters ========== :sequence: str - protein sequence, at least 2 residues. + protein sequence, at least 2 residues long. Returns ======= :dde_df: pd.DataFrame - pandas DataFrame of shape 1 x 400, columns ``DDE_{XY}``. + pandas dataframe of DDE values for protein sequence. Dataframe will be of + the shape 1 x 400, where 400 is the number of features calculated from the + descriptor (20^2 for the 20 canonical amino acids) and 1 is the input + sequence. Column names are of the form DDE_XY. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1808,13 +1887,17 @@ def dde(sequence: str) -> pd.DataFrame: def asdc(sequence: str) -> pd.DataFrame: """ - Calculate the Adaptive Skip Dipeptide Composition (ASDC) [23]: the composition - of every ordered residue pair (s_i, s_j) with i < j, over all gaps: + Calculate Adaptive Skip Dipeptide Composition (ASDC) of protein sequence. + ASDC is a modified dipeptide composition that counts every ordered pair of + amino acids (s_i, s_j), where i < j, at all gaps along the sequence, rather + than just adjacent pairs, calculated as: - ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) x 100 + ASDC(XY) = sum_g N_g(XY) / sum_g sum_XY N_g(XY) * 100 - The denominator is the total number of ordered pairs, L(L-1)/2. Values are - percentages and sum to 100 (zeros for a single residue). + where ASDC(XY) is the composition of the amino acid pair XY, N_g(XY) is the + number of XY pairs separated by g residues, and the denominator is the total + number of ordered pairs in the sequence, L(L-1)/2, where L is the length of + the sequence [23]. The 400 values sum to 100. Parameters ========== @@ -1824,7 +1907,10 @@ def asdc(sequence: str) -> pd.DataFrame: Returns ======= :asdc_df: pd.DataFrame - pandas DataFrame of shape 1 x 400, columns ``ASDC_{XY}``. + pandas dataframe of ASDC values for protein sequence. Dataframe will be of + the shape 1 x 400, where 400 is the number of features calculated from the + descriptor (20^2 for the 20 canonical amino acids) and 1 is the input + sequence. Column names are of the form ASDC_XY. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1844,35 +1930,42 @@ def asdc(sequence: str) -> pd.DataFrame: def distance_pair(sequence: str, rng: int = 3, alphabet: str = "cp14") -> pd.DataFrame: """ - Calculate the PseAAC of distance-pairs over a reduced amino acid alphabet - (iDNA-Prot|dis) [24, 25]. Residues are mapped to the clusters of a cluster - profile cp(n) and the vector is + Calculate Distance Pair (DP) descriptor of protein sequence. The descriptor + incorporates amino acid distance pairs and a reduced amino acid alphabet into + the general pseudo amino acid composition, as used in iDNA-Prot|dis [24, 25]. + The amino acids are first mapped to the clusters of a cluster profile cp(n), + and the descriptor is the composition of the clusters followed by the + composition of cluster pairs at each distance d = 1..rng: - [f^0(1..G), f^1(1..G^2), ..., f^d(1..G^2)] + DP(c) = N(c) / L * 100 - where f^0 is the cluster composition (count / L) and f^d, d = 1..rng, is the - composition of cluster pairs (s_i, s_{i+d}) (count / (L - d)); d = 1 is the - nearest-neighbour pair. This follows Eqs. 5-12 of [24], giving G + G^2·rng - values (e.g. 14 + 14x14x3 = 602 for cp14, rng=3). Values are percentages. + DP(c1c2, d) = N_d(c1c2) / (L - d) * 100 - Alphabets: cp13, cp14, cp19, cp20 (no reduction) from Eq. 10 of [24]. The - printed cp(14) lists E twice; the stray E in the first cluster is dropped, - the only reading with 14 clusters. + where N(c) is the number of residues in cluster c, N_d(c1c2) is the number of + residue pairs (s_i, s_i+d) whose residues are in clusters c1 and c2, and L is + the length of the sequence. A distance of 1 is the nearest-neighbour pair. + The available cluster profiles are cp13, cp14, cp19 and cp20 (no reduction). + The published cp(14) lists E twice; the extra E in the first cluster is + dropped, as it is the only reading that gives 14 clusters. Parameters ========== :sequence: str protein sequence. :rng: int (default=3) - maximum pair distance d. + maximum distance between the two residues of a pair. :alphabet: str (default="cp14") - cluster profile: "cp13", "cp14", "cp19" or "cp20". + cluster profile to use: "cp13", "cp14", "cp19" or "cp20". Returns ======= :distance_pair_df: pd.DataFrame - pandas DataFrame of shape 1 x (G + G^2·rng), columns ``DP_{c}`` and - ``DP_{c1}.{c2}_d{d}`` where c are cluster labels (their residues). + pandas dataframe of DP values for protein sequence. Dataframe will be of + the shape 1 x (G + G^2 * rng), where G is the number of clusters, + G + G^2 * rng is the number of features calculated from the descriptor and + 1 is the input sequence. By default (cp14, rng=3) the shape will be 1 x 602. + Column names are of the form DP_c and DP_c1.c2_dd, where c are the cluster + residues, e.g. DP_IMV and DP_IMV.L_d1. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1897,13 +1990,19 @@ def distance_pair(sequence: str, rng: int = 3, alphabet: str = "cp14") -> pd.Dat def natural_vector(sequence: str) -> pd.DataFrame: """ - Calculate the Natural Vector of a protein sequence [26]. For each amino acid k: + Calculate Natural Vector (NV) of protein sequence. The natural vector + describes the number of each amino acid in a protein sequence together with + the mean and the normalized central moment of their positions, calculated as: + + n_k = number of occurrences of amino acid k + + mu_k = sum of the positions of k / n_k - n_k = number of occurrences of k - mu_k = mean (1-based) position of k - D2_k = sum_i (pos_i - mu_k)^2 / (n_k x L) + D2_k = sum of (position of k - mu_k)^2 / (n_k * L) - mu_k and D2_k are 0 when k does not occur. Values are not percentages. + where positions start at 1, L is the length of the sequence and k is each of + the 20 canonical amino acids [26]. mu_k and D2_k are 0 when amino acid k is + not in the sequence. Parameters ========== @@ -1913,8 +2012,10 @@ def natural_vector(sequence: str) -> pd.DataFrame: Returns ======= :natural_vector_df: pd.DataFrame - pandas DataFrame of shape 1 x 60, columns ``NV_n_{X}``, ``NV_mu_{X}`` and - ``NV_D2_{X}``. + pandas dataframe of NV values for protein sequence. Dataframe will be of + the shape 1 x 60, where 60 is the number of features calculated from the + descriptor (3 values for each of the 20 amino acids) and 1 is the input + sequence. Column names are of the form NV_n_X, NV_mu_X and NV_D2_X. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -1935,16 +2036,18 @@ def natural_vector(sequence: str) -> pd.DataFrame: def atomic_composition(sequence: str, subtract_water: bool = False) -> pd.DataFrame: """ - Calculate the Atomic Composition (ATC) of a protein sequence [27]: the - percentage of C, H, N, O and S atoms, summed over the molecular formulas of - the residues' free amino acids. + Calculate Atomic Composition (ATC) of protein sequence. ATC describes the + percentage of carbon, hydrogen, nitrogen, oxygen and sulphur atoms in a + protein sequence, using the molecular formulas of the free amino acids, + calculated as: - ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) x 100 + ATC(e) = N_e / (N_C + N_H + N_N + N_O + N_S) * 100 - By default free amino-acid formulas are summed without removing water, - matching Pfeature [27]. With ``subtract_water=True``, (L - 1) H2O are removed - for the peptide bonds, giving the formula of the linear peptide (e.g. "GG" - -> C4H8N2O3). + where ATC(e) is the atomic composition of element e and N_e is the number of + atoms of element e summed over all residues [27]. By default the formulas of + the free amino acids are summed without removing water, matching Pfeature + [27]. With subtract_water=True, one water molecule is removed for each of the + L - 1 peptide bonds, giving the formula of the peptide (e.g. "GG" -> C4H8N2O3). Parameters ========== @@ -1956,8 +2059,10 @@ def atomic_composition(sequence: str, subtract_water: bool = False) -> pd.DataFr Returns ======= :atomic_composition_df: pd.DataFrame - pandas DataFrame of shape 1 x 5, columns ``ATC_C``, ``ATC_H``, ``ATC_N``, - ``ATC_O`` and ``ATC_S``. Values are percentages. + pandas dataframe of ATC values for protein sequence. Dataframe will be of + the shape 1 x 5, where 5 is the number of features calculated from the + descriptor (one per element) and 1 is the input sequence. Column names + are ATC_C, ATC_H, ATC_N, ATC_O and ATC_S. """ #validate input protein sequence sequence = _validate_sequence(sequence) diff --git a/protpy/encode.py b/protpy/encode.py index 135071d..13da35c 100644 --- a/protpy/encode.py +++ b/protpy/encode.py @@ -86,7 +86,21 @@ def _set_members(descriptor_set: str) -> list[str]: - """Registry descriptors (outside the classic set) that belong to ``descriptor_set``, in registry order.""" + """ + Get the descriptors outside the classic set that belong to a descriptor set, + in the order of the descriptor registry. Membership is taken from the + descriptor_sets field of each registry entry. + + Parameters + ========== + :descriptor_set: str + descriptor set: "classic", "extended" or "all". + + Returns + ======= + :members: list[str] + names of the descriptors added by the set; empty for "classic". + """ if descriptor_set == "classic": return [] return [name for name, meta in _DESCRIPTOR_REGISTRY.items() diff --git a/protpy/physicochemical.py b/protpy/physicochemical.py index f815669..5e11f56 100644 --- a/protpy/physicochemical.py +++ b/protpy/physicochemical.py @@ -68,13 +68,21 @@ def charge_fractions(sequence: str) -> pd.DataFrame: """ - Calculate the charge fractions of a protein sequence [1, 2]: + Calculate the Charge Fractions (FCR) of protein sequence. The fraction of + charged residues (FCR), the fractions of positive and negative residues and + the overall charge asymmetry (sigma) describe the charge content of a protein + sequence, and are key parameters of the conformations of intrinsically + disordered proteins [1, 2]. They are calculated as: - FCR = (n+ + n-) / L fraction of charged residues - FPlus = n+ / L, FMinus = n- / L - Sigma = NCPR^2 / FCR overall charge asymmetry (0 if FCR = 0) + FCR = (n+ + n-) / L - where n+ counts K/R and n- counts D/E (H is neutral). Values are fractions. + FPlus = n+ / L, FMinus = n- / L + + Sigma = NCPR^2 / FCR + + where n+ is the number of positive residues (K, R), n- the number of negative + residues (D, E), L is the length of the sequence and NCPR = (n+ - n-) / L. His + is treated as neutral and sigma is 0 when there are no charged residues. Parameters ========== @@ -84,8 +92,10 @@ def charge_fractions(sequence: str) -> pd.DataFrame: Returns ======= :charge_fractions_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``Charge_FCR``, ``Charge_FPlus``, - ``Charge_FMinus`` and ``Charge_Sigma``. + pandas dataframe of charge fractions for protein sequence. Dataframe will + be of the shape 1 x 4, where 4 is the number of features calculated from + the descriptor and 1 is the input sequence. Column names are Charge_FCR, + Charge_FPlus, Charge_FMinus and Charge_Sigma. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -101,11 +111,13 @@ def charge_fractions(sequence: str) -> pd.DataFrame: def ncpr(sequence: str) -> pd.DataFrame: """ - Calculate the Net Charge Per Residue (NCPR) of a protein sequence [1]: + Calculate the Net Charge Per Residue (NCPR) of protein sequence. NCPR is the + net charge of a protein sequence divided by its length [1], calculated as: NCPR = (n+ - n-) / L - with n+ counting K/R and n- counting D/E. + where n+ is the number of positive residues (K, R), n- the number of negative + residues (D, E) and L is the length of the sequence. Parameters ========== @@ -115,7 +127,8 @@ def ncpr(sequence: str) -> pd.DataFrame: Returns ======= :ncpr_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``NCPR``. + pandas dataframe of NCPR for protein sequence. Dataframe will be of the + shape 1 x 1, with the column NCPR. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -124,7 +137,29 @@ def ncpr(sequence: str) -> pd.DataFrame: #################################### Kappa ################################################ def _blob_sigma(pos: np.ndarray, neg: np.ndarray, g: int) -> np.ndarray: - """Charge asymmetry (f+ - f-)^2 / (f+ + f-) of every overlapping blob of size g (rows = candidates).""" + """ + Calculate the charge asymmetry of every overlapping blob (window) of size g + along one or more sequences, as used in the kappa calculation [2], where: + + sigma_b = (f+ - f-)^2 / (f+ + f-) + + and f+ and f- are the fractions of positive and negative residues in the blob. + Uncharged blobs have a charge asymmetry of 0. + + Parameters + ========== + :pos: np.ndarray + 1 for positive residues and 0 otherwise; one row per sequence when 2-D. + :neg: np.ndarray + 1 for negative residues and 0 otherwise, same shape as pos. + :g: int + blob size. + + Returns + ======= + :blob_sigma: np.ndarray + charge asymmetry of each blob, with one row per sequence when 2-D. + """ kernel = np.ones(g) if pos.ndim == 1: p = np.convolve(pos, kernel, mode="valid") @@ -139,19 +174,58 @@ def _blob_sigma(pos: np.ndarray, neg: np.ndarray, g: int) -> np.ndarray: def _delta(pos: np.ndarray, neg: np.ndarray, g: int, sigma: float) -> np.ndarray: + """ + Calculate the mean squared deviation (delta) of the blob charge asymmetries + from the charge asymmetry of the whole sequence, as used in the kappa + calculation [2]. + + Parameters + ========== + :pos: np.ndarray + 1 for positive residues and 0 otherwise; one row per sequence when 2-D. + :neg: np.ndarray + 1 for negative residues and 0 otherwise, same shape as pos. + :g: int + blob size. + :sigma: float + charge asymmetry of the whole sequence. + + Returns + ======= + :delta: np.ndarray + delta of each sequence (a single value for a 1-D input). + """ return np.mean((_blob_sigma(pos, neg, g) - sigma) ** 2, axis=-1) def _delta_max(n_pos: int, n_neg: int, length: int, g: int, sigma: float) -> float: """ - Maximal delta over the most segregated arrangements of the composition: a block of - positives and a block of negatives, with the neutral residues split across the three - gaps around them (left a, between b, right c). - - Blobs lying entirely in a neutral flank contribute (0 - sigma)^2 whatever the flank's - length, so for a given b only flank lengths up to g - 1 are distinct. Trying every b - with those a/c splits covers every distinct delta in O(n0 x g) candidates instead of - the O(n0^2) of a full enumeration. + Calculate the maximum delta of a sequence composition, used to normalise kappa + [2]. The maximum is taken over the most segregated arrangements of the + composition: a block of positive residues and a block of negative residues, + with the neutral residues split across the three gaps around them. Blobs lying + entirely in a neutral flank contribute the same value whatever the length of + the flank, so for each gap between the blocks only flank lengths up to g - 1 + need to be tried. This evaluates every distinct arrangement in O(n0 * g) + candidates, where n0 is the number of neutral residues. + + Parameters + ========== + :n_pos: int + number of positive residues. + :n_neg: int + number of negative residues. + :length: int + length of the sequence. + :g: int + blob size. + :sigma: float + charge asymmetry of the whole sequence. + + Returns + ======= + :best: float + maximum delta over the segregated arrangements. """ n0 = length - n_pos - n_neg candidates = set() @@ -174,37 +248,41 @@ def _delta_max(n_pos: int, n_neg: int, length: int, g: int, sigma: float) -> flo def kappa(sequence: str, blob_sizes: tuple[int, ...] = (5, 6)) -> pd.DataFrame: """ - Calculate the kappa charge-patterning parameter of Das & Pappu [2]. For blob - size g, the sequence is split into the L - g + 1 overlapping blobs and + Calculate the Kappa (Kappa) charge patterning parameter of protein sequence. + Kappa describes how well oppositely charged residues are mixed or segregated + along a protein sequence, which strongly affects the conformations of + intrinsically disordered proteins [2]. The sequence is split into overlapping + blobs of g residues and kappa is calculated as: - sigma_b = (f+ - f-)^2 / (f+ + f-) per blob (0 if uncharged) - delta_g = mean over blobs of (sigma_b - sigma)^2 - kappa_g = delta_g / delta_max,g + sigma_b = (f+ - f-)^2 / (f+ + f-) - where sigma = NCPR^2 / FCR of the whole sequence and delta_max,g is the - largest delta over the maximally segregated arrangements of the same - composition (a positive block and a negative block with the neutral residues - distributed over the three surrounding gaps; every distinct arrangement is - evaluated). kappa is the mean of kappa_g - over ``blob_sizes`` (5 and 6 in [2]); 0 means well mixed and 1 fully - segregated. Charges: K, R = +1; D, E = -1; H = 0. + delta_g = mean over all blobs of (sigma_b - sigma)^2 - kappa is reported as 0 (not NaN) when the sequence has no charges or only one - charge type, where oppositely charged residues cannot be segregated. A blob - size longer than the sequence is reduced to the sequence length. + kappa_g = delta_g / delta_max,g + + where sigma_b is the charge asymmetry of a blob, f+ and f- are its fractions of + positive (K, R) and negative (D, E) residues, sigma = NCPR^2 / FCR is the charge + asymmetry of the whole sequence and delta_max,g is the largest delta over the + most segregated arrangements of the same composition. The final kappa is the + mean of kappa_g over the blob sizes (5 and 6 by default). Kappa ranges from 0 + (well mixed) to 1 (fully segregated). It is reported as 0 when the sequence has + no charged residues or only one type of charge, and blobs longer than the + sequence are reduced to its length. Parameters ========== :sequence: str protein sequence. - :blob_sizes: tuple[int, ...] (default=(5, 6)) - blob sizes g to average over. + :blob_sizes: tuple (default=(5, 6)) + blob sizes g to average kappa over. Returns ======= :kappa_df: pd.DataFrame - pandas DataFrame of shape 1 x (1 + len(blob_sizes)), columns ``Kappa`` and - ``Kappa_{g}``. + pandas dataframe of kappa values for protein sequence. Dataframe will be of + the shape 1 x (1 + number of blob sizes), where the first column (Kappa) is + the mean and the remaining columns (Kappa_g) are the values for each blob + size. By default the shape will be 1 x 3. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -230,12 +308,16 @@ def kappa(sequence: str, blob_sizes: tuple[int, ...] = (5, 6)) -> pd.DataFrame: def sequence_charge_decoration(sequence: str) -> pd.DataFrame: """ - Calculate the Sequence Charge Decoration (SCD) of Sawle & Ghosh [3]: + Calculate the Sequence Charge Decoration (SCD) of protein sequence. SCD + describes the patterning of charges along a protein sequence, weighting each + pair of charges by the square root of their separation [3]. It is calculated as: - SCD = (1/L) sum_{m=2}^{L} sum_{n=1}^{m-1} q_m q_n (m - n)^{1/2} + SCD = (1/L) * sum_{m=2}^{L} sum_{n=1}^{m-1} q_m * q_n * (m - n)^(1/2) - with q = +1 for K/R, -1 for D/E and 0 otherwise. More negative values - indicate more segregated opposite charges; uncharged sequences give 0. + where q_m and q_n are the charges of the residues at positions m and n (+1 for + K and R, -1 for D and E, 0 otherwise) and L is the length of the sequence. More + negative values indicate more segregated opposite charges and uncharged + sequences have an SCD of 0. Parameters ========== @@ -245,7 +327,8 @@ def sequence_charge_decoration(sequence: str) -> pd.DataFrame: Returns ======= :scd_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``Charge_SCD``. + pandas dataframe of SCD for protein sequence. Dataframe will be of the + shape 1 x 1, with the column Charge_SCD. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -262,24 +345,28 @@ def sequence_charge_decoration(sequence: str) -> pd.DataFrame: def charge_density(sequence: str, ph: float = 7.4) -> pd.DataFrame: """ - Calculate the charge density of a protein sequence [4]: the net charge at - ``ph`` (from ``charge_distribution``) divided by the molecular weight (from - ``molecular_weight``), in charges per Da. + Calculate the Charge Density of protein sequence. Charge density is the net + charge of a protein sequence at a given pH divided by its molecular weight [4], + calculated as: ChargeDensity = NetCharge(pH) / MW + where NetCharge(pH) is calculated using the charge_distribution function and MW + using the molecular_weight function. + Parameters ========== :sequence: str protein sequence. :ph: float (default=7.4) - pH at which the net charge is evaluated. + pH at which to calculate the net charge. Returns ======= :charge_density_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``ChargeDensity``. The value is - rounded to 6 dp because it is typically of order 1e-4. + pandas dataframe of charge density (charges per Da) for protein sequence. + Dataframe will be of the shape 1 x 1, with the column ChargeDensity. The + value is rounded to 6 decimal places because it is typically of order 1e-4. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -293,33 +380,42 @@ def charge_density(sequence: str, ph: float = 7.4) -> pd.DataFrame: def hydrophobicity_profile(sequence: str, scale: str = "kyte_doolittle", window: int = 9, edge: float = 1.0, threshold: float | None = None) -> pd.DataFrame: """ - Summarise a sliding-window hydrophobicity profile [5, 6]. Each residue is - replaced by its scale value and a window of length ``window`` is slid along - the sequence; ``edge < 1`` weights the window ends linearly down to ``edge`` - (ExPASy ProtScale semantics). The profile is reduced to its mean, max, min - and standard deviation, plus the fraction of windows above ``threshold`` - when one is given (e.g. 1.6 for Kyte-Doolittle transmembrane segments). - Windows longer than the sequence use the whole sequence. + Calculate the Hydrophobicity Profile (HP) of protein sequence. Each amino acid + in the sequence is replaced by its value on a hydrophobicity scale and the + values are averaged over a sliding window, giving a profile along the sequence + [5, 6]. The profile is summarised by its mean, maximum, minimum and standard + deviation, and optionally the fraction of windows above a threshold, e.g. 1.6 + for transmembrane segments with the Kyte-Doolittle scale [5]. The window value + is calculated as: + + HP(i) = sum_j w_j * H(s_i+j) / sum_j w_j + + where H is the hydrophobicity scale and w_j are the window weights. With + edge=1.0 all weights are 1 (a plain mean); with edge < 1 the weights fall + linearly from 1 at the centre of the window to edge at its ends, as in ExPASy + ProtScale. Windows longer than the sequence use the whole sequence. Parameters ========== :sequence: str protein sequence. :scale: str (default="kyte_doolittle") - single-value scale: "kyte_doolittle", "hopp_woods", "eisenberg", - "wimley_white", "engelman", "domcut", or any AAindex1 accession code. + single-value hydrophobicity scale: "kyte_doolittle", "hopp_woods", + "eisenberg", "wimley_white", "engelman", "domcut" or any AAindex + accession number. :window: int (default=9) - window length. + length of the sliding window. :edge: float (default=1.0) - relative weight of the window ends, 0-1. + relative weight of the ends of the window, between 0 and 1. :threshold: float | None (default=None) - if set, add the fraction of windows with a value above it. + if set, also calculate the fraction of windows with a value above it. Returns ======= :profile_df: pd.DataFrame - pandas DataFrame of shape 1 x 4 (1 x 5 with ``threshold``), columns - ``HP_{scale}_w{window}_{mean,max,min,std[,frac_above]}``. + pandas dataframe of hydrophobicity profile statistics for protein sequence. + Dataframe will be of the shape 1 x 4 (1 x 5 with a threshold), with the + columns HP__w_mean, _max, _min, _std and _frac_above. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -341,10 +437,14 @@ def hydrophobicity_profile(sequence: str, scale: str = "kyte_doolittle", window: def hydrophobic_ratio(sequence: str) -> pd.DataFrame: """ - Calculate the hydrophobic ratio of the Antimicrobial Peptide Database (APD) - [7]: the percentage of residues that are L, I, V, F, M, A, C or W. + Calculate the Hydrophobic Ratio of protein sequence. The hydrophobic ratio is + the percentage of hydrophobic residues in a protein sequence, as defined by + the Antimicrobial Peptide Database (APD) [7], calculated as: - HydrophobicRatio = N(LIVFMACW) / L x 100 + HydrophobicRatio = N(LIVFMACW) / L * 100 + + where N(LIVFMACW) is the number of L, I, V, F, M, A, C and W residues and L + is the length of the sequence. Parameters ========== @@ -354,7 +454,8 @@ def hydrophobic_ratio(sequence: str) -> pd.DataFrame: Returns ======= :hydrophobic_ratio_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``HydrophobicRatio`` (percentage). + pandas dataframe of hydrophobic ratio for protein sequence. Dataframe will + be of the shape 1 x 1, with the column HydrophobicRatio. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -364,14 +465,15 @@ def hydrophobic_ratio(sequence: str) -> pd.DataFrame: def half_life(sequence: str) -> pd.DataFrame: """ - Estimate the in vivo half-life of a protein from its N-terminal residue by - the N-end rule [8, 9, 10], as tabulated by ExPASy ProtParam [6]: mammalian - reticulocytes (in vitro), yeast and E. coli (in vivo). - - Values are in hours. Lower bounds in the source (">20 h" in yeast, ">10 h" - in E. coli, ">20 h" for mammalian Pro) are reported as the bound. The E. coli - value for an N-terminal Pro is unknown in the source and returned as NaN. - Not applicable to N-terminally modified proteins. + Calculate the estimated Half Life of protein sequence. The half-life is a + prediction of the time it takes for half of the amount of a protein in a cell + to disappear after its synthesis. It is estimated from the N-terminal residue + using the N-end rule [8, 9, 10], for 3 model systems: mammalian reticulocytes + (in vitro), yeast and E. coli (in vivo), using the values tabulated by ExPASy + ProtParam [6]. Values given as lower bounds in ProtParam (">20 hours" for yeast, + ">10 hours" for E. coli) are reported as the bound. The E. coli value for an + N-terminal proline is unknown in ProtParam and returned as NaN. The estimate is + not applicable to N-terminally modified proteins. Parameters ========== @@ -381,8 +483,9 @@ def half_life(sequence: str) -> pd.DataFrame: Returns ======= :half_life_df: pd.DataFrame - pandas DataFrame of shape 1 x 3, columns ``HalfLife_Mammal_h``, - ``HalfLife_Yeast_h`` and ``HalfLife_Ecoli_h``. + pandas dataframe of half-lives (hours) for protein sequence. Dataframe will + be of the shape 1 x 3, with the columns HalfLife_Mammal_h, HalfLife_Yeast_h + and HalfLife_Ecoli_h. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -395,12 +498,16 @@ def half_life(sequence: str) -> pd.DataFrame: def ivywrel_index(sequence: str) -> pd.DataFrame: """ - Calculate the IVYWREL index of Zeldovich et al. [11]: the fraction of residues - that are I, V, Y, W, R, E or L, which correlates with optimal growth - temperature. + Calculate the IVYWREL Index of protein sequence. The IVYWREL index is the + fraction of I, V, Y, W, R, E and L residues in a protein sequence, which + correlates with the optimal growth temperature of the organism [11]. It is + calculated as: IVYWREL = N(IVYWREL) / L + where N(IVYWREL) is the number of I, V, Y, W, R, E and L residues and L is the + length of the sequence. + Parameters ========== :sequence: str @@ -409,7 +516,8 @@ def ivywrel_index(sequence: str) -> pd.DataFrame: Returns ======= :ivywrel_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``IVYWREL`` (fraction). + pandas dataframe of IVYWREL index for protein sequence. Dataframe will be + of the shape 1 x 1, with the column IVYWREL. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -419,11 +527,14 @@ def ivywrel_index(sequence: str) -> pd.DataFrame: def cvp_bias(sequence: str) -> pd.DataFrame: """ - Calculate the charged-versus-polar (CvP) bias of Suhre & Claverie [12], an - indicator of hyperthermostability: + Calculate the Charged versus Polar Bias (CvP) of protein sequence. The CvP bias + is the difference between the percentages of charged and polar residues in a + protein sequence, an indicator of hyperthermostability [12], calculated as: CvP = %(D + E + K + R) - %(N + Q + S + T) + where %(X) is the percentage of residues of type X in the sequence. + Parameters ========== :sequence: str @@ -432,7 +543,8 @@ def cvp_bias(sequence: str) -> pd.DataFrame: Returns ======= :cvp_df: pd.DataFrame - pandas DataFrame of shape 1 x 1, column ``CvP_Bias`` (percentage points). + pandas dataframe of CvP bias for protein sequence. Dataframe will be of the + shape 1 x 1, with the column CvP_Bias. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -444,29 +556,30 @@ def cvp_bias(sequence: str) -> pd.DataFrame: def flexibility(sequence: str, window: int = 9) -> pd.DataFrame: """ - Summarise the flexibility profile of Vihinen et al. [13]. Average normalised - B-values (Vihinen's "average" parameter set, AAindex VINM940101) are averaged - over a window of 9 whose end weights fall linearly to 0.25 (weights 0.25, - 0.4375, 0.625, 0.8125, 1, ..., 0.25; sum 5.25), and the profile is reduced - to its mean, max, min and standard deviation. - - The neighbour-dependent parameter sets of [13] (residues with 0, 1 or 2 rigid - neighbours) are not used. Compared with Biopython's - ``ProteinAnalysis.flexibility``, which uses the same values and weights, - protpy centres each window on its middle residue and includes the last - window; Biopython takes the residue after the centre and omits the last window. + Calculate the Flexibility (Flex) profile of protein sequence. Each amino acid + in the sequence is replaced by its normalised flexibility parameter (B-value) + and the values are averaged over a sliding window of 9 residues whose end + weights fall linearly to 0.25 (weights 0.25, 0.4375, 0.625, 0.8125, 1, ..., + 0.25), as in Vihinen et al. [13]. The profile is summarised by its mean, + maximum, minimum and standard deviation. The average B-values of Vihinen et + al. are used; their parameter sets for residues with 0, 1 or 2 rigid + neighbours are not. Compared with Biopython's ProteinAnalysis.flexibility, + which uses the same values and weights, each window is centred on its middle + residue and the last window is included. Parameters ========== :sequence: str protein sequence. :window: int (default=9) - window length (the parameters were optimised for 9). + length of the sliding window (the parameters were optimised for 9). Returns ======= :flexibility_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``Flex_{mean,max,min,std}``. + pandas dataframe of flexibility profile statistics for protein sequence. + Dataframe will be of the shape 1 x 4, with the columns Flex_mean, Flex_max, + Flex_min and Flex_std. """ #validate input protein sequence sequence = _validate_sequence(sequence) diff --git a/protpy/scales.py b/protpy/scales.py index bd8e6f8..121a86f 100644 --- a/protpy/scales.py +++ b/protpy/scales.py @@ -86,30 +86,40 @@ def aa_scale_descriptors(sequence: str, scale: str, domains: int = 1) -> pd.DataFrame: """ - Calculate the mean of an amino-acid factor scale over a protein sequence. Each - residue is replaced by its d-dimensional scale vector and each dimension is - averaged: + Calculate an amino acid factor-scale descriptor of protein sequence. Factor + scales describe each amino acid with a small number of values (dimensions) + derived from many physicochemical properties, typically by principal component + or factor analysis [1-9]. Each amino acid in the sequence is replaced by its + d-dimensional scale vector and the mean of each dimension is taken over the + sequence, calculated as: - S_j = (1/L) sum_i z_j(s_i), j = 1..d + S_j = (1/L) * sum_i z_j(s_i) - With ``domains > 1`` the sequence is split into ``domains`` near-equal - contiguous segments and the per-segment means are concatenated. + where S_j is the descriptor value for dimension j (j = 1,2,..,d), z_j(s_i) is + the value of dimension j for the amino acid at position i and L is the length + of the sequence. With domains > 1 the sequence is split into that many + near-equal contiguous segments and the means of each segment are concatenated. + The individual scales are also available as their own functions, e.g. vhse(). Parameters ========== :sequence: str protein sequence. :scale: str - factor-scale family: "z_scales_5", "vhse", "atchley_factors", "t_scales", - "st_scales", "protfp", "fasgai", "blosum_indices" or "ms_whim". + factor-scale family to use: "z_scales_5", "vhse", "atchley_factors", + "t_scales", "st_scales", "protfp", "fasgai", "blosum_indices" or "ms_whim". :domains: int (default=1) - number of segments, at most the sequence length. + number of near-equal segments of the sequence to calculate the means over, + at most the length of the sequence. Returns ======= :scale_df: pd.DataFrame - pandas DataFrame of shape 1 x d·domains, columns ``{ABBR}_{j}`` or - ``{ABBR}_{j}_d{k}`` when ``domains > 1``. + pandas dataframe of factor-scale values for protein sequence. Dataframe + will be of the shape 1 x d * domains, where d is the number of dimensions + of the scale, d * domains is the number of features calculated from the + descriptor and 1 is the input sequence. Column names are of the form + ABBR_j, or ABBR_j_dk when domains > 1, e.g. VHSE_1 or VHSE_1_d2. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -129,70 +139,363 @@ def aa_scale_descriptors(sequence: str, scale: str, domains: int = 1) -> pd.Data return _to_df(values, columns) -def _scale_wrapper(scale: str, ref: str, dim: int): - def wrapper(sequence: str, domains: int = 1) -> pd.DataFrame: - return aa_scale_descriptors(sequence, scale, domains) - wrapper.__name__ = scale - wrapper.__qualname__ = scale - wrapper.__doc__ = f""" - Calculate the mean {scale} factor-scale descriptor of a protein sequence {ref}. - See ``aa_scale_descriptors`` for the formula. +def z_scales_5(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Sandberg 5 z-scales (Z5) of protein sequence. The z-scales are the + principal components of a set of physicochemical properties measured for 87 + coded and non-coded amino acids [1]. z1 mainly describes lipophilicity, z2 + steric bulk and polarizability, z3 polarity and charge, and z4 and z5 + electronegativity, heat of formation, electrophilicity and hardness. + + Each amino acid in the sequence is replaced by its 5 z-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. Parameters ========== :sequence: str protein sequence. :domains: int (default=1) - number of near-equal segments whose means are concatenated. + number of near-equal segments of the sequence to calculate the means over. Returns ======= - :scale_df: pd.DataFrame - pandas DataFrame of shape 1 x {dim}·domains, columns ``{factor_scales_[scale]}_{{j}}``. + :z_scales_5_df: pd.DataFrame + pandas dataframe of Z5 values for protein sequence. Dataframe will be + of the shape 1 x 5 * domains, where 5 * domains is the number of + features calculated from the descriptor (5 scales for each domain) and + 1 is the input sequence. Column names are of the form Z5_j, or + Z5_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "z_scales_5", domains) + +def vhse(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Vectors of Hydrophobic, Steric and Electronic properties (VHSE) of + protein sequence. The VHSE scales are principal components score vectors derived + from physicochemical variables of the 20 coded amino acids [2]. VHSE1-2 describe + hydrophobic properties, VHSE3-4 steric properties and VHSE5-8 electronic + properties. + + Each amino acid in the sequence is replaced by its 8 VHSE scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :vhse_df: pd.DataFrame + pandas dataframe of VHSE values for protein sequence. Dataframe will be + of the shape 1 x 8 * domains, where 8 * domains is the number of + features calculated from the descriptor (8 scales for each domain) and + 1 is the input sequence. Column names are of the form VHSE_j, or + VHSE_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "vhse", domains) + +def atchley_factors(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Atchley Factors (AF) of protein sequence. The Atchley factors are 5 + factors obtained from a factor analysis of 54 amino acid attributes [3]. Factor + I mainly reflects polarity, accessibility and hydrophobicity, factor II + propensity for secondary structure, factor III molecular size, factor IV + relative amino acid composition and factor V electrostatic charge. + + Each amino acid in the sequence is replaced by its 5 Atchley factors and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :atchley_factors_df: pd.DataFrame + pandas dataframe of AF values for protein sequence. Dataframe will be + of the shape 1 x 5 * domains, where 5 * domains is the number of + features calculated from the descriptor (5 scales for each domain) and + 1 is the input sequence. Column names are of the form AF_j, or + AF_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "atchley_factors", domains) + +def t_scales(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Topological Scales (T-scales) of protein sequence. The T-scales are + principal components of topological descriptors of amino acids, describing their + structural and topological characteristics [4]. + + Each amino acid in the sequence is replaced by its 5 T-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :t_scales_df: pd.DataFrame + pandas dataframe of T values for protein sequence. Dataframe will be + of the shape 1 x 5 * domains, where 5 * domains is the number of + features calculated from the descriptor (5 scales for each domain) and + 1 is the input sequence. Column names are of the form T_j, or + T_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "t_scales", domains) + +def st_scales(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Structural Topology Scales (ST-scales) of protein sequence. The ST- + scales are principal components of 3D structural and topological properties of + amino acids [5]. + + Each amino acid in the sequence is replaced by its 8 ST-scales and the mean of + each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :st_scales_df: pd.DataFrame + pandas dataframe of ST values for protein sequence. Dataframe will be + of the shape 1 x 8 * domains, where 8 * domains is the number of + features calculated from the descriptor (8 scales for each domain) and + 1 is the input sequence. Column names are of the form ST_j, or + ST_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "st_scales", domains) + +def protfp(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate ProtFP (ProtFP) of protein sequence. ProtFP is a set of 8 principal + components calculated from a selection of physicochemical properties in the + AAindex database [6]. + + Each amino acid in the sequence is replaced by its 8 ProtFP components and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :protfp_df: pd.DataFrame + pandas dataframe of ProtFP values for protein sequence. Dataframe will be + of the shape 1 x 8 * domains, where 8 * domains is the number of + features calculated from the descriptor (8 scales for each domain) and + 1 is the input sequence. Column names are of the form ProtFP_j, or + ProtFP_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "protfp", domains) + +def fasgai(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Factor Analysis Scales of Generalized Amino Acid Information (FASGAI) + of protein sequence. The FASGAI scales are 6 factors obtained from a factor + analysis of amino acid properties, describing hydrophobicity, alpha and turn + propensities, bulky properties, compositional characteristics, local flexibility + and electronic properties [7]. + + Each amino acid in the sequence is replaced by its 6 FASGAI factors and the mean + of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :fasgai_df: pd.DataFrame + pandas dataframe of FASGAI values for protein sequence. Dataframe will be + of the shape 1 x 6 * domains, where 6 * domains is the number of + features calculated from the descriptor (6 scales for each domain) and + 1 is the input sequence. Column names are of the form FASGAI_j, or + FASGAI_j_dk when domains > 1. """ - return wrapper - -z_scales_5 = _scale_wrapper("z_scales_5", "(Sandberg 5 z-scales) [1]", 5) -vhse = _scale_wrapper("vhse", "(principal components score Vectors of Hydrophobic, Steric and Electronic properties) [2]", 8) -atchley_factors = _scale_wrapper("atchley_factors", "(Atchley factors I-V) [3]", 5) -t_scales = _scale_wrapper("t_scales", "(topological T-scales) [4]", 5) -st_scales = _scale_wrapper("st_scales", "(structural topology ST-scales) [5]", 8) -protfp = _scale_wrapper("protfp", "(ProtFP PCA, 8 components) [6]", 8) -fasgai = _scale_wrapper("fasgai", "(factor analysis scales of generalized amino acid information) [7]", 6) -blosum_indices = _scale_wrapper("blosum_indices", "(BLOSUM62-derived indices) [8]", 10) -ms_whim = _scale_wrapper("ms_whim", "(MS-WHIM scores) [9]", 3) + return aa_scale_descriptors(sequence, "fasgai", domains) + +def blosum_indices(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate BLOSUM Indices (BLOSUM) of protein sequence. The BLOSUM indices are 10 + factors derived from the BLOSUM62 substitution matrix by principal component + analysis and VARIMAX rotation, so they describe the evolutionary similarity of + amino acids [8]. + + Each amino acid in the sequence is replaced by its 10 BLOSUM indices and the + mean of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :blosum_indices_df: pd.DataFrame + pandas dataframe of BLOSUM values for protein sequence. Dataframe will be + of the shape 1 x 10 * domains, where 10 * domains is the number of + features calculated from the descriptor (10 scales for each domain) and + 1 is the input sequence. Column names are of the form BLOSUM_j, or + BLOSUM_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "blosum_indices", domains) + +def ms_whim(sequence: str, domains: int = 1) -> pd.DataFrame: + """ + Calculate Molecular Surface Weighted Holistic Invariant Molecular Scores (MS- + WHIM) of protein sequence. The MS-WHIM scores are 3 principal components of 3D + descriptors calculated from the molecular electrostatic potential on the surface + of each amino acid [9]. + + Each amino acid in the sequence is replaced by its 3 MS-WHIM scores and the mean + of each is taken over the sequence, calculated as: + + S_j = (1/L) * sum_i z_j(s_i) + + where S_j is the descriptor value for scale j, z_j(s_i) is the value of scale + j for the amino acid at position i and L is the length of the sequence. With + domains > 1 the sequence is split into that many near-equal segments and the + means of each segment are concatenated. + + Parameters + ========== + :sequence: str + protein sequence. + :domains: int (default=1) + number of near-equal segments of the sequence to calculate the means over. + + Returns + ======= + :ms_whim_df: pd.DataFrame + pandas dataframe of MSWHIM values for protein sequence. Dataframe will be + of the shape 1 x 3 * domains, where 3 * domains is the number of + features calculated from the descriptor (3 scales for each domain) and + 1 is the input sequence. Column names are of the form MSWHIM_j, or + MSWHIM_j_dk when domains > 1. + """ + return aa_scale_descriptors(sequence, "ms_whim", domains) ############################## Auto/Cross Covariance ###################################### def auto_cross_covariance(sequence: str, scale: str | list[str] = "z_scales_5", lag: int = 5, mode: str = "acc") -> pd.DataFrame: """ - Calculate the auto- and cross-covariance (ACC) transform of an amino-acid - scale [10, 11]. For lag l and scale dimensions j, k: + Calculate Auto Cross Covariance (ACC) of protein sequence. ACC transforms the + amino acid scale values of a sequence into a fixed-length vector that + describes the covariance between the scale dimensions of residues separated + by l positions, for each lag l in the range 1..lag [10, 11]. It is calculated as: - ACC_{j,k}(l) = sum_{i=1}^{L-l} z_{j,i} x z_{k,i+l} / (L - l) + ACC_jk(l) = sum_{i=1}^{L-l} z_j(s_i) * z_k(s_i+l) / (L - l) - ``mode="acc"`` returns every (j, k) pair, ``"ac"`` only j = k (auto - covariance) and ``"cc"`` only j != k (cross covariance). ``scale`` may be a - protpy factor-scale family (used as published) or an AAindex1 code / list - of codes, which are standardised over the 20 amino acids. + where ACC_jk(l) is the covariance between dimensions j and k at lag l, z_j(s_i) + is the value of dimension j for the amino acid at position i and L is the length + of the sequence. When j = k this is the auto covariance and when j != k the cross + covariance. The scale can be one of the factor-scale families, which are used as + published, or one or more AAindex accession numbers, whose values are first + standardised over the 20 amino acids. Parameters ========== :sequence: str protein sequence. :scale: str | list[str] (default="z_scales_5") - factor-scale family, single-value scale name or AAindex1 code(s). + factor-scale family, single-value scale name, AAindex accession number or + list of AAindex accession numbers. :lag: int (default=5) - maximum lag, less than the sequence length. + maximum lag, must be less than the length of the sequence. :mode: str (default="acc") - "acc", "ac" or "cc". + which covariances to return: "acc" (all dimension pairs), "ac" (auto + covariance only, j = k) or "cc" (cross covariance only, j != k). Returns ======= :acc_df: pd.DataFrame - pandas DataFrame of shape 1 x d²·lag ("acc"), d·lag ("ac") or - d(d-1)·lag ("cc"), columns ``ACC_{j}_{k}_lag{l}``. + pandas dataframe of ACC values for protein sequence. Dataframe will be of + the shape 1 x N, where N is the number of features calculated from the + descriptor and 1 is the input sequence; N is d^2 * lag for "acc", d * lag + for "ac" and d(d-1) * lag for "cc", where d is the number of dimensions of + the scale. By default (5 z-scales, lag=5) the shape will be 1 x 125. Column + names are of the form ACC_j_k_lagl, e.g. ACC_1_2_lag3. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -222,25 +525,36 @@ def auto_cross_covariance(sequence: str, scale: str | list[str] = "z_scales_5", def physicochemical_distance_transformation(sequence: str, lag: int = 3, properties: list[str] | None = None) -> pd.DataFrame: """ - Calculate the Physicochemical Distance Transformation (PDT) [12]. For each - AAindex property P_j (standardised over the 20 amino acids) and lag l: + Calculate Physicochemical Distance Transformation (PDT) of protein sequence. + PDT describes how much a physicochemical property changes between residues + separated by l positions, for each lag l in the range 1..lag [12]. For each + AAindex property it is calculated as: + + PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_i+l))^2 / (L - l) - PDT_j(l) = sum_{i=1}^{L-l} (P_j(s_i) - P_j(s_{i+l}))^2 / (L - l) + where PDT_j(l) is the descriptor value for property j at lag l, P_j(s_i) is the + value of property j for the amino acid at position i, standardised over the 20 + amino acids, and L is the length of the sequence. By default the same 8 AAindex + properties as the autocorrelation descriptors are used. Parameters ========== :sequence: str protein sequence. :lag: int (default=3) - maximum lag, less than the sequence length. + maximum lag, must be less than the length of the sequence. :properties: list[str] | None (default=None) - AAindex1 accession codes; defaults to the 8 properties used by the - autocorrelation descriptors. + list of AAindex accession numbers for the properties to use. Defaults to + the 8 properties used by the autocorrelation descriptors. Returns ======= :pdt_df: pd.DataFrame - pandas DataFrame of shape 1 x len(properties)·lag, columns ``PDT_{code}_{l}``. + pandas dataframe of PDT values for protein sequence. Dataframe will be of + the shape 1 x N, where N is the number of features calculated from the + descriptor (number of properties * lag) and 1 is the input sequence. By + default the shape will be 1 x 24. Column names are of the form + PDT__l, e.g. PDT_CIDH920105_1. """ #validate input protein sequence sequence = _validate_sequence(sequence) diff --git a/protpy/signal.py b/protpy/signal.py index ca2c0bc..3a320af 100644 --- a/protpy/signal.py +++ b/protpy/signal.py @@ -29,7 +29,25 @@ def _signal(sequence: str, scale: str | dict) -> tuple[np.ndarray, str]: - """Mean-centred single-value property signal and the name used in column labels.""" + """ + Convert a protein sequence into a numerical signal using a single-value amino + acid scale, with the mean of the signal subtracted. + + Parameters + ========== + :sequence: str + protein sequence. + :scale: str | dict + single-value scale name, AAindex accession number or dict of amino acid + values. + + Returns + ======= + :signal: np.ndarray + mean-centred property values along the sequence. + :name: str + name of the scale used in column names ("custom" for a dict). + """ values = _property_matrix(sequence, scale) if values.shape[1] != 1: raise ValueError(f"Scale '{scale}' is multi-dimensional; use a single-value scale.") @@ -40,30 +58,37 @@ def _signal(sequence: str, scale: str | dict) -> tuple[np.ndarray, str]: def fft_descriptors(sequence: str, scale: str | dict = "eiip", n_bins: int = 10) -> pd.DataFrame: """ - Calculate a length-invariant binned power spectrum of a property signal, as - in the resonant recognition model of Cosic [1]. Each residue is replaced by - its scale value (default: electron-ion interaction potential, EIIP), the - signal is mean-centred, and its power spectrum is + Calculate Fast Fourier Transform (FFT) descriptors of protein sequence. Each + amino acid in the sequence is replaced by its value on a property scale, by + default the electron-ion interaction potential (EIIP) used in the resonant + recognition model [1], and the power spectrum of the mean-centred signal is + calculated as: - P(f) = abs(FFT(x)(f))^2 / L, f = 0 .. 0.5 cycles/residue + P(f) = abs(FFT(x)(f))^2 / L - The normalised frequency range 0-0.5 is divided into ``n_bins`` equal-width - bins (the last bin includes 0.5) and the mean power in each bin is returned; - bins containing no frequency are 0. + where x is the property signal, f is the frequency in cycles per residue + (0 to 0.5) and L is the length of the sequence. To give the same number of + features for any sequence length, the frequency range 0-0.5 is split into + n_bins equal-width bins and the mean power in each bin is returned; bins + containing no frequencies are 0. Parameters ========== :sequence: str protein sequence. :scale: str | dict (default="eiip") - single-value scale name, AAindex1 accession code or {aa: value} dict. + single-value scale name, AAindex accession number or dict of amino acid + values. :n_bins: int (default=10) number of frequency bins. Returns ======= :fft_df: pd.DataFrame - pandas DataFrame of shape 1 x n_bins, columns ``FFT_{scale}_bin{1..n}``. + pandas dataframe of FFT descriptors for protein sequence. Dataframe will be + of the shape 1 x n_bins, where n_bins is the number of features calculated + from the descriptor and 1 is the input sequence. Column names are of the + form FFT__bin, e.g. FFT_eiip_bin1. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -83,27 +108,36 @@ def fft_descriptors(sequence: str, scale: str | dict = "eiip", n_bins: int = 10) def dct_descriptors(sequence: str, scale: str | dict = "kyte_doolittle", n_coeffs: int = 10) -> pd.DataFrame: """ - Calculate the first ``n_coeffs`` coefficients of the orthonormal type-II - discrete cosine transform (DCT-II) of a mean-centred property signal [2]: + Calculate Discrete Cosine Transform (DCT) descriptors of protein sequence. + Each amino acid in the sequence is replaced by its value on a property scale, + by default the Kyte-Doolittle hydropathy, and the first n_coeffs coefficients + of the orthonormal type-II DCT of the mean-centred signal are returned. The + DCT compresses a sequence-length signal into a fixed number of coefficients + [2], calculated as: - X_k = s_k sum_{n=0}^{L-1} x_n cos(pi k (2n + 1) / (2L)), - s_0 = sqrt(1/L), s_k = sqrt(2/L) for k > 0 + X_k = s_k * sum_{n=0}^{L-1} x_n * cos(pi * k * (2n + 1) / (2L)) - X_0 is 0 because the signal is mean-centred. + where X_k is coefficient k, x_n is the property value at position n, L is the + length of the sequence, and s_0 = sqrt(1/L) and s_k = sqrt(2/L) for k > 0. X_0 + is 0 because the signal is mean-centred. Parameters ========== :sequence: str protein sequence. :scale: str | dict (default="kyte_doolittle") - single-value scale name, AAindex1 accession code or {aa: value} dict. + single-value scale name, AAindex accession number or dict of amino acid + values. :n_coeffs: int (default=10) - number of coefficients, at most the sequence length. + number of coefficients, at most the length of the sequence. Returns ======= :dct_df: pd.DataFrame - pandas DataFrame of shape 1 x n_coeffs, columns ``DCT_{scale}_{0..n-1}``. + pandas dataframe of DCT descriptors for protein sequence. Dataframe will be + of the shape 1 x n_coeffs, where n_coeffs is the number of features + calculated from the descriptor and 1 is the input sequence. Column names + are of the form DCT__, e.g. DCT_kyte_doolittle_0. """ #validate input protein sequence sequence = _validate_sequence(sequence) diff --git a/protpy/structural.py b/protpy/structural.py index 9b085be..0b96fff 100644 --- a/protpy/structural.py +++ b/protpy/structural.py @@ -85,42 +85,43 @@ def aggrescan(sequence: str, window: int = 5) -> pd.DataFrame: """ - Calculate AGGRESCAN aggregation "hot spot" descriptors [1]. Each residue is - given its experimental aggregation propensity a3v; the a4v profile (AP) is the - a3v mean over a sliding window, assigned to the window's central residue. - Off-centre N- and C-terminal residues take the first and last window values. - A hot spot (HS) is a run of >= 5 residues with a4v above the hot-spot - threshold HST = -0.02 and containing no proline. Areas use trapezoidal - integration of the AP with HST as the zero axis. - - Outputs:: - a3vSA mean a3v over the sequence - nHS number of hot spots; NnHS = nHS / L x 100 - AAT area of the AP above HST over the whole sequence; AATr = AAT / L - THSA total hot-spot area (sum of HSA); THSAr = THSA / L - TA total area of the AP (HST as zero axis, signed) - a4vSS sum of a4v; Na4vSS = a4vSS / L x 100 - a4vAHS mean a4v inside hot spots (mean over hot spots; 0 if none) - HSA_max largest single hot-spot area (0 if none) - - AGGRESCAN chooses the window (5, 7, 9 or 11) from the sequence length and - gives the two extreme residues specific charge-corrected values; neither - rule is given in the published supplementary material, so ``window`` is a - parameter (default 5) and the extreme residues are treated like the other - off-centre residues. + Calculate AGGRESCAN (AGG) aggregation descriptors of protein sequence. + AGGRESCAN predicts aggregation "hot spots", short stretches that promote + protein aggregation, from experimental aggregation propensities (a3v) of the + 20 amino acids [1]. The a3v values are averaged over a sliding window, giving + the aggregation profile (a4v), with each window value assigned to its central + residue and the off-centre N- and C-terminal residues given the first and last + window values. A hot spot is a run of 5 or more residues with an a4v above the + hot-spot threshold (HST) of -0.02 and containing no proline. Areas are + calculated by trapezoidal integration of the profile, with HST as the zero axis. + + The 12 output values are: the mean a3v of the sequence (a3vSA); the number of + hot spots (nHS) and the number per 100 residues (NnHS); the area of the profile + above HST (AAT) and per residue (AATr); the total area of the hot spots (THSA) + and per residue (THSAr); the total area of the profile (TA); the sum of a4v + (a4vSS) and per 100 residues (Na4vSS); the mean a4v in the hot spots (a4vAHS, + 0 if there are none); and the largest single hot-spot area (HSA_max). + + AGGRESCAN chooses a window of 5, 7, 9 or 11 residues from the sequence length + and gives the two extreme residues specific values; neither rule is given in + the published supplementary material, so the window is a parameter and the + extreme residues are treated like the other off-centre residues. Parameters ========== :sequence: str protein sequence. :window: int (default=5) - odd sliding-window length (AGGRESCAN uses 5, 7, 9 or 11); reduced to the + odd sliding window length (AGGRESCAN uses 5, 7, 9 or 11); reduced to the largest odd length that fits a short sequence. Returns ======= :aggrescan_df: pd.DataFrame - pandas DataFrame of shape 1 x 12, columns prefixed ``AGG_``. + pandas dataframe of AGGRESCAN descriptors for protein sequence. Dataframe + will be of the shape 1 x 12, where 12 is the number of features calculated + from the descriptor and 1 is the input sequence. Column names start + with AGG, e.g. AGG_nHS and AGG_THSA. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -153,20 +154,40 @@ def aggrescan(sequence: str, window: int = 5) -> pd.DataFrame: ######################### Uversky Charge-Hydropathy / FoldIndex ########################### def _mean_hydropathy(sequence: str) -> float: - """Mean Kyte-Doolittle hydropathy rescaled to 0-1, (KD + 4.5) / 9 [2].""" + """ + Calculate the mean hydropathy of a protein sequence using the Kyte-Doolittle + scale rescaled to the range 0-1, (KD + 4.5) / 9, as used by Uversky et al. [2]. + + Parameters + ========== + :sequence: str + protein sequence. + + Returns + ======= + :mean_h: float + mean rescaled hydropathy of the sequence. + """ return sum((kyte_doolittle_[aa] + 4.5) / 9 for aa in sequence) / len(sequence) def uversky_charge_hydropathy(sequence: str) -> pd.DataFrame: """ - Calculate the charge-hydropathy descriptors of Uversky et al. [2]: + Calculate the Uversky Charge-Hydropathy (UV) descriptors of protein sequence. + Uversky et al. showed that natively unfolded proteins can be separated from + folded proteins by their mean net charge and mean hydropathy [2]. These are + calculated as: + + = mean of (KD + 4.5) / 9 + + = abs(n+ - n-) / L - = mean of (KD + 4.5) / 9 (Kyte-Doolittle rescaled to 0-1) - = abs(n+ - n-) / L (K/R positive, D/E negative) - HQ = 2.785 - - 1.151 + HQ = 2.785 - - 1.151 - The boundary = 2.785 - 1.151 separates natively unfolded from folded - proteins (as restated in [15]); ``UV_Unfolded`` is 1 when HQ < 0. + where KD is the Kyte-Doolittle hydropathy of each residue, n+ and n- are the + numbers of positive (K, R) and negative (D, E) residues and L is the length + of the sequence. HQ is the distance from the boundary = 2.785 - 1.151 + (as restated in [15]); the protein is predicted to be unfolded when HQ < 0. Parameters ========== @@ -176,8 +197,9 @@ def uversky_charge_hydropathy(sequence: str) -> pd.DataFrame: Returns ======= :uversky_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``UV_MeanH``, ``UV_MeanR``, - ``UV_HQ`` and ``UV_Unfolded``. + pandas dataframe of charge-hydropathy descriptors for protein sequence. + Dataframe will be of the shape 1 x 4, with the columns UV_MeanH, UV_MeanR, + UV_HQ and UV_Unfolded (1 if predicted unfolded, else 0). """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -189,28 +211,31 @@ def uversky_charge_hydropathy(sequence: str) -> pd.DataFrame: def fold_index(sequence: str, window: int = 51) -> pd.DataFrame: """ - Calculate FoldIndex profile descriptors [3]. For each window of length - ``window``: + Calculate the FoldIndex (FI) of protein sequence. FoldIndex applies the + Uversky charge-hydropathy boundary over a sliding window to find disordered + regions of a protein sequence [3]. For each window it is calculated as: FI = 2.785 - abs() - 1.151 - with and as in ``uversky_charge_hydropathy`` but computed over the - window. Residues covered by at least one window with FI < 0 are counted as - disordered. A window longer than the sequence uses the whole sequence, so - FI then equals ``UV_HQ``. + where is the mean Kyte-Doolittle hydropathy of the window rescaled to 0-1 + and is the mean net charge of the window (K, R positive; D, E negative). + Residues covered by at least one window with FI < 0 are counted as disordered. + A window longer than the sequence uses the whole sequence, so FI then equals + UV_HQ from uversky_charge_hydropathy. Parameters ========== :sequence: str protein sequence. :window: int (default=51) - window length. + length of the sliding window. Returns ======= :fold_index_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``FI_mean``, ``FI_min``, - ``FI_frac_disordered`` and ``FI_longest_disordered`` (residues). + pandas dataframe of FoldIndex descriptors for protein sequence. Dataframe + will be of the shape 1 x 4, with the columns FI_mean, FI_min, + FI_frac_disordered and FI_longest_disordered (in residues). """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -231,10 +256,18 @@ def fold_index(sequence: str, window: int = 51) -> pd.DataFrame: def disorder_propensity(sequence: str) -> pd.DataFrame: """ - Calculate disorder-propensity descriptors: the fractions of - disorder-promoting (A, R, G, Q, S, P, E, K) and order-promoting - (W, C, F, I, Y, V, L, N) residues [4], and the mean TOP-IDP value [5] - (higher = more disorder-prone). + Calculate the Disorder Propensity (DP) of protein sequence. Intrinsically + disordered proteins are enriched in disorder-promoting residues (A, R, G, Q, + S, P, E, K) and depleted in order-promoting residues (W, C, F, I, Y, V, L, N) + [4]. The descriptor gives the fraction of each, together with the mean value of + the TOP-IDP scale, a scale of amino acid propensity for intrinsic disorder [5], + calculated as: + + DP_TOP_IDP_mean = sum_i TOP-IDP(s_i) / L + + where TOP-IDP(s_i) is the TOP-IDP value of the amino acid at position i and L + is the length of the sequence. Higher values indicate a higher propensity for + disorder. Parameters ========== @@ -244,8 +277,9 @@ def disorder_propensity(sequence: str) -> pd.DataFrame: Returns ======= :disorder_df: pd.DataFrame - pandas DataFrame of shape 1 x 3, columns ``DP_frac_disorder_promoting``, - ``DP_frac_order_promoting`` and ``DP_TOP_IDP_mean``. + pandas dataframe of disorder propensity descriptors for protein sequence. + Dataframe will be of the shape 1 x 3, with the columns + DP_frac_disorder_promoting, DP_frac_order_promoting and DP_TOP_IDP_mean. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -261,35 +295,33 @@ def disorder_propensity(sequence: str) -> pd.DataFrame: def transmembrane_features(sequence: str, window: int = 19, kd_threshold: float = 1.6) -> pd.DataFrame: """ - Calculate heuristic transmembrane-segment features from the Kyte-Doolittle - hydropathy profile [6, 7]. Windows of length ``window`` with mean hydropathy - above ``kd_threshold`` (1.6 for 19-residue windows in [6]) mark putative - transmembrane segments; overlapping windows are merged into segments. - - Columns:: - - TM_n_segments number of merged segments - TM_max_KD maximum window hydropathy - TM_frac_in_TM fraction of residues inside a segment - TM_positive_inside_bias (K+R in the 15 residues before the first segment) - - (K+R in the 15 residues after it); 0 if no segment - - This is a hydropathy heuristic, not a topology predictor. + Calculate Transmembrane (TM) segment features of protein sequence. Putative + transmembrane segments are found from the Kyte-Doolittle hydropathy profile: + windows of 19 residues with a mean hydropathy above 1.6 indicate a likely + transmembrane segment [6], and overlapping windows are merged into segments. + The positive-inside rule states that the cytoplasmic side of a transmembrane + segment is enriched in positive residues [7]. The 4 output values are: the + number of segments; the maximum window hydropathy; the fraction of residues + inside a segment; and the positive-inside bias, the number of K and R residues + in the 15 residues before the first segment minus the number in the 15 residues + after it (0 if there is no segment). This is a hydropathy heuristic and not a + topology predictor. Parameters ========== :sequence: str protein sequence. :window: int (default=19) - hydropathy window length. + length of the hydropathy window. :kd_threshold: float (default=1.6) - mean hydropathy threshold. + mean hydropathy above which a window is part of a segment. Returns ======= :tm_df: pd.DataFrame - pandas DataFrame of shape 1 x 4, columns ``TM_n_segments``, - ``TM_max_KD``, ``TM_frac_in_TM`` and ``TM_positive_inside_bias``. + pandas dataframe of transmembrane features for protein sequence. Dataframe + will be of the shape 1 x 4, with the columns TM_n_segments, TM_max_KD, + TM_frac_in_TM and TM_positive_inside_bias. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -311,28 +343,26 @@ def transmembrane_features(sequence: str, window: int = 19, kd_threshold: float def signal_peptide_features(sequence: str, n_term: int = 30) -> pd.DataFrame: """ - Calculate simple signal-peptide features of the N-terminus [8]: - - Columns:: - - SP_nregion_charge net charge of the first 5 residues (K/R +1, D/E -1) - SP_hregion_maxKD maximum mean Kyte-Doolittle hydropathy over 8-residue - windows within the first ``n_term`` residues - - The von Heijne cleavage-site weight matrix is not included. + Calculate Signal Peptide (SP) features of protein sequence. Signal peptides + have a positively charged N-terminal n-region followed by a hydrophobic + h-region [8]. The 2 output values are: the net charge of the first 5 residues + (K and R +1, D and E -1); and the maximum mean Kyte-Doolittle hydropathy over + 8-residue windows within the first n_term residues. The von Heijne + cleavage-site weight matrix is not included. Parameters ========== :sequence: str protein sequence. :n_term: int (default=30) - length of the N-terminal region examined. + length of the N-terminal region to examine. Returns ======= :sp_df: pd.DataFrame - pandas DataFrame of shape 1 x 2, columns ``SP_nregion_charge`` and - ``SP_hregion_maxKD``. + pandas dataframe of signal peptide features for protein sequence. + Dataframe will be of the shape 1 x 2, with the columns SP_nregion_charge + and SP_hregion_maxKD. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -355,11 +385,14 @@ def signal_peptide_features(sequence: str, n_term: int = 30) -> pd.DataFrame: def functional_site_counts(sequence: str) -> pd.DataFrame: """ - Count functional sequence motifs and their density per 100 residues: - N-glycosylation sites (PROSITE PS00001), the C-terminal ER-retention signal - (PS00014) and peroxisomal targeting signal 1 (PS00342) [9], and tryptic - cleavage sites (after K/R, not before P) [10]. Overlapping matches are - counted. These complement the default motifs of ``motif_composition``. + Calculate Functional Site (FS) counts of protein sequence. The number of each + functional sequence motif, and its density per 100 residues, is counted for: + N-glycosylation sites (PROSITE PS00001, N-{P}-[ST]-{P}), the C-terminal + endoplasmic reticulum retention signal (PS00014, [KRHQSA]-[DENQ]-E-L at the + C-terminus), the C-terminal peroxisomal targeting signal 1 (PS00342, + [STAGCN]-[RKH]-[LIVMAFY] at the C-terminus) [9] and tryptic cleavage sites + (after K or R, not before P) [10]. Overlapping matches are counted. These motifs + complement the default motifs of motif_composition. Parameters ========== @@ -369,8 +402,10 @@ def functional_site_counts(sequence: str) -> pd.DataFrame: Returns ======= :sites_df: pd.DataFrame - pandas DataFrame of shape 1 x 8, columns ``FS_{motif}`` (counts) and - ``FS_{motif}_per100``. + pandas dataframe of functional site counts for protein sequence. Dataframe + will be of the shape 1 x 8, where 8 is the number of features calculated + from the descriptor (4 counts and 4 densities) and 1 is the input sequence. + Column names are FS_ and FS__per100, e.g. FS_N_glycosylation. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -382,12 +417,44 @@ def functional_site_counts(sequence: str) -> pd.DataFrame: ###################################### PLAAC ############################################## def _plaac_model() -> tuple[dict, dict, np.ndarray, np.ndarray]: - """Emission log-probabilities (bg, fg), log transition matrix and log initial probabilities.""" + """ + Get the parameters of the PLAAC prion-like domain model [11]: the amino acid + frequencies of S. cerevisiae prion domains (foreground) and of the + S. cerevisiae proteome (background), and the transition and initial + probabilities of the two-state hidden Markov model. Ported from the PLAAC + source code (MIT License). + + Returns + ======= + :bg: dict + background amino acid frequencies. + :fg: dict + prion-domain amino acid frequencies. + :log_t: np.ndarray + log transition probabilities of the hidden Markov model (2 x 2). + :log_i: np.ndarray + log initial probabilities of the hidden Markov model. + """ p = _load_json_table("structural_params.json", "plaac")["values"] eps = p["epsilon"] def _normalise(freqs: dict) -> dict: #as PLAAC: normalise, add epsilon for X and *, normalise again + """ + Normalise amino acid frequencies as PLAAC does: normalise to sum to 1, then + add a small probability for the unknown residue (X) and stop (*) symbols and + normalise again. + + Parameters + ========== + :freqs: dict + amino acid frequencies. + + Returns + ======= + :freqs: dict + normalised amino acid frequencies. + """ total = sum(freqs.values()) freqs = {aa: v / total for aa, v in freqs.items()} total = 1 + 2 * eps @@ -398,7 +465,29 @@ def _normalise(freqs: dict) -> dict: def _viterbi(sequence: str, bg: dict, fg: dict, log_t: np.ndarray, log_i: np.ndarray) -> np.ndarray: - """Most likely background (0) / prion-like (1) state path of the two-state HMM.""" + """ + Calculate the most likely sequence of states of the PLAAC two-state hidden + Markov model (background or prion-like) for a protein sequence using the + Viterbi algorithm [11]. + + Parameters + ========== + :sequence: str + protein sequence. + :bg: dict + background amino acid frequencies. + :fg: dict + prion-domain amino acid frequencies. + :log_t: np.ndarray + log transition probabilities. + :log_i: np.ndarray + log initial probabilities. + + Returns + ======= + :path: np.ndarray + state of each residue, 0 for background and 1 for prion-like. + """ emit = np.log(np.array([[bg[aa], fg[aa]] for aa in sequence])) score = log_i + emit[0] back = np.zeros((len(sequence), 2), dtype=int) @@ -414,38 +503,58 @@ def _viterbi(sequence: str, bg: dict, fg: dict, log_t: np.ndarray, log_i: np.nda def _max_window_sum(values: np.ndarray, width: int) -> float: + """ + Calculate the maximum sum of values over any window of a given width. + + Parameters + ========== + :values: np.ndarray + values along the sequence. + :width: int + window width. + + Returns + ======= + :max_sum: float + maximum window sum. + """ csum = np.concatenate([[0.0], np.cumsum(values)]) return float((csum[width:] - csum[:-width]).max()) def plaac(sequence: str, core_length: int = 60) -> pd.DataFrame: """ - Calculate prion-like domain descriptors with the PLAAC model [11], ported from - the PLAAC source (MIT License; S. cerevisiae parameters, alpha = 1). Each - residue has the log-likelihood ratio LLR = ln(f_prion / f_background). - - Columns:: - - PLAAC_LLR maximum LLR sum over any window of ``core_length`` residues - PLAAC_COREscore maximum LLR sum over a ``core_length`` window lying inside a - prion-like run of the two-state HMM Viterbi parse - PLAAC_PrD_length length of the prion-like run containing the best core - PLAAC_has_PrD 1 if a prion-like run of >= ``core_length`` residues exists - - As in PLAAC, LLR and COREscore are NaN when undefined (sequence shorter than - ``core_length`` / no prion-like run of that length); PrD_length is then 0. + Calculate the Prion-Like Amino Acid Composition (PLAAC) descriptors of protein + sequence. PLAAC identifies prion-like domains, whose composition resembles + that of yeast prion domains, using the amino acid frequencies of prion domains + and of the S. cerevisiae proteome [11]. Each residue has a log-likelihood ratio: + + LLR(s_i) = ln(f_prion(s_i) / f_background(s_i)) + + The 4 output values are: the maximum sum of LLR over any window of core_length + residues (PLAAC_LLR); the maximum sum of LLR over a window of core_length + residues lying entirely inside a prion-like region of the two-state hidden + Markov model Viterbi parse (PLAAC_COREscore); the length of the prion-like + region containing that core (PLAAC_PrD_length); and whether the sequence has a + prion-like region of at least core_length residues (PLAAC_has_PrD). As in + PLAAC, PLAAC_LLR and PLAAC_COREscore are NaN when they are undefined (a + sequence shorter than core_length, or no prion-like region of that length). + The parameters are ported from the PLAAC source code (MIT License), using the + S. cerevisiae background (alpha = 1). Parameters ========== :sequence: str protein sequence. :core_length: int (default=60) - minimal prion-like domain (core) length. + minimum length of a prion-like domain core. Returns ======= :plaac_df: pd.DataFrame - pandas DataFrame of shape 1 x 4. + pandas dataframe of PLAAC descriptors for protein sequence. Dataframe will + be of the shape 1 x 4, with the columns PLAAC_LLR, PLAAC_COREscore, + PLAAC_PrD_length and PLAAC_has_PrD. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -467,23 +576,28 @@ def plaac(sequence: str, core_length: int = 60) -> pd.DataFrame: def antigenicity_surface(sequence: str, window: int = 7) -> pd.DataFrame: """ - Summarise hydrophilicity/surface profiles associated with antigenic sites: - the Parker HPLC hydrophilicity scale averaged over 7-residue windows [12] and - the Janin free energy of transfer from inside to outside of a globular - protein [13] over the same window. Each profile is reduced to its mean, max, - min and standard deviation. + Calculate the Antigenicity and Surface (Antigenicity) profiles of protein + sequence. Antigenic sites tend to be hydrophilic and exposed on the protein + surface. Each amino acid in the sequence is replaced by its value on the + Parker hydrophilicity scale, derived from HPLC peptide retention times [12], + and on the Janin scale of free energy of transfer from the inside to the + outside of a globular protein [13]. Each is averaged over a sliding window of 7 + residues, as used by Parker et al., and the profile is summarised by its mean, + maximum, minimum and standard deviation. Parameters ========== :sequence: str protein sequence. :window: int (default=7) - window length (7 as in Parker et al.). + length of the sliding window. Returns ======= :antigenicity_df: pd.DataFrame - pandas DataFrame of shape 1 x 8, columns ``PARKER_{stat}`` and ``JANIN_{stat}``. + pandas dataframe of antigenicity and surface profile statistics for protein + sequence. Dataframe will be of the shape 1 x 8, with the columns + PARKER_mean, PARKER_max, PARKER_min, PARKER_std and the same for JANIN. """ #validate input protein sequence sequence = _validate_sequence(sequence) @@ -498,14 +612,12 @@ def antigenicity_surface(sequence: str, window: int = 7) -> pd.DataFrame: def cysteine_features(sequence: str) -> pd.DataFrame: """ - Calculate cysteine features [14]: - - Cys_count number of C - Cys_frac C / L - Cys_even 1 if the count is even - Cys_max_disulfides floor(count / 2) - Cys_CxxC overlapping C-x-x-C motifs - Cys_mean_spacing mean gap between consecutive cysteines (0 if fewer than 2) + Calculate Cysteine (Cys) features of protein sequence. Cysteines form + disulfide bonds and functional motifs such as the CxxC motif of thiol + oxidoreductases [14]. The 6 output values are: the number of cysteines; their + fraction; whether the number is even; the maximum number of disulfide bonds + (the number of cysteines // 2); the number of overlapping C-x-x-C motifs; and + the mean gap between consecutive cysteines (0 if there are fewer than 2). Parameters ========== @@ -515,7 +627,9 @@ def cysteine_features(sequence: str) -> pd.DataFrame: Returns ======= :cys_df: pd.DataFrame - pandas DataFrame of shape 1 x 6. + pandas dataframe of cysteine features for protein sequence. Dataframe will + be of the shape 1 x 6, with the columns Cys_count, Cys_frac, Cys_even, + Cys_max_disulfides, Cys_CxxC and Cys_mean_spacing. """ #validate input protein sequence sequence = _validate_sequence(sequence)