Source code for rfproteina.metrics.rasa_metrics

from collections import defaultdict
from typing import Any

import numpy as np
from atomworks.ml.transforms.sasa import calculate_atomwise_rasa
from biotite.structure import AtomArray, AtomArrayStack

from rfproteina.conditions import C_RAS, RasaEnum
from rfproteina.metrics.metric import Metric


[docs] def get_rasa_metrics( atom_array: AtomArray, probe_radius: float = 1.4, atom_radii: str = "element", point_number: int = 100, ) -> dict[str, float]: """Compute RASA adherence metrics for a given input atom array. Args: atom_array (AtomArray): Input AtomArray with predicted structure and rasa annotations. probe_radius (float): Van-der-Waals radius of the probe in Angstrom. Defaults to 1.4 (for water). atom_radii (str): Atom radii set to use for calculation. Defaults to "element", matching the radii used for RASA conditioning during training. point_number (int): Number of points in the Shrake-Rupley algorithm. Defaults to 100. Returns: dict: Dictionary containing RASA adherence metrics Note: RASA is computed with the Shrake-Rupley algorithm. Training uses buried values in ``[0.0, 0.02)``, intermediate values in ``[0.02, 0.1]``, and exposed values greater than ``0.1``, after rounding to the SASA sampling grid. """ output_metrics = {} # Calculate RASA for generated structure generated_rasa = calculate_atomwise_rasa(atom_array, probe_radius, atom_radii, point_number) # Define bin edges matching training (from design_task_modifiers.py) generated_rasa = np.rint(generated_rasa.astype(float) * point_number) / point_number bin_edges = [0.0, 0.02, np.nextafter(0.1, np.inf)] # Bin the generated RASA values generated_rasa_bins = np.digitize(generated_rasa, bin_edges, right=False) if (generated_rasa_bins == 0).any(): raise ValueError("Negative RASA value was detected. This indicates an error.") # Convert to RasaEnum generated_rasa_values = np.full(atom_array.array_length(), RasaEnum.UNSPECIFIED, dtype=int) generated_rasa_values[generated_rasa_bins == 1] = RasaEnum.BURIED generated_rasa_values[generated_rasa_bins == 2] = RasaEnum.INTERMEDIATE generated_rasa_values[generated_rasa_bins == 3] = RasaEnum.EXPOSED generated_rasa_values[np.isnan(generated_rasa)] = RasaEnum.UNSPECIFIED # Extract RASA conditioning rasa_conditioning = C_RAS.annotation(atom_array, default="generate") # Compute adherence for each RASA type rasa_adherence_dict = {} for rasa_annot, rasa_type in zip( [RasaEnum.BURIED, RasaEnum.INTERMEDIATE, RasaEnum.EXPOSED], ["buried", "intermediate", "exposed"], strict=False, ): metric_name = f"{rasa_type}_conditioning_adherence" expected_indices = np.where(rasa_conditioning == rasa_annot)[0] if len(expected_indices) > 0: rasa_adherence = (generated_rasa_values[expected_indices] == rasa_annot).mean() rasa_adherence_dict[metric_name] = rasa_adherence else: # Would be misleading to give a numerical value if no conditioning of this type was provided rasa_adherence_dict[metric_name] = np.nan output_metrics.update(rasa_adherence_dict) return output_metrics
[docs] class RasaAdherenceMetric(Metric): """Compute adherence to RASA conditioning."""
[docs] @property def kwargs_to_compute_args(self) -> dict[str, Any]: return { "atom_array_or_stack": ("predicted_atom_array",), }
[docs] def compute(self, atom_array_or_stack: AtomArray | AtomArrayStack | list[AtomArray]) -> dict[str, float]: out_dict = defaultdict(list) if isinstance(atom_array_or_stack, AtomArray): atom_array_list = [atom_array_or_stack] elif isinstance(atom_array_or_stack, AtomArrayStack): atom_array_list = list(atom_array_or_stack) elif isinstance(atom_array_or_stack, list): atom_array_list = atom_array_or_stack else: raise ValueError( f"RasaAdherenceMetric expects AtomArray, AtomArrayStack, or list of " f"AtomArrays, but got {type(atom_array)}" ) for atom_array in atom_array_list: rasa_metrics = get_rasa_metrics(atom_array) for k, v in rasa_metrics.items(): out_dict[k].append(v) return { key: float(np.nanmean(values)) if np.any(~np.isnan(values)) else float("nan") for key, values in out_dict.items() }