Download src/models/model.py from zz312/RiboSphere: direct link, hf CLI and curl.
- Browser
- Download file 29.7 kB
-
https://huggingface.co/zz312/RiboSphere/resolve/main/src/models/model.py
- Command line
-
hf download hf://zz312/RiboSphere/src/models/model.py
-
curl -L -o model.py https://huggingface.co/zz312/RiboSphere/resolve/main/src/models/model.py
29.7 kB
| """Top-level RiboSphere model and Hugging Face serialization helpers.""" | |
| from __future__ import annotations | |
| import json | |
| from collections.abc import Mapping | |
| from dataclasses import asdict, dataclass, fields | |
| from math import prod | |
| from os import PathLike | |
| from pathlib import Path | |
| from typing import Any, Literal | |
| import torch | |
| from numpy.typing import ArrayLike | |
| from torch import Tensor, nn | |
| from .attention import TransformerStack | |
| from .cfm import ConditionalFlowMatcher | |
| from .dit import DiffusionTransformer | |
| from .fsq import FiniteScalarQuantizer | |
| from .layers import FeedForward | |
| AtomRepresentation = Literal["a1", "a6", "a10", "a11"] | |
| SamplingSchedule = Literal["us", "tan", "1/t"] | |
| WeightValue = Tensor | ArrayLike | |
| ATOM_COUNTS: dict[str, int] = { | |
| "a1": 1, | |
| "a6": 6, | |
| "a10": 10, | |
| "a11": 11, | |
| } | |
| def _get_variant_metadata( | |
| manifest: Mapping[str, Any], | |
| requested_variant: str, | |
| ) -> Mapping[str, Any]: | |
| """Return metadata for an exact canonical variant name.""" | |
| if requested_variant in manifest: | |
| return manifest[requested_variant] | |
| available_variants = ", ".join(sorted(manifest)) | |
| raise ValueError( | |
| f"Unknown variant {requested_variant!r}. Available variants: " | |
| f"{available_variants}." | |
| ) | |
| class RiboSphereConfig: | |
| """Serializable architecture configuration for :class:`RiboSphere`.""" | |
| n_channels_encoder: int = 256 | |
| n_channels_decoder: int = 512 | |
| n_layers_encoder: int = 2 | |
| n_layers_decoder: int = 8 | |
| n_heads: int = 8 | |
| mlp_factor: int = 4 | |
| use_qknorm: bool = False | |
| sigma: float = 0.0 | |
| levels: tuple[int, ...] = (7, 5, 5, 5, 5) | |
| drop_cond_p: float = 0.0 | |
| conditioning_type: str = "cat" | |
| n_channels_pair: int = 64 | |
| encoder_type: str = "xformer" | |
| attention_backend: str = "sdpa" | |
| window_size: int = 8 | |
| share_adaln: bool = False | |
| atoms: AtomRepresentation = "a11" | |
| def __post_init__(self) -> None: | |
| self.levels = tuple(self.levels) | |
| positive_values = { | |
| "n_channels_encoder": self.n_channels_encoder, | |
| "n_channels_decoder": self.n_channels_decoder, | |
| "n_layers_encoder": self.n_layers_encoder, | |
| "n_layers_decoder": self.n_layers_decoder, | |
| "n_heads": self.n_heads, | |
| "mlp_factor": self.mlp_factor, | |
| "window_size": self.window_size, | |
| } | |
| invalid_names = [ | |
| name for name, value in positive_values.items() if value <= 0 | |
| ] | |
| if invalid_names: | |
| raise ValueError( | |
| f"Configuration values must be positive: {', '.join(invalid_names)}" | |
| ) | |
| if self.n_channels_pair < 0: | |
| raise ValueError("n_channels_pair must be non-negative.") | |
| if self.atoms not in ATOM_COUNTS: | |
| raise ValueError( | |
| f"atoms must be one of {', '.join(ATOM_COUNTS)}, " | |
| f"received {self.atoms!r}." | |
| ) | |
| if not self.levels or any(level < 2 for level in self.levels): | |
| raise ValueError("levels must contain integers greater than one.") | |
| if not 0.0 <= self.drop_cond_p <= 1.0: | |
| raise ValueError("drop_cond_p must be in [0, 1].") | |
| if self.sigma < 0: | |
| raise ValueError("sigma must be non-negative.") | |
| if self.attention_backend not in {"sdpa", "flex"}: | |
| raise ValueError("attention_backend must be 'sdpa' or 'flex'.") | |
| if self.conditioning_type != "cat": | |
| raise ValueError("Only 'cat' conditioning is currently supported.") | |
| def from_dict(cls, values: Mapping[str, Any]) -> RiboSphereConfig: | |
| """Build a config while ignoring Hugging Face metadata fields.""" | |
| valid_keys = {field.name for field in fields(cls)} | |
| normalized = dict(values) | |
| if "share_adaLN" in normalized and "share_adaln" not in normalized: | |
| normalized["share_adaln"] = normalized.pop("share_adaLN") | |
| normalized.pop("gpt_prior", None) | |
| normalized.pop("gpt_weight", None) | |
| config_values = { | |
| key: value | |
| for key, value in normalized.items() | |
| if key in valid_keys | |
| } | |
| return cls(**config_values) | |
| class RiboSphere(nn.Module): | |
| """Finite-scalar RNA tokenizer and flow-matching coordinate decoder.""" | |
| def __init__(self, config: RiboSphereConfig) -> None: | |
| super().__init__() | |
| self.config = config | |
| levels = list(config.levels) | |
| self.quantizer = FiniteScalarQuantizer( | |
| levels=levels, | |
| output_dimension=config.n_channels_decoder, | |
| input_dimension=config.n_channels_encoder, | |
| jitter_spread=0.0, | |
| ) | |
| self.codebook_size = prod(levels) | |
| self.flow_matcher = ConditionalFlowMatcher(config.sigma, "uniform") | |
| self.condition_dropout_probability = config.drop_cond_p | |
| self.use_pairwise_bias = config.n_channels_pair > 0 | |
| if self.use_pairwise_bias: | |
| self.pairwise_feature_embedder = PairwiseFeatureEmbedder( | |
| config.n_channels_pair, | |
| 100, | |
| ) | |
| self.num_atoms = ATOM_COUNTS[config.atoms] | |
| self.coordinate_encoder = nn.Sequential( | |
| nn.Linear(self.num_atoms * 3, config.n_channels_encoder), | |
| nn.SiLU(), | |
| nn.Linear(config.n_channels_encoder, config.n_channels_encoder), | |
| nn.LayerNorm(config.n_channels_encoder), | |
| ) | |
| self.encoder = TransformerStack( | |
| num_channels=config.n_channels_encoder, | |
| num_heads=config.n_heads, | |
| mlp_factor=config.mlp_factor, | |
| window_size=config.window_size, | |
| attention_backend=config.attention_backend, | |
| num_layers=config.n_layers_encoder, | |
| pairwise_channels=config.n_channels_pair, | |
| is_causal=False, | |
| ) | |
| self.decoder = DiffusionTransformer( | |
| num_channels=config.n_channels_decoder, | |
| input_channels=self.num_atoms * 3, | |
| num_layers=config.n_layers_decoder, | |
| num_heads=config.n_heads, | |
| mlp_factor=config.mlp_factor, | |
| normalize_queries_and_keys=config.use_qknorm, | |
| conditioning_type=config.conditioning_type, | |
| share_adaln=config.share_adaln, | |
| attention_backend=config.attention_backend, | |
| ) | |
| def from_pretrained( | |
| cls, | |
| model_path: str | PathLike[str], | |
| *, | |
| variant: str | None = None, | |
| subfolder: str | PathLike[str] | None = None, | |
| ) -> RiboSphere: | |
| """Load a variant from a local path or Hugging Face Hub repository.""" | |
| if variant is not None and subfolder is not None: | |
| raise ValueError("Specify either variant or subfolder, not both.") | |
| if variant is not None: | |
| selected_variant = variant | |
| elif subfolder is not None: | |
| selected_variant = str(subfolder) | |
| else: | |
| selected_variant = None | |
| if selected_variant is not None and ( | |
| not selected_variant | |
| or Path(selected_variant).name != selected_variant | |
| ): | |
| raise ValueError("variant must be a single directory-safe name.") | |
| repository_path = Path(model_path) | |
| if not repository_path.exists() and selected_variant is not None: | |
| from huggingface_hub import hf_hub_download | |
| repository_id = str(model_path) | |
| manifest_path = Path( | |
| hf_hub_download( | |
| repo_id=repository_id, | |
| filename="variants.json", | |
| ) | |
| ) | |
| with manifest_path.open(encoding="utf-8") as handle: | |
| manifest = json.load(handle) | |
| variant_metadata = _get_variant_metadata( | |
| manifest, | |
| selected_variant, | |
| ) | |
| config_path = Path( | |
| hf_hub_download( | |
| repo_id=repository_id, | |
| filename=variant_metadata["config"], | |
| ) | |
| ) | |
| weights_path = Path( | |
| hf_hub_download( | |
| repo_id=repository_id, | |
| filename=variant_metadata["weights"], | |
| ) | |
| ) | |
| else: | |
| if not repository_path.exists(): | |
| from huggingface_hub import snapshot_download | |
| repository_path = Path( | |
| snapshot_download(repo_id=str(model_path)) | |
| ) | |
| if not repository_path.is_dir(): | |
| raise ValueError( | |
| "from_pretrained expects a Hugging Face model directory " | |
| "or repository ID." | |
| ) | |
| manifest_path = repository_path / "variants.json" | |
| manifest: dict[str, Any] = {} | |
| if manifest_path.is_file(): | |
| with manifest_path.open(encoding="utf-8") as handle: | |
| manifest = json.load(handle) | |
| if selected_variant is not None and manifest: | |
| variant_metadata = _get_variant_metadata( | |
| manifest, | |
| selected_variant, | |
| ) | |
| config_path = repository_path / variant_metadata.get( | |
| "config", | |
| f"configs/{selected_variant}.json", | |
| ) | |
| weights_path = repository_path / variant_metadata.get( | |
| "weights", | |
| f"weights/{selected_variant}.safetensors", | |
| ) | |
| elif selected_variant is not None: | |
| variant_directory = repository_path / selected_variant | |
| config_path = variant_directory / "config.json" | |
| weights_path = variant_directory / "model.safetensors" | |
| else: | |
| config_path = repository_path / "config.json" | |
| weights_path = repository_path / "model.safetensors" | |
| if ( | |
| selected_variant is None | |
| and manifest | |
| and ( | |
| not config_path.is_file() | |
| or not weights_path.is_file() | |
| ) | |
| ): | |
| available_variants = ", ".join(sorted(manifest)) | |
| raise ValueError( | |
| "This repository contains multiple variants. Pass " | |
| "variant=<name>. Available variants: " | |
| f"{available_variants}." | |
| ) | |
| if not config_path.is_file() or not weights_path.is_file(): | |
| raise FileNotFoundError( | |
| "Checkpoint files are missing: " | |
| f"{config_path} and {weights_path}." | |
| ) | |
| from safetensors.torch import load_file | |
| with config_path.open(encoding="utf-8") as handle: | |
| config = RiboSphereConfig.from_dict(json.load(handle)) | |
| model = cls(config) | |
| model.load_state_dict( | |
| load_file(weights_path, device="cpu"), | |
| strict=True, | |
| ) | |
| return model | |
| def save_pretrained( | |
| self, | |
| output_directory: str | PathLike[str], | |
| *, | |
| variant: str | None = None, | |
| ) -> None: | |
| """Save a standalone checkpoint or a named repository variant.""" | |
| from safetensors.torch import save_file | |
| resolved_directory = Path(output_directory) | |
| resolved_directory.mkdir(parents=True, exist_ok=True) | |
| if variant is not None and ( | |
| not variant or Path(variant).name != variant | |
| ): | |
| raise ValueError("variant must be a single directory-safe name.") | |
| config_values = asdict(self.config) | |
| config_values.update( | |
| { | |
| "architectures": ["RiboSphere"], | |
| "model_type": "ribosphere", | |
| } | |
| ) | |
| if variant is None: | |
| config_path = resolved_directory / "config.json" | |
| weights_path = resolved_directory / "model.safetensors" | |
| else: | |
| config_directory = resolved_directory / "configs" | |
| weights_directory = resolved_directory / "weights" | |
| config_directory.mkdir(exist_ok=True) | |
| weights_directory.mkdir(exist_ok=True) | |
| config_path = config_directory / f"{variant}.json" | |
| weights_path = weights_directory / f"{variant}.safetensors" | |
| with config_path.open( | |
| "w", | |
| encoding="utf-8", | |
| ) as handle: | |
| json.dump(config_values, handle, indent=2) | |
| handle.write("\n") | |
| save_file(self.state_dict(), weights_path) | |
| if variant is not None: | |
| manifest_path = resolved_directory / "variants.json" | |
| manifest: dict[str, Any] = {} | |
| if manifest_path.is_file(): | |
| with manifest_path.open(encoding="utf-8") as handle: | |
| manifest = json.load(handle) | |
| manifest[variant] = { | |
| "atoms": self.config.atoms, | |
| "levels": list(self.config.levels), | |
| "codebook_size": self.codebook_size, | |
| "config": config_path.relative_to( | |
| resolved_directory | |
| ).as_posix(), | |
| "weights": weights_path.relative_to( | |
| resolved_directory | |
| ).as_posix(), | |
| } | |
| with manifest_path.open("w", encoding="utf-8") as handle: | |
| json.dump( | |
| dict(sorted(manifest.items())), | |
| handle, | |
| indent=2, | |
| ) | |
| handle.write("\n") | |
| def num_parameters(self, *, trainable_only: bool = False) -> int: | |
| """Return the total or trainable parameter count.""" | |
| return sum( | |
| parameter.numel() | |
| for parameter in self.parameters() | |
| if not trainable_only or parameter.requires_grad | |
| ) | |
| def _validate_coordinates(self, coordinates: Tensor) -> None: | |
| if coordinates.ndim != 4 or coordinates.shape[-1] != 3: | |
| raise ValueError("coordinates must have shape [B, L, A, 3].") | |
| if coordinates.shape[2] != self.num_atoms: | |
| raise ValueError( | |
| f"Expected {self.num_atoms} atoms per residue, " | |
| f"received {coordinates.shape[2]}." | |
| ) | |
| if not coordinates.is_floating_point(): | |
| raise TypeError("coordinates must use a floating-point dtype.") | |
| def encode( | |
| self, | |
| coordinates: Tensor, | |
| *, | |
| preprocess: bool = False, | |
| ) -> tuple[Tensor, Tensor, Tensor]: | |
| """Encode coordinates into continuous, quantized, and token states. | |
| Args: | |
| coordinates: Tensor shaped ``[B, L, A, 3]``. | |
| preprocess: If true, center Angstrom coordinates and convert to nm. | |
| Returns: | |
| ``(encoder_states, quantized_states, token_ids)`` with shapes | |
| ``[B, L, E]``, ``[B, L, D]``, and ``[B, L]``. | |
| """ | |
| self._validate_coordinates(coordinates) | |
| if preprocess: | |
| coordinates = coordinates - coordinates.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| coordinates = coordinates / 10.0 | |
| batch_size, sequence_length, num_atoms, _ = coordinates.shape | |
| centered_coordinates = coordinates - coordinates.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| pairwise_features = None | |
| if self.use_pairwise_bias: | |
| pairwise_features = self.pairwise_feature_embedder( | |
| centered_coordinates | |
| ) | |
| flattened_coordinates = centered_coordinates.reshape( | |
| batch_size, | |
| sequence_length, | |
| num_atoms * 3, | |
| ) | |
| encoder_states = self.coordinate_encoder(flattened_coordinates) | |
| encoder_states = self.encoder( | |
| encoder_states, | |
| pairwise_features=pairwise_features, | |
| ) | |
| quantized_states, token_ids = self.quantizer(encoder_states) | |
| return encoder_states, quantized_states, token_ids | |
| def _sampling_weights( | |
| noise_weight: WeightValue, | |
| score_weight: WeightValue, | |
| guidance_weight: WeightValue, | |
| *, | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| ) -> tuple[Tensor, Tensor, Tensor]: | |
| tensors = [ | |
| torch.as_tensor(weight, device=device, dtype=dtype).flatten() | |
| for weight in (noise_weight, score_weight, guidance_weight) | |
| ] | |
| setting_count = max(tensor.numel() for tensor in tensors) | |
| if setting_count == 0: | |
| raise ValueError("Sampling weights cannot be empty.") | |
| normalized: list[Tensor] = [] | |
| for tensor in tensors: | |
| if tensor.numel() == 1: | |
| tensor = tensor.expand(setting_count) | |
| elif tensor.numel() != setting_count: | |
| raise ValueError( | |
| "Non-scalar sampling weights must have equal lengths." | |
| ) | |
| normalized.append(tensor.reshape(setting_count, 1, 1, 1)) | |
| return normalized[0], normalized[1], normalized[2] | |
| def decode( | |
| self, | |
| token_ids: Tensor, | |
| *, | |
| num_steps: int = 200, | |
| noise_weight: WeightValue = 0.2, | |
| score_weight: WeightValue = 1.0, | |
| guidance_weight: WeightValue = 1.0, | |
| ) -> Tensor: | |
| """Generate centered nm coordinates from token IDs. | |
| One-dimensional weight inputs evaluate multiple sampling settings and | |
| return setting-major batches with shape ``[S * B, L, A, 3]``. | |
| """ | |
| if token_ids.ndim != 2: | |
| raise ValueError("token_ids must have shape [B, L].") | |
| if num_steps < 2: | |
| raise ValueError("num_steps must be at least 2.") | |
| if torch.any(token_ids < 0) or torch.any(token_ids >= self.codebook_size): | |
| raise ValueError( | |
| f"token_ids must be in [0, {self.codebook_size})." | |
| ) | |
| conditioning_states = self.quantizer.indices_to_codes(token_ids) | |
| device = conditioning_states.device | |
| dtype = conditioning_states.dtype | |
| original_batch_size, sequence_length, _ = conditioning_states.shape | |
| noise_weights, score_weights, guidance_weights = ( | |
| self._sampling_weights( | |
| noise_weight, | |
| score_weight, | |
| guidance_weight, | |
| device=device, | |
| dtype=dtype, | |
| ) | |
| ) | |
| setting_count = noise_weights.shape[0] | |
| if setting_count > 1: | |
| conditioning_states = conditioning_states.repeat( | |
| setting_count, | |
| 1, | |
| 1, | |
| ) | |
| batch_size = original_batch_size * setting_count | |
| coordinates = torch.randn( | |
| batch_size, | |
| sequence_length, | |
| self.num_atoms, | |
| 3, | |
| device=device, | |
| dtype=dtype, | |
| ) | |
| coordinates = coordinates - coordinates.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| time_steps = torch.linspace( | |
| 0, | |
| 1, | |
| num_steps, | |
| device=device, | |
| dtype=dtype, | |
| ) | |
| sampling_schedule = self.compute_sampling_schedule(time_steps) | |
| step_size = time_steps[1] - time_steps[0] | |
| if setting_count > 1 and original_batch_size > 1: | |
| noise_weights = noise_weights.repeat_interleave( | |
| original_batch_size, | |
| dim=0, | |
| ) | |
| score_weights = score_weights.repeat_interleave( | |
| original_batch_size, | |
| dim=0, | |
| ) | |
| guidance_weights = guidance_weights.repeat_interleave( | |
| original_batch_size, | |
| dim=0, | |
| ) | |
| for step_index, current_time in enumerate(time_steps): | |
| flattened_coordinates = coordinates.reshape( | |
| batch_size, | |
| sequence_length, | |
| self.num_atoms * 3, | |
| ) | |
| batch_times = current_time.expand(batch_size) | |
| conditional_vector_field = self.decoder( | |
| flattened_coordinates, | |
| batch_times, | |
| conditioning_states=conditioning_states, | |
| ).view(batch_size, sequence_length, self.num_atoms, 3) | |
| conditional_vector_field = ( | |
| conditional_vector_field | |
| - conditional_vector_field.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| ) | |
| unconditional_vector_field = self.decoder( | |
| flattened_coordinates, | |
| batch_times, | |
| conditioning_states=torch.zeros_like(conditioning_states), | |
| ).view(batch_size, sequence_length, self.num_atoms, 3) | |
| unconditional_vector_field = ( | |
| unconditional_vector_field | |
| - unconditional_vector_field.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| ) | |
| guided_vector_field = ( | |
| unconditional_vector_field | |
| + guidance_weights | |
| * ( | |
| conditional_vector_field | |
| - unconditional_vector_field | |
| ) | |
| ) | |
| if current_time.item() >= 0.99: | |
| coordinates = coordinates + guided_vector_field * step_size | |
| continue | |
| score_times = current_time.expand(coordinates.shape[:-1]) | |
| conditional_score = self.vector_field_to_score( | |
| coordinates, | |
| conditional_vector_field, | |
| score_times, | |
| ) | |
| unconditional_score = self.vector_field_to_score( | |
| coordinates, | |
| unconditional_vector_field, | |
| score_times, | |
| ) | |
| guided_score = ( | |
| unconditional_score | |
| + guidance_weights | |
| * (conditional_score - unconditional_score) | |
| ) | |
| noise = torch.randn_like(coordinates) | |
| noise = noise - noise.mean(dim=(1, 2), keepdim=True) | |
| noise_std = torch.sqrt( | |
| 2 | |
| * sampling_schedule[step_index] | |
| * noise_weights | |
| * step_size | |
| ) | |
| coordinate_delta = ( | |
| guided_vector_field | |
| + sampling_schedule[step_index] | |
| * guided_score | |
| * score_weights | |
| ) * step_size + noise_std * noise | |
| coordinates = coordinates + coordinate_delta | |
| return coordinates | |
| def forward( | |
| self, | |
| coordinates: Tensor, | |
| ) -> tuple[Tensor, dict[str, Tensor]]: | |
| """Compute token IDs and flow-matching training loss.""" | |
| self._validate_coordinates(coordinates) | |
| batch_size, sequence_length, num_atoms, _ = coordinates.shape | |
| centered_coordinates = coordinates - coordinates.mean( | |
| dim=(1, 2), | |
| keepdim=True, | |
| ) | |
| _, conditioning_states, token_ids = self.encode( | |
| centered_coordinates | |
| ) | |
| source_coordinates = torch.randn_like(centered_coordinates) | |
| source_coordinates = ( | |
| source_coordinates | |
| - source_coordinates.mean(dim=(1, 2), keepdim=True) | |
| ) | |
| times, intermediate_coordinates, target_vector_field = ( | |
| self.flow_matcher.sample_flow( | |
| source_coordinates, | |
| centered_coordinates, | |
| ) | |
| ) | |
| condition_mask = ( | |
| torch.rand( | |
| (batch_size,), | |
| device=centered_coordinates.device, | |
| ) | |
| > self.condition_dropout_probability | |
| )[:, None, None] | |
| conditioning_states = conditioning_states * condition_mask | |
| flattened_intermediate_coordinates = ( | |
| intermediate_coordinates.reshape( | |
| batch_size, | |
| sequence_length, | |
| num_atoms * 3, | |
| ) | |
| ) | |
| predicted_vector_field = self.decoder( | |
| flattened_intermediate_coordinates, | |
| times, | |
| conditioning_states=conditioning_states, | |
| ).reshape(batch_size, sequence_length, num_atoms, 3) | |
| flow_loss = ( | |
| (target_vector_field - predicted_vector_field) ** 2 | |
| ).mean() | |
| return token_ids, {"flow_loss": flow_loss} | |
| def compute_sampling_schedule( | |
| times: Tensor, | |
| mode: SamplingSchedule = "us", | |
| exponent: float = 1.0, | |
| maximum: float | None = None, | |
| epsilon: float = 1e-2, | |
| ) -> Tensor: | |
| """Compute a reverse-time sampling schedule.""" | |
| if times.ndim != 1: | |
| raise ValueError("times must be one-dimensional.") | |
| if exponent <= 0: | |
| raise ValueError("exponent must be positive.") | |
| if maximum is not None and maximum < 0: | |
| raise ValueError("maximum must be non-negative or None.") | |
| if epsilon <= 0: | |
| raise ValueError("epsilon must be positive.") | |
| def transform_schedule(schedule: Tensor, power: float) -> Tensor: | |
| if power == 1.0: | |
| return schedule | |
| log_schedule = torch.log(schedule) | |
| mean_log_schedule = torch.mean(log_schedule) | |
| centered_log_schedule = log_schedule - mean_log_schedule | |
| normalized = torch.sigmoid(centered_log_schedule).pow(power) | |
| reconstructed_centered_log = torch.logit( | |
| normalized, | |
| eps=1e-6, | |
| ) | |
| return torch.exp( | |
| reconstructed_centered_log + mean_log_schedule | |
| ) | |
| clamped_times = torch.clamp(times, 0, 1 - 1e-5) | |
| if mode == "us": | |
| schedule = ( | |
| (1.0 - clamped_times) / (clamped_times + epsilon) | |
| ) | |
| elif mode == "tan": | |
| angle = (1.0 - clamped_times) * torch.pi / 2.0 | |
| schedule = ( | |
| (torch.pi / 2.0) | |
| * torch.sin(angle) | |
| / (torch.cos(angle) + epsilon) | |
| ) | |
| elif mode == "1/t": | |
| schedule = 1.0 / (clamped_times + epsilon) | |
| else: | |
| raise ValueError(f"Unsupported sampling schedule mode: {mode}") | |
| schedule = transform_schedule(schedule, exponent) | |
| if maximum is not None: | |
| schedule = torch.clamp_max(schedule, maximum) | |
| return torch.clamp_min(schedule, 0) | |
| def vector_field_to_score( | |
| noisy_coordinates: Tensor, | |
| vector_field: Tensor, | |
| times: Tensor, | |
| reference_scale: float = 1.0, | |
| ) -> Tensor: | |
| """Convert a learned vector field into a noisy-density score.""" | |
| if noisy_coordinates.shape != vector_field.shape: | |
| raise ValueError( | |
| "noisy_coordinates and vector_field must have identical shapes." | |
| ) | |
| if reference_scale <= 0: | |
| raise ValueError("reference_scale must be positive.") | |
| if torch.any(times >= 1.0): | |
| raise ValueError("times must be strictly less than 1.") | |
| numerator = times[..., None] * vector_field - noisy_coordinates | |
| denominator = ( | |
| (1.0 - times)[..., None] * reference_scale**2 | |
| ) | |
| return numerator / denominator | |
| class PairwiseFeatureEmbedder(nn.Module): | |
| """Embed residue distances and relative sequence positions.""" | |
| def __init__( | |
| self, | |
| num_channels: int, | |
| num_distance_buckets: int, | |
| ) -> None: | |
| super().__init__() | |
| if num_channels <= 0 or num_distance_buckets < 2: | |
| raise ValueError( | |
| "num_channels must be positive and " | |
| "num_distance_buckets must be at least 2." | |
| ) | |
| self.distance_embedding = nn.Embedding( | |
| num_distance_buckets, | |
| num_channels, | |
| ) | |
| self.relative_position_embedding = nn.Embedding(128, num_channels) | |
| self.register_buffer( | |
| "bins", | |
| torch.linspace(0, 4**2, num_distance_buckets - 1), | |
| ) | |
| self.projection = FeedForward( | |
| num_channels, | |
| 4 * num_channels, | |
| num_channels, | |
| activation=nn.GELU, | |
| ) | |
| self.norm = nn.LayerNorm(num_channels) | |
| self.num_channels = num_channels | |
| def forward(self, coordinates: Tensor) -> Tensor: | |
| """Return pair features shaped ``[B, L, L, C]``.""" | |
| if coordinates.ndim != 4 or coordinates.shape[-1] != 3: | |
| raise ValueError("coordinates must have shape [B, L, A, 3].") | |
| sequence_length = coordinates.shape[1] | |
| residue_centers = coordinates.mean(dim=2) | |
| squared_distances = ( | |
| ( | |
| residue_centers[:, :, None] | |
| - residue_centers[:, None, :] | |
| ) | |
| ** 2 | |
| ).sum(dim=-1) | |
| residue_indices = torch.arange( | |
| sequence_length, | |
| device=residue_centers.device, | |
| ) | |
| relative_indices = ( | |
| (residue_indices[:, None] - residue_indices[None, :]) | |
| .clip(min=-64, max=63) | |
| + 64 | |
| ) | |
| relative_position_features = self.relative_position_embedding( | |
| relative_indices | |
| ) | |
| distance_buckets = torch.bucketize( | |
| squared_distances, | |
| self.bins, | |
| ) | |
| pairwise_features = ( | |
| self.distance_embedding(distance_buckets) | |
| + relative_position_features | |
| ) | |
| return self.projection(self.norm(pairwise_features)) | |